diff --git a/doc/src/asciidoc/module_dbflyway.adoc b/doc/src/asciidoc/module_dbflyway.adoc
index dca92230d..1bf2708ff 100644
--- a/doc/src/asciidoc/module_dbflyway.adoc
+++ b/doc/src/asciidoc/module_dbflyway.adoc
@@ -26,6 +26,54 @@ flyway:
<1> Sets `flyway.table` property, in this example, we override the new default value `flyway_schema_history` for
backward compatibility.
+The `flyway` subsystem can also be opened against a database selected by a
+jPOS-EE DB config modifier. This is useful for multi-tenant applications that
+register per-tenant DB configurations at runtime:
+
+[source,shell]
+-------------
+flyway --db entity-acme:tenant
+-------------
+
+For backward compatibility, a single positional argument is treated as the DB
+config modifier, so the following is equivalent:
+
+[source,shell]
+-------------
+flyway entity-acme:tenant
+-------------
+
+Entering the subsystem only records the target; it does not bootstrap Hibernate
+metadata or create a `SessionFactory`. The Flyway commands resolve the JDBC
+properties for the target DB and run Flyway directly.
+
+The Flyway history table can be selected independently of the DB target:
+
+[source,shell]
+-------------
+flyway --db entity-acme:tenant --table flyway_schema_history_jpts
+-------------
+
+or by using a prefix shorthand:
+
+[source,shell]
+-------------
+flyway --db entity-acme:tenant --prefix jpts
+-------------
+
+The prefix shorthand maps `jpts` to `jpts_schema_history`. Use `--table` when an
+application needs an exact history table name, such as
+`flyway_schema_history_jpts`.
+
+Inside the subsystem, commands are unchanged:
+
+[source,shell]
+-------------
+info
+migrate --out-of-order
+validate
+-------------
+
[NOTE]
======
By default, the `clean` command is disabled, unless `flyway.cleanDisabled: false` is placed in the environment.
@@ -46,14 +94,19 @@ The QBean descriptor looks like this:
[source,xml]
------------
- <1>
+ <1>
+ <2>
+ <3>
info
migrate
------------
-<1> Optional property used by Flyway migrate.
+<1> Optional DB config modifier. If omitted, Flyway uses the default DB configuration.
+<2> Optional exact Flyway history table name. `table-prefix` is also available
+ and maps `foo` to `foo_schema_history`.
+<3> Optional property used by Flyway migrate.
Valid commands are:
@@ -77,4 +130,3 @@ These Flyway commands use stdout for their output. We recommend to add
to your 00_logger.xml configuration in order to get unified logs.
======
-
diff --git a/modules/db-flyway/src/main/java/org/jpos/flyway/FlywayService.java b/modules/db-flyway/src/main/java/org/jpos/flyway/FlywayService.java
index f71a576cc..4b05db23d 100644
--- a/modules/db-flyway/src/main/java/org/jpos/flyway/FlywayService.java
+++ b/modules/db-flyway/src/main/java/org/jpos/flyway/FlywayService.java
@@ -37,7 +37,7 @@ protected void initService() {
String currentCommand = "init";
try {
FlywaySupport support = new FlywaySupport();
- Flyway flyway = support.getFlyway(cfg.get("config-modifier", null),
+ Flyway flyway = support.getFlyway(FlywayTarget.fromConfiguration(cfg),
cfg.getBoolean("out-of-order") ? new String[] { "--out-of-order" } : new String[] {}
);
dbId = support.getDbId();
diff --git a/modules/db-flyway/src/main/java/org/jpos/flyway/FlywaySupport.java b/modules/db-flyway/src/main/java/org/jpos/flyway/FlywaySupport.java
index 85cadb915..e0b141dc4 100644
--- a/modules/db-flyway/src/main/java/org/jpos/flyway/FlywaySupport.java
+++ b/modules/db-flyway/src/main/java/org/jpos/flyway/FlywaySupport.java
@@ -38,9 +38,9 @@ public class FlywaySupport implements LogCreator, Log {
private String dbId = "";
- protected Flyway getFlyway(String configModifier, String args[]) {
+ protected Flyway getFlyway(FlywayTarget target, String args[]) {
LogFactory.setFallbackLogCreator(this);
- Properties p = new DB(configModifier).getProperties();
+ Properties p = getDbProperties(target);
FluentConfiguration config = Flyway.configure()
.locations("classpath:db/migration")
@@ -50,16 +50,37 @@ protected Flyway getFlyway(String configModifier, String args[]) {
p.getProperty("hibernate.connection.username"),
p.getProperty("hibernate.connection.password"))
.outOfOrder(has(args, "--out-of-order"));
+ String historyTable = target.historyTable();
+ if (historyTable != null)
+ config.table(historyTable);
Flyway flyway = config.load();
- this.dbId = resolveDbId(configModifier, flyway);
+ this.dbId = resolveDbId(target, flyway);
return flyway;
}
+ protected Flyway getFlyway(String configModifier, String args[]) {
+ return getFlyway(new FlywayTarget(configModifier, null, null), args);
+ }
+
public String getDbId() {
return dbId;
}
- private String resolveDbId(String configModifier, Flyway flyway) {
+ private Properties getDbProperties(FlywayTarget target) {
+ try {
+ Properties p = DB.getProperties(target.configModifier());
+ if (p.getProperty("hibernate.connection.url") == null)
+ throw new IllegalArgumentException(
+ "Missing hibernate.connection.url for Flyway target " + target.label()
+ );
+ return p;
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Unable to resolve Flyway DB properties for target "
+ + target.label(), e);
+ }
+ }
+
+ private String resolveDbId(FlywayTarget target, Flyway flyway) {
String catalog = null;
try (Connection c = flyway.getConfiguration().getDataSource().getConnection()) {
catalog = c.getCatalog();
@@ -72,9 +93,11 @@ private String resolveDbId(String configModifier, Flyway flyway) {
catalog = extractDbFromUrl(flyway.getConfiguration().getUrl());
if (catalog == null || catalog.isEmpty())
catalog = "?";
- return (configModifier != null && !configModifier.isEmpty())
- ? configModifier + "@" + catalog
+ String db = (target.configModifier() != null && !target.configModifier().isEmpty())
+ ? target.configModifier() + "@" + catalog
: catalog;
+ String historyTable = target.historyTable();
+ return historyTable != null ? db + "/" + historyTable : db;
}
private String extractDbFromUrl(String url) {
diff --git a/modules/db-flyway/src/main/java/org/jpos/flyway/FlywayTarget.java b/modules/db-flyway/src/main/java/org/jpos/flyway/FlywayTarget.java
new file mode 100644
index 000000000..391d0e33e
--- /dev/null
+++ b/modules/db-flyway/src/main/java/org/jpos/flyway/FlywayTarget.java
@@ -0,0 +1,93 @@
+/*
+ * jPOS Project [http://jpos.org]
+ * Copyright (C) 2000-2026 jPOS Software SRL
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jpos.flyway;
+
+import org.jpos.core.Configuration;
+
+public record FlywayTarget(String configModifier, String table, String tablePrefix) {
+ public static final FlywayTarget DEFAULT = new FlywayTarget(null, null, null);
+
+ public static FlywayTarget fromCli(String[] args) {
+ String configModifier = null;
+ String table = null;
+ String tablePrefix = null;
+ for (int i = 1; i < args.length; i++) {
+ String arg = args[i];
+ if ("--db".equals(arg) || "--config-modifier".equals(arg)) {
+ configModifier = value(args, ++i, arg);
+ } else if (arg.startsWith("--db=")) {
+ configModifier = arg.substring("--db=".length());
+ } else if (arg.startsWith("--config-modifier=")) {
+ configModifier = arg.substring("--config-modifier=".length());
+ } else if ("--table".equals(arg)) {
+ table = value(args, ++i, arg);
+ } else if (arg.startsWith("--table=")) {
+ table = arg.substring("--table=".length());
+ } else if ("--prefix".equals(arg) || "--table-prefix".equals(arg)) {
+ tablePrefix = value(args, ++i, arg);
+ } else if (arg.startsWith("--prefix=")) {
+ tablePrefix = arg.substring("--prefix=".length());
+ } else if (arg.startsWith("--table-prefix=")) {
+ tablePrefix = arg.substring("--table-prefix=".length());
+ } else if (!arg.startsWith("--") && configModifier == null) {
+ configModifier = arg;
+ } else {
+ throw new IllegalArgumentException("Invalid flyway option '" + arg + "'");
+ }
+ }
+ return new FlywayTarget(blankToNull(configModifier), blankToNull(table), blankToNull(tablePrefix));
+ }
+
+ public static FlywayTarget fromConfiguration(Configuration cfg) {
+ return new FlywayTarget(
+ blankToNull(cfg.get("config-modifier", null)),
+ blankToNull(cfg.get("table", null)),
+ blankToNull(cfg.get("table-prefix", null))
+ );
+ }
+
+ public String historyTable() {
+ if (table != null)
+ return table;
+ return tablePrefix != null ? tablePrefix + "_schema_history" : null;
+ }
+
+ public String label() {
+ StringBuilder sb = new StringBuilder();
+ if (configModifier != null)
+ sb.append(configModifier);
+ String historyTable = historyTable();
+ if (historyTable != null) {
+ if (sb.length() > 0)
+ sb.append('/');
+ sb.append(historyTable);
+ }
+ return sb.length() == 0 ? "" : sb.toString();
+ }
+
+ private static String value(String[] args, int index, String option) {
+ if (index >= args.length)
+ throw new IllegalArgumentException("Missing value for " + option);
+ return args[index];
+ }
+
+ private static String blankToNull(String s) {
+ return s == null || s.isBlank() ? null : s;
+ }
+}
diff --git a/modules/db-flyway/src/main/java/org/jpos/q2/cli/FLYWAY.java b/modules/db-flyway/src/main/java/org/jpos/q2/cli/FLYWAY.java
index b95503a9c..68d269266 100644
--- a/modules/db-flyway/src/main/java/org/jpos/q2/cli/FLYWAY.java
+++ b/modules/db-flyway/src/main/java/org/jpos/q2/cli/FLYWAY.java
@@ -18,27 +18,19 @@
package org.jpos.q2.cli;
-import org.flywaydb.core.Flyway;
-import org.jpos.ee.DB;
+import org.jpos.flyway.FlywayTarget;
import org.jpos.q2.CLIContext;
import org.jpos.q2.CLISubSystem;
-import java.util.Properties;
-
public class FLYWAY implements CLISubSystem {
- public static final String PREFIX = "flyway.dbmodifier";
+ public static final String TARGET = "flyway.target";
@Override
public String getPrompt(CLIContext ctx, String[] args) {
- String prefix = null;
- if (args.length > 1) {
- prefix = args[1];
- ctx.getUserData().put(PREFIX, prefix);
- } else {
- ctx.getUserData().remove(PREFIX);
- }
- new DB(prefix); // force DB initialization
- return "flyway" + (prefix != null ? "[" + args[1] + "]" : "") + "> ";
+ FlywayTarget target = FlywayTarget.fromCli(args);
+ ctx.getUserData().put(TARGET, target);
+ String label = target.label();
+ return "flyway" + (!label.isEmpty() ? "[" + label + "]" : "") + "> ";
}
@Override
@@ -46,11 +38,8 @@ public String[] getCompletionPrefixes(CLIContext ctx, String[] args) {
return new String[] { "org.jpos.q2.cli.flyway." };
}
- private Flyway getFlyWay() {
- Properties p = new DB().getProperties();
- return Flyway.configure().dataSource(
- p.getProperty("hibernate.connection.url"),
- p.getProperty("hibernate.connection.username"),
- p.getProperty("hibernate.connection.password")).load();
+ public static FlywayTarget getTarget(CLIContext ctx) {
+ FlywayTarget target = (FlywayTarget) ctx.getUserData().get(TARGET);
+ return target != null ? target : FlywayTarget.DEFAULT;
}
}
diff --git a/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/BASELINE.java b/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/BASELINE.java
index f7bbb1b33..ab4c40aef 100644
--- a/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/BASELINE.java
+++ b/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/BASELINE.java
@@ -27,7 +27,7 @@ public class BASELINE extends FlywaySupport implements CLICommand{
@Override
public void exec(CLIContext cli, String[] args) {
try {
- getFlyway((String) cli.getUserData().get(FLYWAY.PREFIX), args).baseline();
+ getFlyway(FLYWAY.getTarget(cli), args).baseline();
} catch (Exception e) {
cli.println(e.getMessage());
}
diff --git a/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/CLEAN.java b/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/CLEAN.java
index 910884136..8b7c702c3 100644
--- a/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/CLEAN.java
+++ b/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/CLEAN.java
@@ -39,7 +39,7 @@ public void exec(CLIContext cli, String[] args) {
if (superSure) {
try {
- getFlyway((String) cli.getUserData().get(FLYWAY.PREFIX), args).clean();
+ getFlyway(FLYWAY.getTarget(cli), args).clean();
} catch (FlywayException e) {
cli.println (e.getMessage());
}
diff --git a/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/INFO.java b/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/INFO.java
index 2097bee87..f1b70991d 100644
--- a/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/INFO.java
+++ b/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/INFO.java
@@ -32,7 +32,7 @@ public class INFO extends FlywaySupport implements CLICommand{
@Override
public void exec(CLIContext cli, String[] args) {
try {
- Flyway flyway = getFlyway((String) cli.getUserData().get(FLYWAY.PREFIX), args);
+ Flyway flyway = getFlyway(FLYWAY.getTarget(cli), args);
MigrationInfoService info = flyway.info();
MigrationInfo current = info.current();
MigrationVersion currentSchemaVersion = current == null ? MigrationVersion.EMPTY : current.getVersion();
diff --git a/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/MIGRATE.java b/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/MIGRATE.java
index 0e6ef5b20..82c61cc08 100644
--- a/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/MIGRATE.java
+++ b/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/MIGRATE.java
@@ -28,7 +28,7 @@ public class MIGRATE extends FlywaySupport implements CLICommand{
@Override
public void exec(CLIContext cli, String[] args) {
try {
- Flyway flyway = getFlyway((String) cli.getUserData().get(FLYWAY.PREFIX), args);
+ Flyway flyway = getFlyway(FLYWAY.getTarget(cli), args);
var result = flyway.migrate();
cli.println ("Applied " + result.migrationsExecuted + " migration(s)");
} catch (Exception e) {
diff --git a/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/REPAIR.java b/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/REPAIR.java
index 0c8cc9e49..d02843fc4 100644
--- a/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/REPAIR.java
+++ b/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/REPAIR.java
@@ -27,7 +27,7 @@ public class REPAIR extends FlywaySupport implements CLICommand{
@Override
public void exec(CLIContext cli, String[] args) {
try {
- getFlyway((String) cli.getUserData().get(FLYWAY.PREFIX), args).repair();
+ getFlyway(FLYWAY.getTarget(cli), args).repair();
} catch (Exception e) {
cli.println(e.getMessage());
}
diff --git a/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/VALIDATE.java b/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/VALIDATE.java
index b07f22af7..40cbf920a 100644
--- a/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/VALIDATE.java
+++ b/modules/db-flyway/src/main/java/org/jpos/q2/cli/flyway/VALIDATE.java
@@ -27,7 +27,7 @@ public class VALIDATE extends FlywaySupport implements CLICommand{
@Override
public void exec(CLIContext cli, String[] args) {
try {
- getFlyway((String) cli.getUserData().get(FLYWAY.PREFIX), args).validate();
+ getFlyway(FLYWAY.getTarget(cli), args).validate();
} catch (Exception e) {
cli.println(e.getMessage());
}
diff --git a/modules/db-flyway/src/test/java/org/jpos/flyway/FlywayTargetTest.java b/modules/db-flyway/src/test/java/org/jpos/flyway/FlywayTargetTest.java
new file mode 100644
index 000000000..fd3d3b3b9
--- /dev/null
+++ b/modules/db-flyway/src/test/java/org/jpos/flyway/FlywayTargetTest.java
@@ -0,0 +1,57 @@
+/*
+ * jPOS Project [http://jpos.org]
+ * Copyright (C) 2000-2026 jPOS Software SRL
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jpos.flyway;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class FlywayTargetTest {
+ @Test
+ void parsesLegacyDbModifier() {
+ FlywayTarget target = FlywayTarget.fromCli(new String[] { "flyway", "entity-acme:tenant" });
+
+ assertEquals("entity-acme:tenant", target.configModifier());
+ assertEquals("entity-acme:tenant", target.label());
+ }
+
+ @Test
+ void parsesNamedDbAndTable() {
+ FlywayTarget target = FlywayTarget.fromCli(new String[] {
+ "flyway",
+ "--db", "entity-acme:tenant",
+ "--table", "flyway_schema_history_jpts"
+ });
+
+ assertEquals("entity-acme:tenant", target.configModifier());
+ assertEquals("flyway_schema_history_jpts", target.historyTable());
+ assertEquals("entity-acme:tenant/flyway_schema_history_jpts", target.label());
+ }
+
+ @Test
+ void parsesPrefixAsHistoryTablePrefix() {
+ FlywayTarget target = FlywayTarget.fromCli(new String[] {
+ "flyway",
+ "--db=entity-acme:tenant",
+ "--prefix", "jpts"
+ });
+
+ assertEquals("jpts_schema_history", target.historyTable());
+ }
+}
diff --git a/modules/dbsupport/src/main/java/org/jpos/ee/DB.java b/modules/dbsupport/src/main/java/org/jpos/ee/DB.java
index bff7dcea6..fc65fb0fa 100644
--- a/modules/dbsupport/src/main/java/org/jpos/ee/DB.java
+++ b/modules/dbsupport/src/main/java/org/jpos/ee/DB.java
@@ -227,6 +227,53 @@ public Properties getProperties() {
return properties.get(cm);
}
+ /**
+ * Resolves Hibernate database properties for a config modifier without
+ * building Hibernate metadata or a SessionFactory.
+ *
+ *
This is intended for tools such as Flyway that only need JDBC
+ * connection settings. It follows the same modifier rules used by
+ * {@link #DB(String)}: registered runtime properties first, then the
+ * modifier-specific {@code DB_PROPERTIES} system property / file fallback,
+ * with DBInstantiator username/password overrides from {@code tspace:dbconfig}.