Skip to content

Commit 998e146

Browse files
authored
Bugfix/jdbc UI (#123)
* fix(ui): missing dbtable in connection options for jdbc connection * feat(build): build for different os architectures and fix link for downloads
1 parent 74d78a0 commit 998e146

10 files changed

Lines changed: 492 additions & 87 deletions

File tree

.github/workflows/build.yml

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ name: Build docker images
22

33
on:
44
push:
5-
tags: ["*"]
5+
branches:
6+
- main
67

78
jobs:
89
build:
@@ -41,7 +42,16 @@ jobs:
4142

4243
osx:
4344
needs: build
44-
runs-on: [macos-latest]
45+
strategy:
46+
matrix:
47+
include:
48+
- runner: macos-13 # Intel x64
49+
arch: x64
50+
arch_name: x86_64
51+
- runner: macos-14 # Apple Silicon arm64
52+
arch: aarch64
53+
arch_name: aarch64
54+
runs-on: ${{ matrix.runner }}
4555

4656
steps:
4757
- uses: actions/checkout@v4
@@ -53,7 +63,7 @@ jobs:
5363
with:
5464
java-version: '21'
5565
java-package: jdk
56-
architecture: x64
66+
architecture: ${{ matrix.arch }}
5767
distribution: oracle
5868
- name: Download fat jar
5969
uses: actions/download-artifact@v4
@@ -62,11 +72,13 @@ jobs:
6272
path: app/build/libs/
6373
- name: Package jar as dmg installer
6474
run: 'jpackage --main-jar data-caterer.jar "@misc/jpackage/jpackage.cfg" "@misc/jpackage/jpackage-mac.cfg"'
75+
- name: Rename DMG with version and architecture
76+
run: mv DataCaterer-*.dmg DataCaterer-${{ env.APP_VERSION }}-macos-${{ matrix.arch_name }}.dmg
6577
- name: Upload dmg
6678
uses: actions/upload-artifact@v4
6779
with:
68-
name: data-caterer-mac
69-
path: "DataCaterer-1.0.0.dmg"
80+
name: data-caterer-macos-${{ matrix.arch_name }}
81+
path: "DataCaterer-${{ env.APP_VERSION }}-macos-${{ matrix.arch_name }}.dmg"
7082
overwrite: true
7183

7284
windows:
@@ -92,16 +104,27 @@ jobs:
92104
path: app/build/libs/
93105
- name: Package jar as exe
94106
run: 'jpackage --main-jar data-caterer.jar "@misc/jpackage/jpackage.cfg" "@misc/jpackage/jpackage-windows.cfg"'
107+
- name: Rename EXE with version and architecture
108+
run: mv DataCaterer-*.exe DataCaterer-$env:APP_VERSION-windows-x86_64.exe
95109
- name: Upload installer
96110
uses: actions/upload-artifact@v4
97111
with:
98-
name: data-caterer-windows
99-
path: "DataCaterer-1.0.0.exe"
112+
name: data-caterer-windows-x86_64
113+
path: "DataCaterer-${{ env.APP_VERSION }}-windows-x86_64.exe"
100114
overwrite: true
101115

102116
linux:
103117
needs: build
104-
runs-on: [ubuntu-latest]
118+
strategy:
119+
matrix:
120+
include:
121+
- runner: ubuntu-latest
122+
arch: x64
123+
arch_name: amd64
124+
- runner: ubuntu-latest
125+
arch: aarch64
126+
arch_name: arm64
127+
runs-on: ${{ matrix.runner }}
105128

106129
steps:
107130
- uses: actions/checkout@v4
@@ -113,7 +136,7 @@ jobs:
113136
with:
114137
java-version: '21'
115138
java-package: jdk
116-
architecture: x64
139+
architecture: ${{ matrix.arch }}
117140
distribution: oracle
118141
- name: Download fat jar
119142
uses: actions/download-artifact@v4
@@ -122,11 +145,11 @@ jobs:
122145
path: app/build/libs/
123146
- name: Package jar as debian package
124147
run: 'jpackage --main-jar data-caterer.jar "@misc/jpackage/jpackage.cfg" "@misc/jpackage/jpackage-linux.cfg"'
125-
- name: List directory
126-
run: ls -lart
148+
- name: Rename DEB with architecture
149+
run: mv datacaterer_*_*.deb datacaterer_${{ env.APP_VERSION }}_${{ matrix.arch_name }}.deb
127150
- name: Upload deb
128151
uses: actions/upload-artifact@v4
129152
with:
130-
name: data-caterer-linux
131-
path: "datacaterer_1.0.0_amd64.deb"
153+
name: data-caterer-linux-${{ matrix.arch_name }}
154+
path: "datacaterer_${{ env.APP_VERSION }}_${{ matrix.arch_name }}.deb"
132155
overwrite: true

README.md

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ and deep dive into issues [from the generated report](https://data.catering/late
3838

3939
1. Docker
4040
```shell
41-
docker run -d -i -p 9898:9898 -e DEPLOY_MODE=standalone --name datacaterer datacatering/data-caterer:0.17.2
41+
docker run -d -i -p 9898:9898 -e DEPLOY_MODE=standalone --name datacaterer datacatering/data-caterer:0.17.3
4242
```
4343
[Open localhost:9898](http://localhost:9898).
4444
1. [Run Scala/Java examples](#run-scalajava-examples)
@@ -47,14 +47,20 @@ and deep dive into issues [from the generated report](https://data.catering/late
4747
cd data-caterer-example && ./run.sh
4848
#check results under docker/sample/report/index.html folder
4949
```
50-
1. [UI App: Mac download](https://nightly.link/data-catering/data-caterer/workflows/build/main/data-caterer-mac.zip)
51-
1. [UI App: Windows download](https://nightly.link/data-catering/data-caterer/workflows/build/main/data-caterer-windows.zip)
52-
1. After downloading, go to 'Downloads' folder and 'Extract All' from data-caterer-windows
53-
1. Double-click 'DataCaterer-1.0.0' to install Data Caterer
54-
1. Click on 'More info' then at the bottom, click 'Run anyway'
55-
1. Go to '/Program Files/DataCaterer' folder and run DataCaterer application
56-
1. If your browser doesn't open, go to [http://localhost:9898](http://localhost:9898) in your preferred browser
57-
1. [UI App: Linux download](https://nightly.link/data-catering/data-caterer/workflows/build/main/data-caterer-linux.zip)
50+
1. UI App Downloads (Nightly builds from `main` branch)
51+
- **macOS**:
52+
- [Intel (x86_64)](https://nightly.link/data-catering/data-caterer/workflows/build/main/data-caterer-macos-x86_64.zip)
53+
- [Apple Silicon (M1/M2/M3)](https://nightly.link/data-catering/data-caterer/workflows/build/main/data-caterer-macos-aarch64.zip)
54+
- **Windows**:
55+
- [x64](https://nightly.link/data-catering/data-caterer/workflows/build/main/data-caterer-windows-x86_64.zip)
56+
1. After downloading, go to 'Downloads' folder and 'Extract All' from data-caterer-windows-x86_64
57+
1. Double-click the installer to install Data Caterer
58+
1. Click on 'More info' then at the bottom, click 'Run anyway'
59+
1. Go to '/Program Files/DataCaterer' folder and run DataCaterer application
60+
1. If your browser doesn't open, go to [http://localhost:9898](http://localhost:9898) in your preferred browser
61+
- **Linux**:
62+
- [amd64](https://nightly.link/data-catering/data-caterer/workflows/build/main/data-caterer-linux-amd64.zip)
63+
- [arm64](https://nightly.link/data-catering/data-caterer/workflows/build/main/data-caterer-linux-arm64.zip)
5864

5965
[Follow quick start instructions from here if you want more details](https://data.catering/latest/get-started/quick-start/).
6066

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package io.github.datacatering.datacaterer.core.ui.mapper
2+
3+
import io.github.datacatering.datacaterer.api.model.Constants._
4+
import org.apache.log4j.Logger
5+
6+
/**
7+
* Maps UI step options to internal format required by data sources
8+
*
9+
* The UI may send options in a user-friendly format (e.g., separate schema and table fields)
10+
* that need to be converted to the format expected by the underlying data source connectors
11+
*/
12+
object StepOptionsMapper {
13+
14+
private val LOGGER = Logger.getLogger(getClass.getName)
15+
16+
/**
17+
* Convert UI step options to the format expected by the data source
18+
*
19+
* @param options Step options from the UI
20+
* @param format Data source format (jdbc, cassandra, etc.)
21+
* @return Converted options
22+
*/
23+
def mapStepOptions(options: Map[String, String], format: String): Map[String, String] = {
24+
format.toLowerCase match {
25+
case JDBC => mapJdbcOptions(options)
26+
case CASSANDRA => mapCassandraOptions(options)
27+
case ICEBERG => mapIcebergOptions(options)
28+
case BIGQUERY => mapBigQueryOptions(options)
29+
case _ => options // No mapping needed for other formats
30+
}
31+
}
32+
33+
/**
34+
* Map JDBC options: convert schema + table to dbtable
35+
*
36+
* JDBC connections require 'dbtable' in format 'schema.table' or just 'table'
37+
* The UI sends these as separate 'schema' and 'table' fields
38+
*/
39+
private def mapJdbcOptions(options: Map[String, String]): Map[String, String] = {
40+
val hasSchema = options.contains("schema")
41+
val hasTable = options.contains("table")
42+
val hasDbTable = options.contains(JDBC_TABLE)
43+
44+
if (!hasDbTable && hasSchema && hasTable) {
45+
// Convert schema + table to dbtable
46+
val schema = options("schema")
47+
val table = options("table")
48+
val dbtable = s"$schema.$table"
49+
LOGGER.debug(s"Converting JDBC schema+table to dbtable: schema=$schema, table=$table, dbtable=$dbtable")
50+
options - "schema" - "table" + (JDBC_TABLE -> dbtable)
51+
} else if (!hasDbTable && hasTable) {
52+
// Only table provided, rename to dbtable
53+
LOGGER.debug(s"Converting JDBC table to dbtable: table=${options("table")}")
54+
options - "table" + (JDBC_TABLE -> options("table"))
55+
} else {
56+
// dbtable already exists or neither schema nor table provided
57+
options
58+
}
59+
}
60+
61+
/**
62+
* Map Cassandra options: ensure keyspace and table are properly set
63+
*
64+
* Cassandra requires separate 'keyspace' and 'table' options
65+
* This is already the format the UI sends, so no conversion needed currently
66+
*/
67+
private def mapCassandraOptions(options: Map[String, String]): Map[String, String] = {
68+
// Cassandra already uses keyspace and table separately
69+
// No conversion needed, but we validate they exist
70+
if (options.contains(CASSANDRA_KEYSPACE) && options.contains(CASSANDRA_TABLE)) {
71+
LOGGER.debug(s"Cassandra options validated: keyspace=${options(CASSANDRA_KEYSPACE)}, table=${options(CASSANDRA_TABLE)}")
72+
}
73+
options
74+
}
75+
76+
/**
77+
* Map Iceberg options: ensure table is properly formatted
78+
*
79+
* Iceberg requires 'table' in format 'database.table'
80+
* The UI sends this as a single 'table' field
81+
*/
82+
private def mapIcebergOptions(options: Map[String, String]): Map[String, String] = {
83+
// Iceberg uses 'table' field which should already be in database.table format from UI
84+
if (options.contains(TABLE)) {
85+
LOGGER.debug(s"Iceberg table option: table=${options(TABLE)}")
86+
}
87+
options
88+
}
89+
90+
/**
91+
* Map BigQuery options: ensure table is properly formatted
92+
*
93+
* BigQuery requires 'table' in format 'project.dataset.table'
94+
* The UI sends this as a single 'table' field
95+
*/
96+
private def mapBigQueryOptions(options: Map[String, String]): Map[String, String] = {
97+
// BigQuery uses 'table' field which should already be in project.dataset.table format from UI
98+
if (options.contains(TABLE)) {
99+
LOGGER.debug(s"BigQuery table option: table=${options(TABLE)}")
100+
}
101+
options
102+
}
103+
}

app/src/main/scala/io/github/datacatering/datacaterer/core/ui/plan/PlanRepository.scala

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import io.github.datacatering.datacaterer.core.parser.PlanParser
1111
import io.github.datacatering.datacaterer.core.plan.{PlanProcessor, YamlPlanRun}
1212
import io.github.datacatering.datacaterer.core.ui.config.UiConfiguration.INSTALL_DIRECTORY
1313
import io.github.datacatering.datacaterer.core.ui.mapper.ConfigurationMapper.configurationMapping
14+
import io.github.datacatering.datacaterer.core.ui.mapper.StepOptionsMapper
1415
import io.github.datacatering.datacaterer.core.ui.model.{ConfigurationRequest, Connection, EnhancedPlanRunRequest, PlanRunExecution, PlanRunRequest, PlanRunRequests, SampleResponse, SchemaSampleRequest}
1516
import io.github.datacatering.datacaterer.core.ui.plan.PlanResponseHandler.{KO, OK, Response}
1617
import io.github.datacatering.datacaterer.core.ui.resource.SparkSessionManager
@@ -195,7 +196,7 @@ object PlanRepository {
195196
.map(t => t.name -> t.dataSourceName)
196197
.toMap
197198

198-
val dataSourceConnectionInfo = getConnectionDetails(taskToDataSourceMap)
199+
val dataSourceConnectionInfo = getConnectionDetails(taskToDataSourceMap, baseDirectory)
199200
.map(c => {
200201
val additionalConfig = c.`type` match {
201202
case POSTGRES => Map(FORMAT -> JDBC, DRIVER -> POSTGRES_DRIVER)
@@ -207,8 +208,8 @@ object PlanRepository {
207208
.toMap
208209

209210
//find tasks and validation using data source connection
210-
val updatedValidation = validationWithConnectionInfo(parsedRequest, dataSourceConnectionInfo)
211-
val updatedTasks = tasksWithConnectionInfo(parsedRequest, taskToDataSourceMap, dataSourceConnectionInfo)
211+
val updatedValidation = validationWithConnectionInfo(parsedRequest, dataSourceConnectionInfo, baseDirectory)
212+
val updatedTasks = tasksWithConnectionInfo(parsedRequest, taskToDataSourceMap, dataSourceConnectionInfo, baseDirectory)
212213
val updatedConfiguration = parsedRequest.configuration
213214
.map(c => configurationMapping(c, baseDirectory))
214215
.getOrElse(DataCatererConfigurationBuilder())
@@ -218,14 +219,15 @@ object PlanRepository {
218219

219220
private def validationWithConnectionInfo(
220221
parsedRequest: PlanRunRequest,
221-
dataSourceConnectionInfo: Map[String, Map[String, String]]
222+
dataSourceConnectionInfo: Map[String, Map[String, String]],
223+
baseDirectory: String
222224
): List[ValidationConfiguration] = {
223225
parsedRequest.validation.map(yamlV => {
224226
val updatedDataSources = yamlV.dataSources.map(ds => {
225227
val dataSourceName = ds._1
226228
val connectionInfo = dataSourceConnectionInfo(dataSourceName)
227229
val updatedValidationOptions = ds._2.map(yamlDs => {
228-
val metadataOpts = getMetadataSourceInfo(dataSourceConnectionInfo, yamlDs.options)
230+
val metadataOpts = getMetadataSourceInfo(dataSourceConnectionInfo, yamlDs.options, baseDirectory)
229231
val allOpts = yamlDs.options ++ connectionInfo ++ metadataOpts
230232
val listValidationBuilders = yamlDs.validations.map {
231233
case yamlUpstreamDataSourceValidation: YamlUpstreamDataSourceValidation =>
@@ -245,7 +247,8 @@ object PlanRepository {
245247
private def tasksWithConnectionInfo(
246248
parsedRequest: PlanRunRequest,
247249
taskToDataSourceMap: Map[String, String],
248-
dataSourceConnectionInfo: Map[String, Map[String, String]]
250+
dataSourceConnectionInfo: Map[String, Map[String, String]],
251+
baseDirectory: String
249252
): List[Task] = {
250253
val updatedTasks = parsedRequest.tasks.map(s => {
251254
val taskName = s.name
@@ -254,23 +257,29 @@ object PlanRepository {
254257
}
255258
val dataSourceName = taskToDataSourceMap(taskName)
256259
val connectionInfo = dataSourceConnectionInfo(dataSourceName)
257-
val metadataOpts = getMetadataSourceInfo(dataSourceConnectionInfo, s.options)
258-
val updatedStep = s.copy(options = s.options ++ connectionInfo ++ metadataOpts)
260+
val metadataOpts = getMetadataSourceInfo(dataSourceConnectionInfo, s.options, baseDirectory)
261+
262+
// Map UI options to data source format using StepOptionsMapper
263+
val format = connectionInfo.getOrElse(FORMAT, "")
264+
265+
val baseOptions = s.options ++ connectionInfo ++ metadataOpts
266+
val mappedOptions = StepOptionsMapper.mapStepOptions(baseOptions, format)
267+
val updatedStep = s.copy(options = mappedOptions)
259268
Task(taskName, List(updatedStep))
260269
})
261270
updatedTasks
262271
}
263272

264-
private def getMetadataSourceInfo(dataSourceConnectionInfo: Map[String, Map[String, String]], options: Map[String, String]): Map[String, String] = {
273+
private def getMetadataSourceInfo(dataSourceConnectionInfo: Map[String, Map[String, String]], options: Map[String, String], baseDirectory: String): Map[String, String] = {
265274
if (options.contains(METADATA_SOURCE_NAME)) {
266-
ConnectionService.getMetadataSourceInfo(options(METADATA_SOURCE_NAME), dataSourceConnectionInfo)
275+
ConnectionService.getMetadataSourceInfo(options(METADATA_SOURCE_NAME), dataSourceConnectionInfo, baseDirectory)
267276
} else {
268277
Map()
269278
}
270279
}
271280

272-
private def getConnectionDetails(taskToDataSourceMap: Map[String, String]): List[Connection] = {
273-
ConnectionService.getConnections(taskToDataSourceMap.values.toList, masking = false)
281+
private def getConnectionDetails(taskToDataSourceMap: Map[String, String], baseDirectory: String): List[Connection] = {
282+
ConnectionService.getConnections(taskToDataSourceMap.values.toList, masking = false, baseDirectory)
274283
}
275284

276285
private def savePlanRunExecution(

0 commit comments

Comments
 (0)