Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
140 commits
Select commit Hold shift + click to select a range
2119555
feat(optimizer): add data model — schema, entities, DTOs, converters
mkuchenbecker Apr 3, 2026
3c93d52
fix: address PR review feedback on optimizer data model
mkuchenbecker Apr 3, 2026
d419eb3
feat(optimizer): add repositories and repository tests
mkuchenbecker Apr 6, 2026
7ff3b43
fix: consolidate repo methods — single find with optional filters
mkuchenbecker Apr 6, 2026
f7f6812
feat(optimizer): add REST service layer, controllers, and shared module
mkuchenbecker Apr 6, 2026
ef3260f
fix: update service impl to use consolidated find methods
mkuchenbecker Apr 6, 2026
ac1da01
feat(optimizer): add apps/optimizer shared module with find-only repos
mkuchenbecker Apr 6, 2026
be353ca
Merge mkuchenb/optimizer-1 into optimizer-2
mkuchenbecker Apr 6, 2026
02a5ab3
fix: remove orphan fields from CompleteOperationRequest
mkuchenbecker Apr 6, 2026
5c78c8f
Merge mkuchenb/optimizer-0 into optimizer-1
mkuchenbecker Apr 6, 2026
2ddc445
Merge mkuchenb/optimizer-1 into optimizer-2
mkuchenbecker Apr 6, 2026
01466c7
feat(optimizer): add service-layer integration tests
mkuchenbecker Apr 6, 2026
ff07fde
fix: assert stats history delta values in upsert test
mkuchenbecker Apr 6, 2026
1cbe556
Merge branch 'main' into mkuchenb/optimizer-0
mkuchenbecker Apr 30, 2026
231e1a1
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker Apr 30, 2026
ff448a0
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker Apr 30, 2026
f82d1b3
fix(optimizer): address PR #527 review feedback
mkuchenbecker May 1, 2026
e907a31
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker May 1, 2026
a109f02
fix(optimizer): propagate optimizer-0 renames into repos and tests
mkuchenbecker May 1, 2026
a1ef430
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 1, 2026
df01c26
fix(optimizer): propagate optimizer-0 renames into service + controller
mkuchenbecker May 1, 2026
027fccd
fix(optimizer): add databaseName + tableName to apps/optimizer histor…
mkuchenbecker May 1, 2026
e0a49da
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 1, 2026
79753f1
fix(optimizer): index table_operations_history on (database_name, tab…
mkuchenbecker May 1, 2026
ae610ae
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker May 1, 2026
85a432f
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 1, 2026
dceef97
feat(optimizer): unify REST prefix to /v1/optimizer; add name-based h…
mkuchenbecker May 1, 2026
bf04488
fix(optimizer): align apps/optimizer entities with services schema
mkuchenbecker May 12, 2026
f2ac002
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 12, 2026
62f426a
feat(optimizer): add findLatestPerTable to history repo
mkuchenbecker May 12, 2026
e13a31b
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 12, 2026
3483b25
perf(optimizer): index table_operations_history for findLatestPerTable
mkuchenbecker May 13, 2026
9748548
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 13, 2026
0293009
feat(optimizer): add findDistinctDatabaseNames to TableStatsRepository
mkuchenbecker May 13, 2026
0efab45
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 13, 2026
6fa885d
refactor(optimizer): Optional<T> for optional filter params in servic…
mkuchenbecker May 13, 2026
eba1392
feat(optimizer): promote internal model types to shared apps/optimizer
mkuchenbecker May 14, 2026
10ed1bb
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 14, 2026
e576593
refactor(optimizer): rename apps/optimizer entities + repos to plural…
mkuchenbecker May 14, 2026
9f88a4a
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 14, 2026
6f98e1a
refactor(optimizer): consolidate entities/repos into apps/optimizer; …
mkuchenbecker May 14, 2026
d90c26f
refactor(optimizer): move apps/optimizer module into services/optimizer
mkuchenbecker May 14, 2026
62f33b7
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 14, 2026
17e280f
refactor(optimizer): drop apps/optimizer-data dep; simplify history API
mkuchenbecker May 14, 2026
9a129a8
refactor(optimizer): align data model — rename HistoryStatus; String …
mkuchenbecker May 14, 2026
a8978a0
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker May 14, 2026
dfb9102
refactor(optimizer): realign entity shapes with optimizer-0
mkuchenbecker May 14, 2026
e3bf9e1
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 14, 2026
681407e
feat(optimizer): add internal model layer
mkuchenbecker May 14, 2026
2005bca
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker May 14, 2026
b689969
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 14, 2026
e3fb777
perf(optimizer): index table_operations_history for findLatestPerTable
mkuchenbecker May 14, 2026
f89889d
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker May 14, 2026
beaaf88
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 14, 2026
d3e1726
refactor(optimizer): enforce layer boundaries in api/ + model/
mkuchenbecker May 14, 2026
db9513a
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker May 14, 2026
1d469a7
refactor(optimizer): remove db-layer types from optimizer-0
mkuchenbecker May 14, 2026
eee8eca
refactor(optimizer): remove DB schema + schema-init properties
mkuchenbecker May 14, 2026
0567753
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker May 14, 2026
328e5b9
refactor(optimizer): scrub MySQL / JPA / datasource references
mkuchenbecker May 14, 2026
f7a5d20
refactor(optimizer): drop UpsertTableOperationsRequest
mkuchenbecker May 14, 2026
2a532b5
refactor(optimizer): drop JobResult from the wire and internal model
mkuchenbecker May 14, 2026
2e3a231
feat(optimizer): add debug echo fields to CompleteOperationRequest
mkuchenbecker May 14, 2026
db5eb29
refactor(optimizer): move application.properties out of optimizer-0
mkuchenbecker May 14, 2026
bbcf84a
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker May 14, 2026
ac3abc0
feat(optimizer): introduce db/ layer with per-layer types
mkuchenbecker May 14, 2026
e79eec7
refactor(optimizer): split TableStats envelope into snapshot + delta …
mkuchenbecker May 14, 2026
f955ded
fix(optimizer): drop CommitDeltaMetrics from TableStatsRow
mkuchenbecker May 14, 2026
13987c1
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 14, 2026
969949d
refactor(optimizer): rewire service layer onto api/model/db mappers
mkuchenbecker May 14, 2026
861b584
feat(optimizer): extend model layer for service-only types
mkuchenbecker May 14, 2026
41d4c6d
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker May 14, 2026
69d9e8f
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 14, 2026
b60a3bf
feat(optimizer): extend ModelDbMapper for service-only types
mkuchenbecker May 14, 2026
eb6e3be
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 14, 2026
b80b2e5
refactor(optimizer): service layer returns only model/ types
mkuchenbecker May 14, 2026
25d98aa
feat(optimizer): restore batch CAS methods on TableOperationsRepository
mkuchenbecker May 14, 2026
31fac5b
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 14, 2026
188713d
docs(optimizer): comment every field on opt-0 api/ and model/ types
mkuchenbecker May 14, 2026
f060b5e
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker May 14, 2026
1119699
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 14, 2026
8d64273
refactor(optimizer): remove clusterId from SnapshotMetrics
mkuchenbecker May 14, 2026
ee7bcab
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker May 14, 2026
c1ad246
refactor(optimizer): comment every db/ field; drop clusterId and version
mkuchenbecker May 14, 2026
72b431c
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 14, 2026
c72aae8
refactor(optimizer): move api↔model conversion onto api types; delete…
mkuchenbecker May 14, 2026
1fca287
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker May 14, 2026
8ae8777
refactor(optimizer): move model↔db conversion onto model types; delet…
mkuchenbecker May 14, 2026
b3aacff
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 14, 2026
bb8aa4d
refactor(optimizer): service + controllers use type to/from methods
mkuchenbecker May 14, 2026
af23d5e
fix(optimizer): make TableStats self-describing; route DTO conversion…
mkuchenbecker May 15, 2026
3864e42
chore(optimizer): cascade self-describing TableStats from opt-0 to opt-1
mkuchenbecker May 15, 2026
0a1125b
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 15, 2026
a6045b5
feat(optimizer): add TableStats↔TableStatsRow conversion on model
mkuchenbecker May 15, 2026
4427de0
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 15, 2026
db5921e
refactor(optimizer): service stats methods take/return TableStats, no…
mkuchenbecker May 15, 2026
3aebf64
chore(optimizer): enable toBuilder on model.Table and model.TableOper…
mkuchenbecker May 15, 2026
bf30f86
chore(optimizer): cascade toBuilder annotations from opt-0 to opt-1
mkuchenbecker May 15, 2026
faba6d7
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 15, 2026
b6c7f42
refactor(optimizer): drop fileCount enrichment from model.TableOperation
mkuchenbecker May 18, 2026
177af95
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker May 18, 2026
487ac56
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 18, 2026
2b06c92
feat(repo): add findClaimedIds for transactional batch-claim verifica…
mkuchenbecker May 18, 2026
5b5aae2
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 18, 2026
437a0ed
refactor(optimizer): add Dto suffix to all api/model classes (PR #527…
mkuchenbecker May 19, 2026
aabb51c
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker May 19, 2026
928d537
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 19, 2026
eedf6d0
refactor(optimizer): update controllers for renamed api/model Dto types
mkuchenbecker May 19, 2026
4f98c22
refactor(optimizer): rename api.model package to api.spec (PR #527 re…
mkuchenbecker May 19, 2026
2c26872
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker May 19, 2026
b849b7d
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 19, 2026
231efde
refactor(optimizer): update controller imports for api.model -> api.s…
mkuchenbecker May 19, 2026
b31decf
refactor(optimizer): move Dto suffix from api/spec to model
mkuchenbecker May 20, 2026
caf3294
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker May 20, 2026
c6a64bf
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 20, 2026
91e89ef
refactor(optimizer): update controller + service refs after Dto suffi…
mkuchenbecker May 20, 2026
4e86569
feat(optimizer): propagate jobId through model + api conversions
mkuchenbecker May 20, 2026
cc8aa80
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker May 20, 2026
efcceea
feat(optimizer): propagate jobId through model ↔ db conversions
mkuchenbecker May 20, 2026
f85edd5
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 20, 2026
c00f201
chore(optimizer): rename OPTIMIZER_DB_USERNAME → OPTIMIZER_DB_USER
mkuchenbecker May 20, 2026
1fe71f0
refactor(optimizer): rename CompleteOperationRequest → UpdateOperatio…
mkuchenbecker May 20, 2026
fb5e726
Merge branch 'mkuchenb/optimizer-0' into mkuchenb/optimizer-1
mkuchenbecker May 20, 2026
ad0c0f1
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 20, 2026
947bedf
refactor(optimizer): rename completeOperation → updateOperation
mkuchenbecker May 20, 2026
b96c388
Merge remote-tracking branch 'linkedin/main' into mkuchenb/optimizer-1
mkuchenbecker May 20, 2026
d65b511
refactor(optimizer-repo): unify find/updateBatch with Optional params
mkuchenbecker May 21, 2026
78de390
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 21, 2026
49e43bc
refactor(optimizer-service): use Optional repo API + configurable limit
mkuchenbecker May 21, 2026
b69e09a
test(optimizer-repo): truncate Instant to micros for CI precision
mkuchenbecker May 21, 2026
a028a98
Merge branch 'mkuchenb/optimizer-1' into mkuchenb/optimizer-2
mkuchenbecker May 21, 2026
6eb6a1e
Merge remote-tracking branch 'linkedin/main' into mkuchenb/optimizer-2
mkuchenbecker May 21, 2026
a89e037
feat(optimizer): require limit on list-API endpoints
mkuchenbecker May 21, 2026
1e361af
feat(optimizer): basic error-code handling across controllers
mkuchenbecker May 22, 2026
a37169d
refactor(optimizer): simplify error handling per PR review
mkuchenbecker May 22, 2026
6416c9d
refactor(optimizer): drop GlobalExceptionHandler + ApiError; use Spri…
mkuchenbecker May 22, 2026
bbef386
refactor(optimizer): revert UpdateOperationRequest doc edits
mkuchenbecker May 22, 2026
02bbc5c
(wip) feat(optimizer): basic error-code handling across controllers (…
mkuchenbecker May 22, 2026
144da72
Merge remote-tracking branch 'linkedin/mkuchenb/optimizer-2' into mku…
mkuchenbecker May 22, 2026
6ef7964
docs(optimizer): add @ApiResponses to controllers for OpenAPI spec
mkuchenbecker May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.linkedin.openhouse.optimizer.api.controller;

import com.linkedin.openhouse.optimizer.api.spec.OperationStatus;
import com.linkedin.openhouse.optimizer.api.spec.OperationType;
import com.linkedin.openhouse.optimizer.api.spec.TableOperations;
import com.linkedin.openhouse.optimizer.api.spec.TableOperationsHistory;
import com.linkedin.openhouse.optimizer.api.spec.UpdateOperationRequest;
import com.linkedin.openhouse.optimizer.service.OptimizerDataService;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

/** REST controller for {@code table_operations}. */
@RestController
@RequestMapping("/v1/optimizer/operations")
@RequiredArgsConstructor
public class TableOperationsController {
Comment thread
mkuchenbecker marked this conversation as resolved.

private final OptimizerDataService service;

/**
* Report an update to an operation. {@code id} comes from the URL; the body's {@code operationId}
* must match (the controller rejects mismatched requests with 400). The backend looks up the
* operation row, writes a history entry with the operation's table metadata, and returns 201
* Created with the history row, or 404 if the operation does not exist.
*/
@ApiResponses(
value = {
@ApiResponse(responseCode = "201", description = "Operation UPDATE: CREATED"),
@ApiResponse(responseCode = "400", description = "Operation UPDATE: BAD_REQUEST"),
@ApiResponse(responseCode = "404", description = "Operation UPDATE: NOT_FOUND")
})
@PostMapping("/{id}/update")
public ResponseEntity<TableOperationsHistory> updateOperation(
@PathVariable String id, @RequestBody UpdateOperationRequest request) {
if (!StringUtils.hasText(request.getOperationId())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "operationId is required");
}
if (!Objects.equals(id, request.getOperationId())) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST,
String.format(
"operationId in body (%s) does not match path id (%s)",
request.getOperationId(), id));
}
if (request.getStatus() == null) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "status is required");
}
return service
.updateOperation(id, request.getStatus().toModel())
.map(
history ->
ResponseEntity.status(HttpStatus.CREATED)
.body(TableOperationsHistory.fromModel(history)))
.orElseThrow(
() ->
new ResponseStatusException(
HttpStatus.NOT_FOUND, String.format("no operation with id %s", id)));
}

/** Fetch a single operation row by its ID, regardless of status. Returns 404 if not found. */
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "Operation GET: OK"),
@ApiResponse(responseCode = "404", description = "Operation GET: NOT_FOUND")
})
@GetMapping("/{id}")
public ResponseEntity<TableOperations> getTableOperation(@PathVariable String id) {
return service
.getTableOperation(id)
.map(TableOperations::fromModel)
.map(ResponseEntity::ok)
.orElseThrow(
() ->
new ResponseStatusException(
HttpStatus.NOT_FOUND, String.format("no operation with id %s", id)));
}

/**
* List operations matching the given filters, capped at {@code limit} rows. Every filter is
* optional; {@code limit} is required so callers always state how much they want back.
*/
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "Operation SEARCH: OK"),
@ApiResponse(responseCode = "400", description = "Operation SEARCH: BAD_REQUEST")
})
@GetMapping
public ResponseEntity<List<TableOperations>> listTableOperations(
@RequestParam(required = false) OperationType operationType,
@RequestParam(required = false) OperationStatus status,
@RequestParam(required = false) String databaseName,
@RequestParam(required = false) String tableName,
@RequestParam(required = false) String tableUuid,
@RequestParam int limit) {
List<TableOperations> result =
service
.listTableOperations(
Optional.ofNullable(operationType).map(OperationType::toModel),
Optional.ofNullable(status).map(OperationStatus::toModel),
Optional.ofNullable(databaseName),
Optional.ofNullable(tableName),
Optional.ofNullable(tableUuid),
limit)
.stream()
.map(TableOperations::fromModel)
.collect(Collectors.toList());
return ResponseEntity.ok(result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.linkedin.openhouse.optimizer.api.controller;

import com.linkedin.openhouse.optimizer.api.spec.TableOperationsHistory;
import com.linkedin.openhouse.optimizer.service.OptimizerDataService;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/** REST controller for {@code table_operations_history}. */
@RestController
@RequestMapping("/v1/optimizer/operations-history")
@RequiredArgsConstructor
public class TableOperationsHistoryController {

private final OptimizerDataService service;

/** Append a completed-job result. Called by the SparkJob after each run (success or failure). */
@ApiResponses(
value = {
@ApiResponse(responseCode = "201", description = "OperationsHistory CREATE: CREATED")
})
@PostMapping
public ResponseEntity<TableOperationsHistory> appendHistory(
@RequestBody TableOperationsHistory dto) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(TableOperationsHistory.fromModel(service.appendHistory(dto.toModel())));
}

/**
* Return the most recent history for a table, newest first, capped at {@code limit} rows. {@code
* limit} is required.
*/
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "OperationsHistory GET: OK"),
@ApiResponse(responseCode = "400", description = "OperationsHistory GET: BAD_REQUEST")
})
@GetMapping("/{tableUuid}")
public ResponseEntity<List<TableOperationsHistory>> getHistory(
@PathVariable String tableUuid, @RequestParam int limit) {
List<TableOperationsHistory> result =
service.getHistory(tableUuid, limit).stream()
.map(TableOperationsHistory::fromModel)
.collect(Collectors.toList());
return ResponseEntity.ok(result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.linkedin.openhouse.optimizer.api.controller;

import com.linkedin.openhouse.optimizer.api.spec.TableStats;
import com.linkedin.openhouse.optimizer.api.spec.TableStatsHistory;
import com.linkedin.openhouse.optimizer.api.spec.UpsertTableStatsRequest;
import com.linkedin.openhouse.optimizer.service.OptimizerDataService;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import java.time.Instant;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

/** REST controller for managing per-table stats in the optimizer DB. */
@RestController
@RequestMapping("/v1/optimizer/stats")
@RequiredArgsConstructor
public class TableStatsController {

private final OptimizerDataService service;

/**
* Create or overwrite the stats row for {@code tableUuid}. Called by the Tables Service on every
* Iceberg commit. Idempotent.
*/
@ApiResponses(value = {@ApiResponse(responseCode = "200", description = "Stats PUT: OK")})
@PutMapping("/{tableUuid}")
public ResponseEntity<TableStats> upsertTableStats(
@PathVariable String tableUuid, @RequestBody UpsertTableStatsRequest request) {
Comment thread
mkuchenbecker marked this conversation as resolved.
return ResponseEntity.ok(
TableStats.fromModel(service.upsertTableStats(request.toModel(tableUuid))));
}

/** Fetch the stats row for {@code tableUuid}. Returns 404 if no stats have been written yet. */
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "Stats GET: OK"),
@ApiResponse(responseCode = "404", description = "Stats GET: NOT_FOUND")
})
@GetMapping("/{tableUuid}")
public ResponseEntity<TableStats> getTableStats(@PathVariable String tableUuid) {
return service
.getTableStats(tableUuid)
.map(TableStats::fromModel)
.map(ResponseEntity::ok)
.orElseThrow(
() ->
new ResponseStatusException(
HttpStatus.NOT_FOUND, String.format("no stats for tableUuid %s", tableUuid)));
}

/**
* List stats rows matching the given filters, capped at {@code limit} rows. Every filter is
* optional; {@code limit} is required so callers always state how much they want back.
*/
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "Stats SEARCH: OK"),
@ApiResponse(responseCode = "400", description = "Stats SEARCH: BAD_REQUEST")
})
@GetMapping
public ResponseEntity<List<TableStats>> listTableStats(
@RequestParam(required = false) String databaseName,
@RequestParam(required = false) String tableName,
@RequestParam(required = false) String tableUuid,
@RequestParam int limit) {
List<TableStats> result =
service
.listTableStats(
Optional.ofNullable(databaseName),
Optional.ofNullable(tableName),
Optional.ofNullable(tableUuid),
limit)
.stream()
.map(TableStats::fromModel)
.collect(Collectors.toList());
return ResponseEntity.ok(result);
}

/**
* Return per-commit stats history for {@code tableUuid}, newest first, capped at {@code limit}
* rows. Optional {@code since} filter (inclusive). {@code limit} is required.
*/
@ApiResponses(
value = {
@ApiResponse(responseCode = "200", description = "StatsHistory GET: OK"),
@ApiResponse(responseCode = "400", description = "StatsHistory GET: BAD_REQUEST")
})
@GetMapping("/{tableUuid}/history")
public ResponseEntity<List<TableStatsHistory>> getStatsHistory(
@PathVariable String tableUuid,
@RequestParam(required = false) Instant since,
@RequestParam int limit) {
List<TableStatsHistory> result =
service.getStatsHistory(tableUuid, Optional.ofNullable(since), limit).stream()
.map(TableStatsHistory::fromModel)
.collect(Collectors.toList());
return ResponseEntity.ok(result);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.linkedin.openhouse.optimizer.service;

import com.linkedin.openhouse.optimizer.model.HistoryStatusDto;
import com.linkedin.openhouse.optimizer.model.OperationStatusDto;
import com.linkedin.openhouse.optimizer.model.OperationTypeDto;
import com.linkedin.openhouse.optimizer.model.TableOperationDto;
import com.linkedin.openhouse.optimizer.model.TableOperationsHistoryDto;
import com.linkedin.openhouse.optimizer.model.TableStatsDto;
import com.linkedin.openhouse.optimizer.model.TableStatsHistoryDto;
import java.time.Instant;
import java.util.List;
import java.util.Optional;

/**
* Service interface for optimizer data operations.
*
* <p>The service is the boundary between the wire-API surface and the database. Inputs and outputs
* are <em>internal-model</em> types only — callers (controllers, future CLI, in-process consumers)
* convert at their own edge. No api/-package types appear here.
*/
public interface OptimizerDataService {

// --- TableOperations ---

/**
* List operations matching the given filters, capped at {@code limit} rows. Every filter
* parameter is optional — pass {@link Optional#empty()} to skip that filter.
*/
List<TableOperationDto> listTableOperations(
Optional<OperationTypeDto> operationType,
Optional<OperationStatusDto> status,
Optional<String> databaseName,
Optional<String> tableName,
Optional<String> tableUuid,
int limit);

/**
* Update an operation by writing a history entry. Looks up the operation row by {@code
* operationId}, copies its table metadata into a new history row with the supplied terminal
* {@code status}, and saves it. Returns the history record, or empty if the operation does not
* exist.
*/
Optional<TableOperationsHistoryDto> updateOperation(String operationId, HistoryStatusDto status);

/**
* Return the operation row for {@code id} regardless of status, or empty if it does not exist.
* Used to poll a specific operation (e.g. waiting for SUCCESS after a Spark job completes).
*/
Optional<TableOperationDto> getTableOperation(String id);

// --- TableStatsDto ---

/**
* Create or update the stats row for {@code stats.getTableUuid()}. Fully idempotent: the same
* call overwrites the previous snapshot with the latest commit values. The service stamps {@link
* TableStatsDto#getUpdatedAt()} server-side and returns the resulting {@link TableStatsDto}.
*/
TableStatsDto upsertTableStats(TableStatsDto stats);

/** Return the stats row for {@code tableUuid}, or empty if none exists. */
Optional<TableStatsDto> getTableStats(String tableUuid);

/**
* List stats rows matching the given filters, capped at {@code limit} rows. Every filter
* parameter is optional — pass {@link Optional#empty()} to skip that filter.
*/
List<TableStatsDto> listTableStats(
Optional<String> databaseName,
Optional<String> tableName,
Optional<String> tableUuid,
int limit);

/**
* Return per-commit stats history for {@code tableUuid}, newest first.
*
* @param tableUuid the stable table UUID
* @param since if present, only return rows recorded at or after this instant
* @param limit maximum number of rows to return
*/
List<TableStatsHistoryDto> getStatsHistory(String tableUuid, Optional<Instant> since, int limit);

// --- TableOperationsHistoryDto ---

/** Append a completed-job result record. */
TableOperationsHistoryDto appendHistory(TableOperationsHistoryDto history);

/**
* Return the most recent history rows for a table UUID, newest first.
*
* @param tableUuid the stable table UUID
* @param limit maximum number of rows to return
*/
List<TableOperationsHistoryDto> getHistory(String tableUuid, int limit);
}
Loading