+
SqlJam
Move tables across databases. Schema, data and all.
A desktop import/export tool that copies tables, indexes, constraints, sequences, partitions and rows between relational and OLAP databases, directly, as copies in the same schema, or through a self-describing SQL, Parquet or CSV export package you can import later. Imports into a database run in one transaction which an error rolls back, and rows are inserted, updated or skipped by key.
diff --git a/pom.xml b/pom.xml
index 120e122..e320645 100644
--- a/pom.xml
+++ b/pom.xml
@@ -652,5 +652,39 @@
+
+
+ app
+
+ mac-aarch64
+
+
+
+
+ org.apache.maven.plugins
+ maven-antrun-plugin
+
+
+ app
+ package
+
+ run
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/bin/sqljam.png b/src/bin/sqljam.png
index 9bb5e80..98386da 100644
Binary files a/src/bin/sqljam.png and b/src/bin/sqljam.png differ
diff --git a/src/main/java/com/github/sqljam/face/service/JdbcUrls.java b/src/main/java/com/github/sqljam/face/service/JdbcUrls.java
new file mode 100644
index 0000000..116020a
--- /dev/null
+++ b/src/main/java/com/github/sqljam/face/service/JdbcUrls.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2023-2026 Fred Feng
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.github.sqljam.face.service;
+
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.StringUtils;
+import com.github.sqljam.face.model.ConnectionProfile;
+import com.github.sqljam.impexp.DbType;
+
+/**
+ * @Description: JdbcUrls reads the database type, host, port and database of a jdbc url, and builds the url of them,
+ * so that the fields of a data source and its url follow each other like DBeaver does
+ * @Author: Fred Feng
+ * @Date: 18/10/2026
+ * @Version 1.0.0
+ */
+public final class JdbcUrls {
+
+ /**
+ * jdbc:mysql://host:port/db?..., jdbc:mariadb, jdbc:postgresql, jdbc:db2 (db:props;) and ClickHouse alike
+ */
+ private static final Pattern SERVER = Pattern.compile(
+ "(?i)^jdbc:(?:mysql|mariadb|postgresql|db2|clickhouse|ch):(?:https?:)?//([^/:?;\\\\]+)(?::(\\d+))?"
+ + "(?:/([^?;:]*))?.*$");
+ /**
+ * jdbc:oracle:thin:@//host:port/service, @host:port/service and @host:port:SID
+ */
+ private static final Pattern ORACLE = Pattern.compile(
+ "(?i)^jdbc:oracle:thin:@(?://)?([^/:]+)(?::(\\d+))?[/:]([^?]+?)(?:\\?.*)?$");
+ /**
+ * jdbc:sqlserver://host\instance:port;databaseName=db;...
+ */
+ private static final Pattern SQLSERVER = Pattern.compile(
+ "(?i)^jdbc:sqlserver://([^;:\\\\]+)(?:\\\\[^;:]+)?(?::(\\d+))?(;.*)?$");
+ private static final Pattern SQLSERVER_DATABASE = Pattern.compile("(?i);(?:databaseName|database)=([^;]+)");
+ private static final Pattern H2_SERVER = Pattern.compile("(?i)^jdbc:h2:(?:tcp|ssl)://([^/:]+)(?::(\\d+))?/(.+)$");
+
+ private JdbcUrls() {
+ }
+
+ /**
+ * Database type, host, port and database of the url, null if it is not a url of a supported database
+ */
+ public static ConnectionProfile parse(String url) {
+ DbType dbType = DbType.forUrl(StringUtils.trim(url));
+ if (dbType == null) {
+ return null;
+ }
+ String text = url.trim();
+ ConnectionProfile profile = new ConnectionProfile();
+ profile.setDbType(dbType);
+ profile.setPort(dbType.getDefaultPort());
+ Matcher matcher;
+ switch (dbType) {
+ case ORACLE:
+ matcher = ORACLE.matcher(text);
+ // A TNS descriptor, e.g. @(DESCRIPTION=...), is kept as the url only
+ if (!matcher.matches()) {
+ return profile;
+ }
+ return fill(profile, matcher.group(1), matcher.group(2), matcher.group(3));
+ case SQLSERVER:
+ matcher = SQLSERVER.matcher(text);
+ if (!matcher.matches()) {
+ return profile;
+ }
+ Matcher database = SQLSERVER_DATABASE.matcher(StringUtils.defaultString(matcher.group(3)));
+ return fill(profile, matcher.group(1), matcher.group(2), database.find() ? database.group(1) : null);
+ case H2:
+ matcher = H2_SERVER.matcher(text);
+ if (matcher.matches()) {
+ return fill(profile, matcher.group(1), matcher.group(2), matcher.group(3));
+ }
+ // jdbc:h2:file:path, jdbc:h2:path and jdbc:h2:mem:name, options follow a semicolon
+ String path = StringUtils.substringBefore(text.substring("jdbc:h2:".length()), ";");
+ if (path.toLowerCase(Locale.ENGLISH).startsWith("file:")) {
+ path = path.substring("file:".length());
+ }
+ return fill(profile, null, null, path);
+ case SQLITE:
+ return fill(profile, null, null, StringUtils.substringBefore(text.substring("jdbc:sqlite:".length()),
+ "?"));
+ case DUCKDB:
+ return fill(profile, null, null, StringUtils.substringBefore(text.substring("jdbc:duckdb:".length()),
+ "?"));
+ default:
+ matcher = SERVER.matcher(text);
+ if (!matcher.matches()) {
+ return profile;
+ }
+ return fill(profile, matcher.group(1), matcher.group(2), matcher.group(3));
+ }
+ }
+
+ private static ConnectionProfile fill(ConnectionProfile profile, String host, String port, String database) {
+ profile.setHostname(StringUtils.trimToNull(host));
+ if (StringUtils.isNotBlank(port)) {
+ profile.setPort(Integer.parseInt(port));
+ } else if (profile.getHostname() == null) {
+ // Files have no port
+ profile.setPort(0);
+ }
+ profile.setDatabase(StringUtils.trimToNull(database));
+ return profile;
+ }
+
+ /**
+ * Url of the fields, while they are being entered: a missing host or a required database is left empty
+ */
+ public static String build(DbType dbType, String host, int port, String database) {
+ if (dbType == null) {
+ return "";
+ }
+ String hostname = StringUtils.trimToNull(host);
+ boolean file = dbType.isFileBased() && hostname == null;
+ if (file && StringUtils.isBlank(database)) {
+ return "";
+ }
+ // A placeholder passes the checks of required databases, it is removed from the url
+ String placeholder = "\u0000";
+ String url = dbType.getUrl(file ? null : StringUtils.defaultString(hostname), port,
+ StringUtils.isBlank(database) ? placeholder : database.trim());
+ return url.replace(placeholder, "");
+ }
+}
diff --git a/src/main/java/com/github/sqljam/face/view/ConnectionForm.java b/src/main/java/com/github/sqljam/face/view/ConnectionForm.java
index 905563a..5499cad 100644
--- a/src/main/java/com/github/sqljam/face/view/ConnectionForm.java
+++ b/src/main/java/com/github/sqljam/face/view/ConnectionForm.java
@@ -22,6 +22,7 @@
import org.apache.commons.lang3.StringUtils;
import com.github.sqljam.face.model.ConnectionProfile;
import com.github.sqljam.face.service.DatabaseSession;
+import com.github.sqljam.face.service.JdbcUrls;
import com.github.sqljam.impexp.DbType;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
@@ -73,6 +74,10 @@ public class ConnectionForm extends VBox {
private final HBox hostBox;
private ConnectionProfile profile = new ConnectionProfile();
+ /**
+ * The url and the fields are being updated by each other
+ */
+ private boolean syncing;
public ConnectionForm() {
setSpacing(10);
@@ -139,6 +144,13 @@ public ConnectionForm() {
grid.add(rememberPasswordCheck, 1, row++);
grid.addRow(row, new Label(Messages.get("connection.url")), urlField);
+ // The url follows the fields, a pasted or edited url fills the fields, like DBeaver
+ hostField.textProperty().addListener((obs, oldText, text) -> updateUrl());
+ portField.textProperty().addListener((obs, oldText, text) -> updateUrl());
+ databaseField.textProperty().addListener((obs, oldText, text) -> updateUrl());
+ dbTypeCombo.valueProperty().addListener((obs, oldType, newType) -> updateUrl());
+ urlField.textProperty().addListener((obs, oldText, text) -> updateFields(text));
+
HBox statusBox = new HBox(8, progressIndicator, statusLabel);
statusBox.setPadding(new Insets(4, 0, 0, 0));
getChildren().addAll(grid, statusBox);
@@ -177,6 +189,52 @@ private void onDbTypeChanged(DbType oldType, DbType newType) {
}
}
+ /**
+ * Url of the fields
+ */
+ private String buildUrl() {
+ return JdbcUrls.build(dbTypeCombo.getValue(), hostField.getText(), StringUtils.isNumeric(portField.getText())
+ ? Integer.parseInt(portField.getText()) : 0, databaseField.getText());
+ }
+
+ private void updateUrl() {
+ if (syncing) {
+ return;
+ }
+ syncing = true;
+ try {
+ urlField.setText(buildUrl());
+ } finally {
+ syncing = false;
+ }
+ }
+
+ /**
+ * Database type, host, port and database of an entered url, other urls leave the fields as they are
+ */
+ private void updateFields(String url) {
+ if (syncing) {
+ return;
+ }
+ ConnectionProfile parsed = JdbcUrls.parse(url);
+ if (parsed == null) {
+ return;
+ }
+ syncing = true;
+ try {
+ dbTypeCombo.setValue(parsed.getDbType());
+ // A url being typed (e.g. jdbc:mysql:) has no host or database yet, the fields are kept
+ if (parsed.getHostname() == null && parsed.getDatabase() == null) {
+ return;
+ }
+ hostField.setText(StringUtils.defaultString(parsed.getHostname()));
+ portField.setText(parsed.getPort() > 0 ? String.valueOf(parsed.getPort()) : "");
+ databaseField.setText(StringUtils.defaultString(parsed.getDatabase()));
+ } finally {
+ syncing = false;
+ }
+ }
+
private void browseFile() {
FileChooser chooser = new FileChooser();
chooser.setTitle(databaseLabel.getText());
@@ -248,7 +306,12 @@ public void setProfile(ConnectionProfile profile, boolean passwordRemembered) {
usernameField.setText(StringUtils.defaultString(profile.getUsername()));
passwordField.setText(StringUtils.defaultString(profile.getPassword()));
rememberPasswordCheck.setSelected(passwordRemembered);
- urlField.setText(StringUtils.defaultString(profile.getUrl()));
+ syncing = true;
+ try {
+ urlField.setText(StringUtils.isNotBlank(profile.getUrl()) ? profile.getUrl() : buildUrl());
+ } finally {
+ syncing = false;
+ }
clearStatus();
}
@@ -264,7 +327,9 @@ public ConnectionProfile getProfile() {
result.setDatabase(StringUtils.trimToNull(databaseField.getText()));
result.setUsername(StringUtils.trimToNull(usernameField.getText()));
result.setPassword(passwordField.getText());
- result.setUrl(StringUtils.trimToNull(urlField.getText()));
+ // The url of the fields is not kept, a url with its own options is
+ String url = StringUtils.trimToNull(urlField.getText());
+ result.setUrl(url == null || url.equals(buildUrl()) ? null : url);
String name = StringUtils.trimToNull(nameField.getText());
if (name == null) {
name = buildDefaultName(result);
diff --git a/src/main/resources/com/github/sqljam/face/images/icon-128.png b/src/main/resources/com/github/sqljam/face/images/icon-128.png
index 3d7a893..6706d89 100644
Binary files a/src/main/resources/com/github/sqljam/face/images/icon-128.png and b/src/main/resources/com/github/sqljam/face/images/icon-128.png differ
diff --git a/src/main/resources/com/github/sqljam/face/images/icon-16.png b/src/main/resources/com/github/sqljam/face/images/icon-16.png
index 8a7ded7..ab04635 100644
Binary files a/src/main/resources/com/github/sqljam/face/images/icon-16.png and b/src/main/resources/com/github/sqljam/face/images/icon-16.png differ
diff --git a/src/main/resources/com/github/sqljam/face/images/icon-24.png b/src/main/resources/com/github/sqljam/face/images/icon-24.png
index 6da8582..48c3e12 100644
Binary files a/src/main/resources/com/github/sqljam/face/images/icon-24.png and b/src/main/resources/com/github/sqljam/face/images/icon-24.png differ
diff --git a/src/main/resources/com/github/sqljam/face/images/icon-256.png b/src/main/resources/com/github/sqljam/face/images/icon-256.png
index 6ce202c..93da296 100644
Binary files a/src/main/resources/com/github/sqljam/face/images/icon-256.png and b/src/main/resources/com/github/sqljam/face/images/icon-256.png differ
diff --git a/src/main/resources/com/github/sqljam/face/images/icon-32.png b/src/main/resources/com/github/sqljam/face/images/icon-32.png
index 0a0b373..a1c18ed 100644
Binary files a/src/main/resources/com/github/sqljam/face/images/icon-32.png and b/src/main/resources/com/github/sqljam/face/images/icon-32.png differ
diff --git a/src/main/resources/com/github/sqljam/face/images/icon-48.png b/src/main/resources/com/github/sqljam/face/images/icon-48.png
index 07abc00..fb9bcb7 100644
Binary files a/src/main/resources/com/github/sqljam/face/images/icon-48.png and b/src/main/resources/com/github/sqljam/face/images/icon-48.png differ
diff --git a/src/main/resources/com/github/sqljam/face/images/icon-512.png b/src/main/resources/com/github/sqljam/face/images/icon-512.png
index 9bb5e80..98386da 100644
Binary files a/src/main/resources/com/github/sqljam/face/images/icon-512.png and b/src/main/resources/com/github/sqljam/face/images/icon-512.png differ
diff --git a/src/main/resources/com/github/sqljam/face/images/icon-64.png b/src/main/resources/com/github/sqljam/face/images/icon-64.png
index 8bdfd10..745a2fe 100644
Binary files a/src/main/resources/com/github/sqljam/face/images/icon-64.png and b/src/main/resources/com/github/sqljam/face/images/icon-64.png differ
diff --git a/src/main/resources/com/github/sqljam/face/images/logo-mark.png b/src/main/resources/com/github/sqljam/face/images/logo-mark.png
index d5d0292..1b17052 100644
Binary files a/src/main/resources/com/github/sqljam/face/images/logo-mark.png and b/src/main/resources/com/github/sqljam/face/images/logo-mark.png differ
diff --git a/src/main/resources/com/github/sqljam/face/images/logo.png b/src/main/resources/com/github/sqljam/face/images/logo.png
index 34a5808..8373173 100644
Binary files a/src/main/resources/com/github/sqljam/face/images/logo.png and b/src/main/resources/com/github/sqljam/face/images/logo.png differ
diff --git a/src/packaging/macos/SqlJam.icns b/src/packaging/macos/SqlJam.icns
new file mode 100644
index 0000000..8f23aff
Binary files /dev/null and b/src/packaging/macos/SqlJam.icns differ
diff --git a/src/packaging/package.sh b/src/packaging/package.sh
new file mode 100755
index 0000000..2021d10
--- /dev/null
+++ b/src/packaging/package.sh
@@ -0,0 +1,112 @@
+#!/bin/sh
+#
+# Packages SqlJam for download with jpackage (JDK 17 or later), after mvn package, on the system of the platform
+# (jpackage builds for the system it runs on):
+#
+# sh src/packaging/package.sh
[version] platform: mac-aarch64, mac, win or linux
+#
+# mac-aarch64, mac SqlJam.app in SqlJam--.dmg
+# win SqlJam--win.zip, SqlJam\SqlJam.exe without installing
+# linux SqlJam--linux.tar.gz (SqlJam/bin/SqlJam) and sqljam__amd64.deb
+# every platform SqlJam---jar.zip, the runnable jar with its launch scripts (Java 17+)
+#
+# The packages have their own Java runtime and the JVM options of sqljam.vmoptions. They are not signed with a
+# certificate: macOS opens a downloaded app by System Settings > Privacy & Security > Open Anyway, Windows by
+# More info > Run anyway.
+#
+set -e
+
+PLATFORM=${1:?platform: mac-aarch64, mac, win or linux}
+DIR=$(cd "$(dirname "$0")/../.." && pwd)
+VERSION=${2:-$(sed -n 's:^ \(.*\):\1:p' "$DIR/pom.xml" | head -1)}
+JAR="sqljam-$VERSION-$PLATFORM.jar"
+# jpackage takes numbers only, e.g. 1.0.0 of 1.0.0-SNAPSHOT
+APP_VERSION=$(echo "$VERSION" | sed 's/[^0-9.].*$//')
+WORK="$DIR/target/package-$PLATFORM"
+OUT="$DIR/bin"
+
+if [ ! -f "$OUT/$JAR" ]; then
+ echo "Runnable jar not found: $OUT/$JAR, run mvn package first" >&2
+ exit 1
+fi
+
+rm -rf "$WORK"
+mkdir -p "$WORK/input" "$WORK/portable"
+# sqljam.properties is read next to the jar
+cp "$OUT/$JAR" "$OUT/sqljam.properties" "$WORK/input/"
+
+# The runnable jar with its launch scripts, for a Java runtime of the user
+cp "$OUT/$JAR" "$OUT/sqljam.properties" "$OUT/sqljam.vmoptions" "$OUT/sqljam.png" "$WORK/portable/"
+case "$PLATFORM" in
+ win) cp "$OUT/sqljam.bat" "$WORK/portable/" ;;
+ *) cp "$OUT/sqljam.sh" "$WORK/portable/" ;;
+esac
+rm -f "$OUT/SqlJam-$VERSION-$PLATFORM-jar.zip"
+(cd "$WORK" && mv portable sqljam && jar --create --no-manifest --file "$OUT/SqlJam-$VERSION-$PLATFORM-jar.zip" sqljam)
+
+# JVM options of the launcher; they are applied already, SqlJam does not start another java process
+set --
+while IFS= read -r line; do
+ line=$(echo "$line" | tr -d '\r')
+ case "$line" in
+ ''|\#*) ;;
+ *) set -- "$@" --java-options "$line" ;;
+ esac
+done < "$OUT/sqljam.vmoptions"
+set -- "$@" --java-options "-Dsqljam.vmoptions.applied=true"
+
+case "$PLATFORM" in
+ mac*) set -- "$@" --icon "$DIR/src/packaging/macos/SqlJam.icns" --mac-package-identifier com.github.sqljam \
+ --mac-package-name SqlJam ;;
+ win) set -- "$@" --icon "$DIR/src/packaging/windows/SqlJam.ico" ;;
+ linux) set -- "$@" --icon "$OUT/sqljam.png" ;;
+ *) echo "Unknown platform: $PLATFORM" >&2; exit 1 ;;
+esac
+
+jpackage --type app-image \
+ --dest "$WORK" \
+ --input "$WORK/input" \
+ --main-jar "$JAR" \
+ --main-class com.github.sqljam.face.Launcher \
+ --name SqlJam \
+ --app-version "$APP_VERSION" \
+ --vendor "Fred Feng" \
+ --copyright "Copyright 2023-2026 Fred Feng, Apache License 2.0" \
+ "$@"
+
+case "$PLATFORM" in
+ mac*)
+ APP="$WORK/SqlJam.app"
+ PLIST="$APP/Contents/Info.plist"
+ # Databases of the local network are refused without the permission of the local network (macOS 15+)
+ plist() {
+ /usr/libexec/PlistBuddy -c "Set :$1 $2" "$PLIST" 2>/dev/null \
+ || /usr/libexec/PlistBuddy -c "Add :$1 string $2" "$PLIST"
+ }
+ plist NSLocalNetworkUsageDescription "SqlJam connects to the databases of your local network."
+ plist LSApplicationCategoryType public.app-category.developer-tools
+ # Info.plist is changed, the app is signed again (ad hoc, without a Developer ID)
+ codesign --force --deep --sign - "$APP"
+ jpackage --type dmg --dest "$WORK" --app-image "$APP" --name SqlJam --app-version "$APP_VERSION" \
+ --vendor "Fred Feng"
+ rm -f "$OUT/SqlJam-$VERSION-$PLATFORM.dmg"
+ mv "$WORK"/SqlJam-*.dmg "$OUT/SqlJam-$VERSION-$PLATFORM.dmg"
+ rm -rf "$OUT/SqlJam.app"
+ cp -R "$APP" "$OUT/SqlJam.app"
+ ;;
+ win)
+ rm -f "$OUT/SqlJam-$VERSION-win.zip"
+ (cd "$WORK" && jar --create --no-manifest --file "$OUT/SqlJam-$VERSION-win.zip" SqlJam)
+ ;;
+ linux)
+ rm -f "$OUT/SqlJam-$VERSION-linux.tar.gz"
+ tar -czf "$OUT/SqlJam-$VERSION-linux.tar.gz" -C "$WORK" SqlJam
+ # A deb for Debian and Ubuntu, installed into /opt/sqljam with a menu entry
+ if command -v dpkg-deb >/dev/null 2>&1; then
+ jpackage --type deb --dest "$WORK" --app-image "$WORK/SqlJam" --name SqlJam --app-version "$APP_VERSION" \
+ --vendor "Fred Feng" --linux-package-name sqljam --linux-shortcut --linux-menu-group Development
+ mv "$WORK"/sqljam_*.deb "$OUT/"
+ fi
+ ;;
+esac
+echo "Packaged SqlJam $VERSION for $PLATFORM into $OUT"
diff --git a/src/packaging/windows/SqlJam.ico b/src/packaging/windows/SqlJam.ico
new file mode 100644
index 0000000..1d3a5aa
Binary files /dev/null and b/src/packaging/windows/SqlJam.ico differ
diff --git a/src/test/java/com/github/sqljam/face/service/JdbcUrlsTest.java b/src/test/java/com/github/sqljam/face/service/JdbcUrlsTest.java
new file mode 100644
index 0000000..41ac976
--- /dev/null
+++ b/src/test/java/com/github/sqljam/face/service/JdbcUrlsTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2023-2026 Fred Feng
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.github.sqljam.face.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import org.junit.jupiter.api.Test;
+import com.github.sqljam.face.model.ConnectionProfile;
+import com.github.sqljam.impexp.DbType;
+
+/**
+ * @Description: JdbcUrlsTest verifies the database type, host, port and database read from the urls of each database,
+ * default ports, files, options, and the urls built while the fields are being entered
+ * @Author: Fred Feng
+ * @Date: 18/10/2026
+ * @Version 1.0.0
+ */
+class JdbcUrlsTest {
+
+ private static String parts(String url) {
+ ConnectionProfile profile = JdbcUrls.parse(url);
+ return profile == null ? null : profile.getDbType().name() + " " + profile.getHostname() + " " + profile.getPort()
+ + " " + profile.getDatabase();
+ }
+
+ @Test
+ void readsServerUrls() {
+ assertEquals("MYSQL db.example.com 3307 app", parts("jdbc:mysql://db.example.com:3307/app?useSSL=false"));
+ assertEquals("MYSQL localhost 3306 null", parts("jdbc:mysql://localhost"));
+ assertEquals("MARIADB 192.168.0.63 13307 demo", parts("jdbc:mariadb://192.168.0.63:13307/demo"));
+ assertEquals("POSTGRESQL pg 5432 demo", parts("jdbc:postgresql://pg/demo?stringtype=unspecified"));
+ assertEquals("DB2 db2 60000 demo", parts("jdbc:db2://db2:60000/demo:progressiveStreaming=2;"));
+ assertEquals("CLICKHOUSE ch 18123 app", parts("jdbc:clickhouse:http://ch:18123/app?compress=0"));
+ assertEquals("CLICKHOUSE ch 8123 null", parts("jdbc:ch://ch:8123"));
+ }
+
+ @Test
+ void readsOracleAndSqlServerUrls() {
+ assertEquals("ORACLE 192.168.0.63 11521 DEMO", parts("jdbc:oracle:thin:@//192.168.0.63:11521/DEMO"));
+ assertEquals("ORACLE ora 1521 XE", parts("jdbc:oracle:thin:@ora:1521:XE"));
+ assertEquals("ORACLE ora 1521 FREEPDB1", parts("jdbc:oracle:thin:@ora/FREEPDB1"));
+ // A TNS descriptor is kept as the url only
+ assertEquals("ORACLE null 1521 null", parts("jdbc:oracle:thin:@(DESCRIPTION=(ADDRESS=(HOST=ora)))"));
+ assertEquals("SQLSERVER mssql 11433 demo",
+ parts("jdbc:sqlserver://mssql:11433;databaseName=demo;encrypt=true"));
+ assertEquals("SQLSERVER mssql 1433 sales", parts("jdbc:sqlserver://mssql\\SQLEXPRESS;database=sales"));
+ assertEquals("SQLSERVER mssql 1433 null", parts("jdbc:sqlserver://mssql"));
+ }
+
+ @Test
+ void readsFiles() {
+ assertEquals("H2 null 0 /data/shop", parts("jdbc:h2:file:/data/shop;MODE=MySQL"));
+ assertEquals("H2 null 0 mem:test", parts("jdbc:h2:mem:test"));
+ assertEquals("H2 h2 9093 ~/shop", parts("jdbc:h2:tcp://h2:9093/~/shop"));
+ assertEquals("SQLITE null 0 /data/app.sqlite", parts("jdbc:sqlite:/data/app.sqlite?date_class=TEXT"));
+ assertEquals("DUCKDB null 0 /data/lake.duckdb", parts("jdbc:duckdb:/data/lake.duckdb"));
+ assertNull(parts("jdbc:informix-sqli://host:9088/db"));
+ assertNull(parts(null));
+ // Being typed
+ assertEquals("MYSQL null 3306 null", parts("jdbc:mysql:"));
+ }
+
+ @Test
+ void buildsUrlsOfFields() {
+ assertEquals("jdbc:oracle:thin:@//ora:1521/DEMO", JdbcUrls.build(DbType.ORACLE, "ora", 1521, "DEMO"));
+ // A required database not entered yet
+ assertEquals("jdbc:oracle:thin:@//ora:1521/", JdbcUrls.build(DbType.ORACLE, "ora", 1521, " "));
+ assertEquals(DbType.POSTGRESQL.getUrl("pg", 5432, "demo"), JdbcUrls.build(DbType.POSTGRESQL, " pg ", 5432,
+ "demo "));
+ assertEquals("jdbc:h2:file:/data/shop", JdbcUrls.build(DbType.H2, "", 0, "/data/shop"));
+ assertEquals("jdbc:h2:tcp://h2:9092//data/shop", JdbcUrls.build(DbType.H2, "h2", 9092, "/data/shop"));
+ assertEquals("", JdbcUrls.build(DbType.SQLITE, null, 0, ""));
+ assertEquals("", JdbcUrls.build(null, "host", 1, "db"));
+ // Read back
+ assertEquals("ORACLE ora 1521 DEMO", parts(JdbcUrls.build(DbType.ORACLE, "ora", 1521, "DEMO")));
+ assertEquals("SQLSERVER mssql 1433 demo", parts(JdbcUrls.build(DbType.SQLSERVER, "mssql", 1433, "demo")));
+ }
+}
diff --git a/src/test/java/com/github/sqljam/face/view/ConnectionFormTest.java b/src/test/java/com/github/sqljam/face/view/ConnectionFormTest.java
index 02a11c9..941cf19 100644
--- a/src/test/java/com/github/sqljam/face/view/ConnectionFormTest.java
+++ b/src/test/java/com/github/sqljam/face/view/ConnectionFormTest.java
@@ -226,6 +226,54 @@ void customUrlOverridesHostAndPort() {
assertEquals("jdbc:mysql://db.example.com:3307/app", profile.getJdbcUrl());
}
+ /**
+ * The url follows the fields, a pasted url fills the fields, like DBeaver
+ */
+ @Test
+ void syncsUrlAndFields() {
+ fill(DbType.MYSQL, "localhost", "3306", "test");
+ assertEquals(DbType.MYSQL.getUrl("localhost", 3306, "test"), FxTestSupport.call(() -> field("urlField")
+ .getText()));
+ // A url of the fields is not kept by the profile
+ assertNull(FxTestSupport.call(form::getProfile).getUrl());
+ FxTestSupport.run(() -> field("hostField").setText("db.example.com"));
+ assertEquals(DbType.MYSQL.getUrl("db.example.com", 3306, "test"), FxTestSupport.call(() ->
+ field("urlField").getText()));
+
+ FxTestSupport.run(() -> field("urlField").setText("jdbc:oracle:thin:@//192.168.0.63:11521/DEMO"));
+ assertEquals(DbType.ORACLE, FxTestSupport.call(() -> dbTypeCombo().getValue()));
+ assertEquals("192.168.0.63", FxTestSupport.call(() -> field("hostField").getText()));
+ assertEquals("11521", FxTestSupport.call(() -> field("portField").getText()));
+ assertEquals("DEMO", FxTestSupport.call(() -> field("databaseField").getText()));
+ assertEquals("Service name", FxTestSupport.call(() -> ((Label) form.lookup("#databaseLabel")).getText()));
+ assertNull(FxTestSupport.call(form::getProfile).getUrl());
+
+ // Options of the url are kept, the fields still follow it
+ FxTestSupport.run(() -> field("urlField").setText(
+ "jdbc:sqlserver://mssql:11433;databaseName=demo;encrypt=true"));
+ ConnectionProfile profile = FxTestSupport.call(form::getProfile);
+ assertEquals(DbType.SQLSERVER, profile.getDbType());
+ assertEquals("mssql", profile.getHostname());
+ assertEquals(11433, profile.getPort());
+ assertEquals("demo", profile.getDatabase());
+ assertEquals("jdbc:sqlserver://mssql:11433;databaseName=demo;encrypt=true", profile.getUrl());
+ // A url being typed keeps the fields
+ FxTestSupport.run(() -> field("urlField").setText("jdbc:postgresql:"));
+ assertEquals(DbType.POSTGRESQL, FxTestSupport.call(() -> dbTypeCombo().getValue()));
+ assertEquals("mssql", FxTestSupport.call(() -> field("hostField").getText()));
+
+ // A saved profile shows the url of its fields
+ ConnectionProfile saved = new ConnectionProfile();
+ saved.setDbType(DbType.POSTGRESQL);
+ saved.setHostname("pg");
+ saved.setPort(5432);
+ saved.setDatabase("demo");
+ FxTestSupport.run(() -> form.setProfile(saved, true));
+ assertEquals(DbType.POSTGRESQL.getUrl("pg", 5432, "demo"), FxTestSupport.call(() -> field("urlField")
+ .getText()));
+ assertNull(FxTestSupport.call(form::getProfile).getUrl());
+ }
+
@Test
void generatesDefaultName() {
fill(DbType.MYSQL, "localhost", "3306", "test");
diff --git a/src/test/java/com/github/sqljam/face/view/DocScreenshots.java b/src/test/java/com/github/sqljam/face/view/DocScreenshots.java
index 24952d6..0fe18af 100644
--- a/src/test/java/com/github/sqljam/face/view/DocScreenshots.java
+++ b/src/test/java/com/github/sqljam/face/view/DocScreenshots.java
@@ -164,6 +164,15 @@ private static void sleep(long millis) {
}
}
+ /**
+ * Splash screen while starting, with the logo and the slogan
+ */
+ @Test
+ void splash() throws IOException {
+ SplashView splash = FxTestSupport.call(SplashView::new);
+ render(splash, SplashView.WIDTH, SplashView.HEIGHT, "splash.png");
+ }
+
@Test
void login() throws IOException {
LoginView loginView = FxTestSupport.call(() -> new LoginView(context, profile -> {
diff --git a/src/test/java/com/github/sqljam/face/view/TutorialVideo.java b/src/test/java/com/github/sqljam/face/view/TutorialVideo.java
index 421674e..7a8aeb0 100644
--- a/src/test/java/com/github/sqljam/face/view/TutorialVideo.java
+++ b/src/test/java/com/github/sqljam/face/view/TutorialVideo.java
@@ -31,6 +31,7 @@
import java.util.function.Supplier;
import org.apache.commons.io.FileUtils;
+import org.fxmisc.richtext.CodeArea;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -39,6 +40,7 @@
import org.testfx.framework.junit5.Start;
import com.github.sqljam.face.model.ConnectionProfile;
import com.github.sqljam.face.service.ExampleDatabase;
+import com.github.sqljam.face.service.JdbcUrls;
import com.github.sqljam.impexp.DbType;
import javafx.application.Platform;
import javafx.event.ActionEvent;
@@ -58,6 +60,7 @@
import javafx.scene.control.RadioButton;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TabPane;
+import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
@@ -70,16 +73,18 @@
/**
* @Description: TutorialVideo records the tutorial video of SqlJam with the example database: connect, query rows,
- * export into CSV files, import the package into PostgreSQL, and export directly into SQL Server
- * (created in the export dialog) and Oracle. Imports run in one transaction, the package import is
- * confirmed before its commit. Subtitles are in English, the background music is generated.
+ * export into CSV files, import the package into PostgreSQL (with a partitioned table in the tree),
+ * query with the SQL editor and export the result into PostgreSQL, export directly into SQL Server
+ * (its data source created from a pasted JDBC URL) and Db2 (skipping existing rows and rows with
+ * errors). Imports run in one transaction, the package import is confirmed before its commit.
+ * Subtitles are in English, the background music is generated. It runs about 3 minutes.
* It is recorded once per major version, it is not a test of the build (the name does not
* end with Test), run it by
* mvn test -Dtest=TutorialVideo -Dvideo.file=docs/assets/sqljam-tutorial.mp4
* The windows are shown on the screen while recording, the target databases are those of the
- * integration tests (-Dvideo.postgresql.url, -Dvideo.sqlserver.url, -Dvideo.oracle.url, user and
- * password by -Dvideo.username and -Dvideo.password). Recorded tables are dropped afterwards and an
- * existing table of the same name stops the recording.
+ * integration tests (-Dvideo.postgresql.url, -Dvideo.sqlserver.url, -Dvideo.db2.url, user and
+ * password by -Dvideo.username and -Dvideo.password). Recorded tables are
+ * dropped afterwards and an existing table of the same name stops the recording.
* @Author: Fred Feng
* @Date: 26/03/2023
* @Version 1.0.0
@@ -97,8 +102,19 @@ class TutorialVideo {
"jdbc:postgresql://localhost:5432/demo");
private static final String SQLSERVER_URL = System.getProperty("video.sqlserver.url",
"jdbc:sqlserver://localhost:1433;databaseName=demo;encrypt=false;trustServerCertificate=true");
- private static final String ORACLE_URL = System.getProperty("video.oracle.url",
- "jdbc:oracle:thin:@//localhost:1521/demo");
+ private static final String DB2_URL = System.getProperty("video.db2.url", "jdbc:db2://localhost:50000/demo");
+ /**
+ * Schema of Db2, names of Db2 are upper case
+ */
+ private static final String DB2_SCHEMA = "SHOP";
+ /**
+ * Partitioned table of PostgreSQL shown in the tree with its partitions
+ */
+ private static final String PARTITIONED = "order_archive";
+ /**
+ * Table of the rows of the SQL editor, exported into PostgreSQL
+ */
+ private static final String QUERY_TABLE = "category_revenue";
private static final double WIDTH = 1280;
private static final double HEIGHT = 800;
@@ -115,6 +131,10 @@ class TutorialVideo {
* Tables are dropped only after the check found none of them, tables of the user are never touched
*/
private boolean targetsChecked;
+ /**
+ * Keeps Db2 active while recording, a database without connections is deactivated and started slowly
+ */
+ private Connection db2Connection;
@Start
void start(Stage stage) throws Exception {
@@ -123,8 +143,8 @@ void start(Stage stage) throws Exception {
context = FxTestSupport.createContext(dir);
ConnectionProfile example = context.getExampleDatabase().create();
context.getProfileRegistry().saveProfile(example, true);
- saveProfile("PostgreSQL demo", DbType.POSTGRESQL, 5432);
- saveProfile("Oracle demo", DbType.ORACLE, 1521);
+ saveProfile("PostgreSQL demo", POSTGRESQL_URL);
+ saveProfile("Db2 demo", DB2_URL);
holder = new StackPane();
holder.setStyle("-fx-background-color: -color-bg-default;");
Scene scene = new Scene(holder, WIDTH, HEIGHT);
@@ -136,13 +156,12 @@ void start(Stage stage) throws Exception {
stage.centerOnScreen();
}
- private void saveProfile(String name, DbType dbType, int port) throws Exception {
- ConnectionProfile profile = new ConnectionProfile();
+ /**
+ * Data source of the url: database type, host, port and database read from it
+ */
+ private void saveProfile(String name, String url) throws Exception {
+ ConnectionProfile profile = JdbcUrls.parse(url);
profile.setName(name);
- profile.setDbType(dbType);
- profile.setHostname("localhost");
- profile.setPort(port);
- profile.setDatabase("demo");
profile.setUsername(USERNAME);
profile.setPassword(PASSWORD);
context.getProfileRegistry().saveProfile(profile, true);
@@ -152,6 +171,9 @@ private void saveProfile(String name, DbType dbType, int port) throws Exception
void close() throws Exception {
context.getSessionManager().closeAll();
FxTestSupport.closeAllDialogs();
+ if (db2Connection != null) {
+ db2Connection.close();
+ }
if (targetsChecked) {
dropTables();
}
@@ -162,6 +184,7 @@ void close() throws Exception {
void record() throws Exception {
checkTargets();
targetsChecked = true;
+ db2Connection = DriverManager.getConnection(DB2_URL, USERNAME, PASSWORD);
File work = new File(System.getProperty("video.work", "target/video"));
work.mkdirs();
packageDir = new File(work, "shop-export");
@@ -176,8 +199,9 @@ void record() throws Exception {
query();
exportFiles();
importPackage();
+ sqlQuery();
exportSqlServer();
- exportOracle();
+ exportDb2();
outro();
recorder.stop();
@@ -185,21 +209,22 @@ void record() throws Exception {
TutorialMusic.write(music, recorder.getDurationSeconds() + 2);
recorder.finish(output, music, System.getProperty("video.font", "Helvetica Neue"));
assertTrue(output.length() > 0);
- assertTrue(recorder.getDurationSeconds() < 180, "The video is longer than 3 minutes");
+ assertTrue(recorder.getDurationSeconds() < 200, "The video is longer than about 3 minutes");
}
private void intro() throws Exception {
- FxTestSupport.run(() -> holder.getChildren().setAll(card("Export & Import", "Getting started with the example database",
- "Example Shop (H2) → PostgreSQL · SQL Server · Oracle")));
+ FxTestSupport.run(() -> holder.getChildren().setAll(card("Export & Import",
+ "Getting started with the example database",
+ "Example Shop (H2) → PostgreSQL · SQL Server · Db2")));
recorder.caption("Getting started with the example database");
- pause(4500);
+ pause(3500);
}
private void outro() throws Exception {
FxTestSupport.run(() -> holder.getChildren().setAll(card("Schema, data and all.",
"Move tables across databases", Branding.REPOSITORY_URL)));
recorder.caption("More on GitHub: paganini2008/sqljam");
- pause(5000);
+ pause(4000);
}
private static VBox card(String title, String subtitle, String footer) {
@@ -326,7 +351,7 @@ private void query() throws Exception {
type((TextField) node(mainView, "whereField"), "status = 'PAID'");
press(node(mainView, "applyQueryButton"));
waitFor(() -> "60 rows".equals(((Label) mainView.lookup("#totalLabel")).getText()) ? true : null);
- pause(2500);
+ pause(1800);
}
/**
@@ -359,7 +384,7 @@ private void exportFiles() throws Exception {
Node viewer = waitFor(() -> topNode("packageViewer"));
fitDialog(onFx(() -> viewer.getScene().getWindow()));
recorder.caption("The package viewer shows every file of the package");
- pause(3200);
+ pause(2400);
FxTestSupport.run(() -> viewer.getScene().getWindow().hide());
pause(500);
closeProgress(progress);
@@ -404,11 +429,68 @@ private void importPackage() throws Exception {
recorder.caption("The tables are in PostgreSQL now");
TreeItem postgresql = expand(connection("PostgreSQL demo"));
TreeItem shop = expand(child(expand(child(postgresql, "demo")), SCHEMA));
- assertEquals(TABLES.size(), FxTestSupport.call(() -> shop.getChildren().size()).intValue());
- pause(2400);
+ // The partitioned table of the schema, its partitions are below it
+ assertEquals(TABLES.size() + 1, FxTestSupport.call(() -> shop.getChildren().size()).intValue());
+ pause(1200);
+ recorder.caption("Partitioned tables show their partitioning, their partitions are listed below them");
+ TreeItem partitioned = child(shop, PARTITIONED);
+ moveTo(cell(partitioned));
+ recorder.click();
+ FxTestSupport.run(() -> partitioned.setExpanded(true));
+ pause(2600);
collapse(postgresql);
}
+ /**
+ * Revenue of the categories by a join in the SQL editor, its rows go into a new table of PostgreSQL
+ */
+ private void sqlQuery() throws Exception {
+ recorder.caption("SQL Query: write any SELECT, keywords are highlighted");
+ select(examplePublic());
+ press(node(mainView, "action-sqlQuery"));
+ CodeArea editor = waitFor(() -> (CodeArea) mainView.lookup("#sqlEditor"));
+ pause(600);
+ moveTo(editor);
+ recorder.click();
+ FxTestSupport.run(editor::requestFocus);
+ String sql = "SELECT c.name AS category, SUM(i.quantity * i.unit_price) AS revenue\n"
+ + "FROM order_items i\nJOIN products p ON p.id = i.product_id\n"
+ + "JOIN categories c ON c.id = p.category_id\nGROUP BY c.name ORDER BY revenue DESC";
+ for (char c : sql.toCharArray()) {
+ FxTestSupport.run(() -> editor.appendText(String.valueOf(c)));
+ Thread.sleep(16);
+ }
+ pause(500);
+ press(node(mainView, "sqlRunButton"));
+ TableView> result = (TableView>) node(mainView, "sqlResultTable");
+ waitFor(() -> result.getItems().isEmpty() ? null : true);
+ pause(1500);
+ recorder.caption("Export the rows of the query as a new table of PostgreSQL");
+ press(node(mainView, "sqlExportButton"));
+ TextField table = waitFor(() -> (TextField) topNode("queryTableField"));
+ fitDialog(topWindow());
+ pause(500);
+ type(table, QUERY_TABLE);
+ RadioButton database = (RadioButton) topNode("databaseRadio");
+ scrollDialog(1);
+ press(database, () -> database.setSelected(true));
+ pause(500);
+ scrollDialog(1);
+ ConnectionProfile postgresql = context.getProfileRegistry().getProfiles().stream().filter(profile ->
+ "PostgreSQL demo".equals(profile.getName())).findFirst().orElseThrow();
+ choose(topNode("targetCombo"), postgresql);
+ ComboBox> schema = waitFor(() -> {
+ ComboBox> combo = (ComboBox>) topNode("targetSchemaCombo");
+ return combo.getItems().contains(SCHEMA) ? combo : null;
+ });
+ choose(schema, SCHEMA);
+ pause(500);
+ press(topNode("startButton"));
+ Window progress = waitForProgress();
+ pause(1000);
+ closeProgress(progress);
+ }
+
private void exportSqlServer() throws Exception {
recorder.caption("Export straight into SQL Server, create its data source with the + button");
openExport();
@@ -423,15 +505,14 @@ private void exportSqlServer() throws Exception {
fitDialog(topWindow());
pause(700);
type((TextField) form, "SQL Server demo");
- choose(topNode("dbTypeCombo"), DbType.SQLSERVER);
- type((TextField) topNode("hostField"), "localhost");
- type((TextField) topNode("portField"), "1433");
- type((TextField) topNode("databaseField"), "demo");
+ recorder.caption("Paste a JDBC URL: the type, host, port and database follow it");
+ type((TextField) topNode("urlField"), SQLSERVER_URL);
+ pause(1500);
type((TextField) topNode("usernameField"), USERNAME);
type((TextField) topNode("passwordField"), PASSWORD);
press(buttonOf(topWindow(), "Test"));
waitFor(() -> ((Labeled) topNode("formStatusLabel")).getText().startsWith("Connected") ? true : null);
- pause(1500);
+ pause(1200);
press(buttonOf(topWindow(), "Save"));
waitFor(() -> topWindow() == export ? true : null);
recorder.caption("The new data source is the target, a missing schema is created");
@@ -449,35 +530,42 @@ private void exportSqlServer() throws Exception {
Window progress = waitForProgress();
pause(1500);
closeProgress(progress);
- TreeItem sqlServer = expand(waitFor(() -> connection("SQL Server demo")));
- expand(child(expand(child(sqlServer, "demo")), SCHEMA));
- pause(2400);
- collapse(sqlServer);
}
- private void exportOracle() throws Exception {
- recorder.caption("The same tables go straight into Oracle");
+ /**
+ * Into Db2 with the write modes: existing rows are skipped by key, rows with errors are skipped and logged
+ */
+ private void exportDb2() throws Exception {
+ recorder.caption("Straight into Db2: types are translated, keys, indexes and comments come along");
openExport();
RadioButton database = (RadioButton) topNode("databaseRadio");
scrollDialog(1);
press(database, () -> database.setSelected(true));
- pause(700);
+ pause(500);
scrollDialog(1);
- ConnectionProfile oracle = context.getProfileRegistry().getProfiles().stream().filter(profile -> "Oracle demo"
+ ConnectionProfile db2 = context.getProfileRegistry().getProfiles().stream().filter(profile -> "Db2 demo"
.equals(profile.getName())).findFirst().orElseThrow();
- choose(topNode("targetCombo"), oracle);
+ choose(topNode("targetCombo"), db2);
ComboBox> schema = waitFor(() -> {
ComboBox> combo = (ComboBox>) topNode("targetSchemaCombo");
- return USERNAME.equalsIgnoreCase(Objects.toString(combo.getValue(), "")) ? combo : null;
+ return combo.getItems().isEmpty() ? null : combo;
});
- moveTo(schema);
- pause(1200);
+ type(schema.getEditor(), DB2_SCHEMA);
+ recorder.caption("Rows are inserted, updated or skipped by key");
+ RadioButton skipExisting = (RadioButton) topNode("skipExistingRadio");
+ press(skipExisting, () -> skipExisting.setSelected(true));
+ recorder.caption("Rows with errors can be skipped, each of them is logged");
+ CheckBox skipErrors = (CheckBox) topNode("skipErrorsCheck");
+ press(skipErrors, () -> skipErrors.setSelected(true));
+ pause(800);
press(topNode("startButton"));
Window progress = waitForProgress();
- recorder.caption("Types are translated, keys, indexes, comments and identities come along");
- pause(2200);
+ pause(1500);
closeProgress(progress);
- pause(800);
+ TreeItem db2Item = expand(waitFor(() -> connection("Db2 demo")));
+ expand(child(db2Item, DB2_SCHEMA));
+ pause(1800);
+ collapse(db2Item);
}
/**
@@ -725,18 +813,24 @@ private static void checkTargets() throws SQLException {
Statement statement = connection.createStatement()) {
assertEquals(0, count(statement, "SELECT COUNT(*) FROM pg_tables WHERE schemaname = '" + SCHEMA + "'"),
"Schema shop of PostgreSQL has tables");
- // The package import uses an existing schema
+ // The package import uses an existing schema, it has a partitioned table
statement.execute("CREATE SCHEMA IF NOT EXISTS " + SCHEMA);
+ statement.execute("CREATE TABLE " + SCHEMA + "." + PARTITIONED + " (id BIGINT NOT NULL, order_year INT"
+ + " NOT NULL, total NUMERIC(12,2), PRIMARY KEY (id, order_year)) PARTITION BY RANGE (order_year)");
+ for (int year : new int[]{2024, 2025}) {
+ statement.execute(String.format("CREATE TABLE %s.%s_%d PARTITION OF %s.%s FOR VALUES FROM (%d) TO (%d)",
+ SCHEMA, PARTITIONED, year, SCHEMA, PARTITIONED, year, year + 1));
+ }
}
try (Connection connection = DriverManager.getConnection(SQLSERVER_URL, USERNAME, PASSWORD);
Statement statement = connection.createStatement()) {
assertEquals(0, count(statement, "SELECT COUNT(*) FROM sys.schemas WHERE name = '" + SCHEMA + "'"),
"Schema shop of SQL Server exists");
}
- try (Connection connection = DriverManager.getConnection(ORACLE_URL, USERNAME, PASSWORD);
+ try (Connection connection = DriverManager.getConnection(DB2_URL, USERNAME, PASSWORD);
Statement statement = connection.createStatement()) {
- assertEquals(0, count(statement, "SELECT COUNT(*) FROM user_tables WHERE table_name IN ("
- + "'CATEGORIES', 'CUSTOMERS', 'PRODUCTS', 'ORDERS', 'ORDER_ITEMS')"), "Oracle has the tables");
+ assertEquals(0, count(statement, "SELECT COUNT(*) FROM SYSCAT.TABLES WHERE TABSCHEMA = '" + DB2_SCHEMA
+ + "'"), "Schema SHOP of Db2 has tables");
}
}
@@ -760,12 +854,13 @@ private static void dropTables() {
}
sqlServer.add("IF SCHEMA_ID('" + SCHEMA + "') IS NOT NULL DROP SCHEMA " + SCHEMA);
execute(SQLSERVER_URL, sqlServer);
- List oracle = new ArrayList<>();
- for (String table : TABLES) {
- oracle.add("BEGIN EXECUTE IMMEDIATE 'DROP TABLE " + table.toUpperCase() + " CASCADE CONSTRAINTS PURGE'; "
- + "EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;");
+ List db2 = new ArrayList<>();
+ for (String table : List.of("order_items", "orders", "products", "customers", "categories")) {
+ db2.add("DROP TABLE IF EXISTS " + DB2_SCHEMA + "." + table.toUpperCase());
}
- execute(ORACLE_URL, oracle);
+ db2.add("BEGIN DECLARE CONTINUE HANDLER FOR SQLSTATE '42704' BEGIN END; "
+ + "EXECUTE IMMEDIATE 'DROP SCHEMA " + DB2_SCHEMA + " RESTRICT'; END");
+ execute(DB2_URL, db2);
}
private static void execute(String url, List sqls) {