feat: pin running instances to immutable published definition versions

Replace register/replace with publish so new graphs can ship without rewriting old ones, and keep in-flight work on the version it started with.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
0264408
2026-09-16 10:30:14 +08:00
co-authored by Cursor
parent 8bc628613d
commit 9bda7c417c
35 changed files with 688 additions and 382 deletions
@@ -15,6 +15,8 @@ import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
@@ -24,36 +26,45 @@ import java.util.Optional;
/** JDBC implementation of the definition storage port; steps and their candidates live in separate tables. */
public final class JdbcProcessDefinitionRepository implements ProcessDefinitionRepository {
private static final String INSERT_PROCESS =
"INSERT INTO ordo_process (id, current_version, name) VALUES (?, ?, ?)";
private static final String UPDATE_PROCESS =
"UPDATE ordo_process SET current_version = ?, name = ? WHERE id = ?";
private static final String LOCK_PROCESS =
"SELECT current_version FROM ordo_process WHERE id = ? FOR UPDATE";
private static final String INSERT_DEFINITION =
"INSERT INTO ordo_process_definition (id, name) VALUES (?, ?)";
"INSERT INTO ordo_process_definition (id, version, name, created_at) VALUES (?, ?, ?, ?)";
private static final String INSERT_STEP =
"INSERT INTO ordo_approval_step (definition_id, step_id, step_name, policy, step_order, kind, action_key,"
+ " due_after, due_then, due_to, due_action) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
"INSERT INTO ordo_approval_step (definition_id, definition_version, step_id, step_name, policy, step_order,"
+ " kind, action_key, due_after, due_then, due_to, due_action)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
private static final String INSERT_CANDIDATE =
"INSERT INTO ordo_step_candidate (definition_id, step_id, candidate, candidate_order) VALUES (?, ?, ?, ?)";
"INSERT INTO ordo_step_candidate (definition_id, definition_version, step_id, candidate, candidate_order)"
+ " VALUES (?, ?, ?, ?, ?)";
private static final String INSERT_TRANSITION =
"INSERT INTO ordo_step_transition (definition_id, from_step_id, to_step_id, condition_key, priority) VALUES (?, ?, ?, ?, ?)";
private static final String UPDATE_DEFINITION_NAME =
"UPDATE ordo_process_definition SET name = ? WHERE id = ?";
private static final String DELETE_TRANSITIONS =
"DELETE FROM ordo_step_transition WHERE definition_id = ?";
private static final String DELETE_CANDIDATES =
"DELETE FROM ordo_step_candidate WHERE definition_id = ?";
private static final String DELETE_STEPS =
"DELETE FROM ordo_approval_step WHERE definition_id = ?";
"INSERT INTO ordo_step_transition (definition_id, definition_version, from_step_id, to_step_id,"
+ " condition_key, priority) VALUES (?, ?, ?, ?, ?, ?)";
private static final String SELECT_DEFINITION =
"SELECT id, name FROM ordo_process_definition WHERE id = ?";
"SELECT name FROM ordo_process_definition WHERE id = ? AND version = ?";
private static final String SELECT_STEPS =
"SELECT step_id, step_name, policy, kind, action_key, due_after, due_then, due_to, due_action"
+ " FROM ordo_approval_step"
+ " WHERE definition_id = ? ORDER BY step_order";
+ " WHERE definition_id = ? AND definition_version = ? ORDER BY step_order";
private static final String SELECT_CANDIDATES =
"SELECT step_id, candidate FROM ordo_step_candidate WHERE definition_id = ? ORDER BY step_id, candidate_order";
"SELECT step_id, candidate FROM ordo_step_candidate"
+ " WHERE definition_id = ? AND definition_version = ? ORDER BY step_id, candidate_order";
private static final String SELECT_TRANSITIONS =
"SELECT from_step_id, to_step_id, condition_key, priority FROM ordo_step_transition "
+ "WHERE definition_id = ? ORDER BY from_step_id, priority";
private static final String SELECT_DEFINITIONS_PAGE =
"SELECT id, name FROM ordo_process_definition ORDER BY id LIMIT ? OFFSET ?";
+ "WHERE definition_id = ? AND definition_version = ? ORDER BY from_step_id, priority";
private static final String SELECT_LATEST_PAGE =
"SELECT p.id, p.current_version, d.name FROM ordo_process p"
+ " JOIN ordo_process_definition d ON d.id = p.id AND d.version = p.current_version"
+ " ORDER BY p.id LIMIT ? OFFSET ?";
private static final String COUNT_PROCESSES = "SELECT COUNT(*) FROM ordo_process";
private static final String SELECT_VERSIONS_PAGE =
"SELECT version, name FROM ordo_process_definition WHERE id = ? ORDER BY version DESC LIMIT ? OFFSET ?";
private static final String COUNT_VERSIONS =
"SELECT COUNT(*) FROM ordo_process_definition WHERE id = ?";
private final JdbcConnectionProvider connectionProvider;
@@ -62,62 +73,51 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
}
@Override
public boolean insertIfAbsent(ProcessDefinition definition) {
public ProcessDefinition publish(ProcessDefinition definition) {
Objects.requireNonNull(definition, "definition must not be null");
Connection connection = connectionProvider.getConnection();
try {
if (!insertDefinitionRow(connection, definition)) {
return false;
Integer current = lockCurrentVersion(connection, definition.id());
if (current == null) {
if (insertProcessRow(connection, definition.id(), 1, definition.name())) {
ProcessDefinition first = definition.withVersion(1);
insertDefinitionVersion(connection, first);
insertGraph(connection, first);
return first;
}
current = lockCurrentVersion(connection, definition.id());
if (current == null) {
throw new JdbcStorageException(
"failed to lock process after concurrent publish: " + definition.id(),
new IllegalStateException("process row missing"));
}
}
insertGraph(connection, definition);
return true;
ProcessDefinition latest = assemble(connection, definition.id(), current);
if (latest.sameGraph(definition)) {
return latest;
}
int next = current + 1;
ProcessDefinition published = definition.withVersion(next);
insertDefinitionVersion(connection, published);
insertGraph(connection, published);
updateProcess(connection, definition.id(), next, definition.name());
return published;
} catch (SQLException e) {
throw new JdbcStorageException("failed to insert definition: " + definition.id(), e);
throw new JdbcStorageException("failed to publish definition: " + definition.id(), e);
} finally {
connectionProvider.close(connection);
}
}
@Override
public void upsert(ProcessDefinition definition) {
Objects.requireNonNull(definition, "definition must not be null");
public Optional<ProcessDefinition> findLatest(String definitionId) {
Connection connection = connectionProvider.getConnection();
try {
// PostgreSQL aborts the current transaction on unique-constraint violations,
// so upsert must not probe existence via a failing INSERT.
if (definitionExists(connection, definition.id())) {
updateDefinitionName(connection, definition);
deleteGraph(connection, definition.id());
} else {
try (PreparedStatement insert = connection.prepareStatement(INSERT_DEFINITION)) {
insert.setString(1, definition.id());
insert.setString(2, definition.name());
insert.executeUpdate();
}
Integer version = currentVersion(connection, definitionId);
if (version == null) {
return Optional.empty();
}
insertGraph(connection, definition);
} catch (SQLException e) {
throw new JdbcStorageException("failed to upsert definition: " + definition.id(), e);
} finally {
connectionProvider.close(connection);
}
}
@Override
public Optional<ProcessDefinition> findById(String definitionId) {
Connection connection = connectionProvider.getConnection();
try {
String name;
try (PreparedStatement selectDefinition = connection.prepareStatement(SELECT_DEFINITION)) {
selectDefinition.setString(1, definitionId);
try (ResultSet resultSet = selectDefinition.executeQuery()) {
if (!resultSet.next()) {
return Optional.empty();
}
name = resultSet.getString("name");
}
}
return Optional.of(assemble(connection, definitionId, name));
return Optional.of(assemble(connection, definitionId, version));
} catch (SQLException e) {
throw new JdbcStorageException("failed to load definition: " + definitionId, e);
} finally {
@@ -125,24 +125,41 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
}
}
@Override
public Optional<ProcessDefinition> find(String definitionId, int version) {
Connection connection = connectionProvider.getConnection();
try {
if (!definitionExists(connection, definitionId, version)) {
return Optional.empty();
}
return Optional.of(assemble(connection, definitionId, version));
} catch (SQLException e) {
throw new JdbcStorageException("failed to load definition: " + definitionId + " version " + version, e);
} finally {
connectionProvider.close(connection);
}
}
@Override
public Page<ProcessDefinition> findAll(PageRequest pageRequest) {
Objects.requireNonNull(pageRequest, "pageRequest must not be null");
Connection connection = connectionProvider.getConnection();
try {
long total = PageSupport.count(connection, "SELECT COUNT(*) FROM ordo_process_definition", List.of());
long total = PageSupport.count(connection, COUNT_PROCESSES, List.of());
List<ProcessDefinition> content = new ArrayList<>();
try (PreparedStatement select = connection.prepareStatement(SELECT_DEFINITIONS_PAGE)) {
try (PreparedStatement select = connection.prepareStatement(SELECT_LATEST_PAGE)) {
select.setInt(1, pageRequest.size());
select.setInt(2, pageRequest.offset());
List<Map.Entry<String, String>> idsAndNames = new ArrayList<>();
List<int[]> versions = new ArrayList<>();
List<String> ids = new ArrayList<>();
try (ResultSet resultSet = select.executeQuery()) {
while (resultSet.next()) {
idsAndNames.add(Map.entry(resultSet.getString("id"), resultSet.getString("name")));
ids.add(resultSet.getString("id"));
versions.add(new int[] {resultSet.getInt("current_version")});
}
}
for (Map.Entry<String, String> idAndName : idsAndNames) {
content.add(assemble(connection, idAndName.getKey(), idAndName.getValue()));
for (int i = 0; i < ids.size(); i++) {
content.add(assemble(connection, ids.get(i), versions.get(i)[0]));
}
}
return new Page<>(content, total, pageRequest.page(), pageRequest.size());
@@ -153,10 +170,52 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
}
}
private static ProcessDefinition assemble(Connection connection, String definitionId, String name) throws SQLException {
@Override
public Page<ProcessDefinition> findVersions(String definitionId, PageRequest pageRequest) {
Objects.requireNonNull(pageRequest, "pageRequest must not be null");
Connection connection = connectionProvider.getConnection();
try {
long total = PageSupport.count(connection, COUNT_VERSIONS, List.of(definitionId));
List<Integer> versionNumbers = new ArrayList<>();
try (PreparedStatement select = connection.prepareStatement(SELECT_VERSIONS_PAGE)) {
select.setString(1, definitionId);
select.setInt(2, pageRequest.size());
select.setInt(3, pageRequest.offset());
try (ResultSet resultSet = select.executeQuery()) {
while (resultSet.next()) {
versionNumbers.add(resultSet.getInt("version"));
}
}
}
List<ProcessDefinition> content = new ArrayList<>();
for (int version : versionNumbers) {
content.add(assemble(connection, definitionId, version));
}
return new Page<>(content, total, pageRequest.page(), pageRequest.size());
} catch (SQLException e) {
throw new JdbcStorageException("failed to list definition versions: " + definitionId, e);
} finally {
connectionProvider.close(connection);
}
}
private static ProcessDefinition assemble(Connection connection, String definitionId, int version)
throws SQLException {
String name;
try (PreparedStatement selectDefinition = connection.prepareStatement(SELECT_DEFINITION)) {
selectDefinition.setString(1, definitionId);
selectDefinition.setInt(2, version);
try (ResultSet resultSet = selectDefinition.executeQuery()) {
if (!resultSet.next()) {
throw new SQLException("definition row missing: " + definitionId + " version " + version);
}
name = resultSet.getString("name");
}
}
List<StepRow> stepRows = new ArrayList<>();
try (PreparedStatement selectSteps = connection.prepareStatement(SELECT_STEPS)) {
selectSteps.setString(1, definitionId);
selectSteps.setInt(2, version);
try (ResultSet resultSet = selectSteps.executeQuery()) {
while (resultSet.next()) {
stepRows.add(ApprovalStepMapper.readRow(resultSet));
@@ -166,6 +225,7 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
Map<String, List<String>> candidatesByStep = new LinkedHashMap<>();
try (PreparedStatement selectCandidates = connection.prepareStatement(SELECT_CANDIDATES)) {
selectCandidates.setString(1, definitionId);
selectCandidates.setInt(2, version);
try (ResultSet resultSet = selectCandidates.executeQuery()) {
while (resultSet.next()) {
candidatesByStep.computeIfAbsent(resultSet.getString("step_id"), key -> new ArrayList<>())
@@ -182,6 +242,7 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
List<StepTransition> transitions = new ArrayList<>();
try (PreparedStatement selectTransitions = connection.prepareStatement(SELECT_TRANSITIONS)) {
selectTransitions.setString(1, definitionId);
selectTransitions.setInt(2, version);
try (ResultSet resultSet = selectTransitions.executeQuery()) {
while (resultSet.next()) {
TransitionRow row = StepTransitionMapper.readRow(resultSet);
@@ -190,23 +251,46 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
}
}
}
return new ProcessDefinition(definitionId, name, steps, transitions);
return new ProcessDefinition(definitionId, version, name, steps, transitions);
}
private static boolean definitionExists(Connection connection, String definitionId) throws SQLException {
private static Integer lockCurrentVersion(Connection connection, String definitionId) throws SQLException {
try (PreparedStatement select = connection.prepareStatement(LOCK_PROCESS)) {
select.setString(1, definitionId);
try (ResultSet resultSet = select.executeQuery()) {
return resultSet.next() ? resultSet.getInt("current_version") : null;
}
}
}
private static Integer currentVersion(Connection connection, String definitionId) throws SQLException {
try (PreparedStatement select = connection.prepareStatement(
"SELECT current_version FROM ordo_process WHERE id = ?")) {
select.setString(1, definitionId);
try (ResultSet resultSet = select.executeQuery()) {
return resultSet.next() ? resultSet.getInt("current_version") : null;
}
}
}
private static boolean definitionExists(Connection connection, String definitionId, int version)
throws SQLException {
try (PreparedStatement select = connection.prepareStatement(SELECT_DEFINITION)) {
select.setString(1, definitionId);
select.setInt(2, version);
try (ResultSet resultSet = select.executeQuery()) {
return resultSet.next();
}
}
}
private static boolean insertDefinitionRow(Connection connection, ProcessDefinition definition) throws SQLException {
try (PreparedStatement insertDefinition = connection.prepareStatement(INSERT_DEFINITION)) {
insertDefinition.setString(1, definition.id());
insertDefinition.setString(2, definition.name());
insertDefinition.executeUpdate();
private static boolean insertProcessRow(Connection connection, String id, int version, String name)
throws SQLException {
try (PreparedStatement insert = connection.prepareStatement(INSERT_PROCESS)) {
insert.setString(1, id);
insert.setInt(2, version);
insert.setString(3, name);
insert.executeUpdate();
return true;
} catch (SQLException e) {
if (isDuplicateKey(e)) {
@@ -216,26 +300,23 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
}
}
private static void updateDefinitionName(Connection connection, ProcessDefinition definition) throws SQLException {
try (PreparedStatement update = connection.prepareStatement(UPDATE_DEFINITION_NAME)) {
update.setString(1, definition.name());
update.setString(2, definition.id());
private static void updateProcess(Connection connection, String id, int version, String name) throws SQLException {
try (PreparedStatement update = connection.prepareStatement(UPDATE_PROCESS)) {
update.setInt(1, version);
update.setString(2, name);
update.setString(3, id);
update.executeUpdate();
}
}
private static void deleteGraph(Connection connection, String definitionId) throws SQLException {
try (PreparedStatement deleteTransitions = connection.prepareStatement(DELETE_TRANSITIONS)) {
deleteTransitions.setString(1, definitionId);
deleteTransitions.executeUpdate();
}
try (PreparedStatement deleteCandidates = connection.prepareStatement(DELETE_CANDIDATES)) {
deleteCandidates.setString(1, definitionId);
deleteCandidates.executeUpdate();
}
try (PreparedStatement deleteSteps = connection.prepareStatement(DELETE_STEPS)) {
deleteSteps.setString(1, definitionId);
deleteSteps.executeUpdate();
private static void insertDefinitionVersion(Connection connection, ProcessDefinition definition)
throws SQLException {
try (PreparedStatement insert = connection.prepareStatement(INSERT_DEFINITION)) {
insert.setString(1, definition.id());
insert.setInt(2, definition.version());
insert.setString(3, definition.name());
insert.setTimestamp(4, Timestamp.from(Instant.now()));
insert.executeUpdate();
}
}
@@ -244,22 +325,23 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
for (ApprovalStep step : definition.steps()) {
try (PreparedStatement insertStep = connection.prepareStatement(INSERT_STEP)) {
insertStep.setString(1, definition.id());
insertStep.setString(2, step.id());
insertStep.setString(3, step.name());
insertStep.setString(4, step.policy().name());
insertStep.setInt(5, stepOrder++);
insertStep.setString(6, step.kind().name());
insertStep.setString(7, step.actionKey());
insertStep.setInt(2, definition.version());
insertStep.setString(3, step.id());
insertStep.setString(4, step.name());
insertStep.setString(5, step.policy().name());
insertStep.setInt(6, stepOrder++);
insertStep.setString(7, step.kind().name());
insertStep.setString(8, step.actionKey());
if (step.due() == null) {
insertStep.setString(8, null);
insertStep.setString(9, null);
insertStep.setString(10, null);
insertStep.setString(11, null);
insertStep.setString(12, null);
} else {
insertStep.setString(8, step.due().after().toString());
insertStep.setString(9, step.due().then().name());
insertStep.setString(10, step.due().to());
insertStep.setString(11, step.due().action());
insertStep.setString(9, step.due().after().toString());
insertStep.setString(10, step.due().then().name());
insertStep.setString(11, step.due().to());
insertStep.setString(12, step.due().action());
}
insertStep.executeUpdate();
}
@@ -267,9 +349,10 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
for (String candidate : step.candidates()) {
try (PreparedStatement insertCandidate = connection.prepareStatement(INSERT_CANDIDATE)) {
insertCandidate.setString(1, definition.id());
insertCandidate.setString(2, step.id());
insertCandidate.setString(3, candidate);
insertCandidate.setInt(4, candidateOrder++);
insertCandidate.setInt(2, definition.version());
insertCandidate.setString(3, step.id());
insertCandidate.setString(4, candidate);
insertCandidate.setInt(5, candidateOrder++);
insertCandidate.executeUpdate();
}
}
@@ -277,10 +360,11 @@ public final class JdbcProcessDefinitionRepository implements ProcessDefinitionR
for (StepTransition transition : definition.transitions()) {
try (PreparedStatement insertTransition = connection.prepareStatement(INSERT_TRANSITION)) {
insertTransition.setString(1, definition.id());
insertTransition.setString(2, transition.fromStepId());
insertTransition.setString(3, transition.toStepId());
insertTransition.setString(4, transition.conditionKey());
insertTransition.setInt(5, transition.priority());
insertTransition.setInt(2, definition.version());
insertTransition.setString(3, transition.fromStepId());
insertTransition.setString(4, transition.toStepId());
insertTransition.setString(5, transition.conditionKey());
insertTransition.setInt(6, transition.priority());
insertTransition.executeUpdate();
}
}
@@ -22,14 +22,14 @@ import java.util.Optional;
/** JDBC implementation of the instance storage port. */
public final class JdbcProcessInstanceRepository implements ProcessInstanceRepository {
private static final String INSERT_INSTANCE =
"INSERT INTO ordo_process_instance (id, definition_id, initiator, status, context_json, started_at, finished_at)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
"INSERT INTO ordo_process_instance (id, definition_id, definition_version, initiator, status, context_json,"
+ " started_at, finished_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
private static final String UPDATE_INSTANCE =
"UPDATE ordo_process_instance SET status = ?, finished_at = ? WHERE id = ?";
private static final String COMPLETE_IF_RUNNING =
"UPDATE ordo_process_instance SET status = ?, finished_at = ? WHERE id = ? AND status = 'RUNNING'";
private static final String SELECT_INSTANCE =
"SELECT id, definition_id, initiator, status, context_json, started_at, finished_at"
"SELECT id, definition_id, definition_version, initiator, status, context_json, started_at, finished_at"
+ " FROM ordo_process_instance WHERE id = ?";
private static final String EXISTS_RUNNING =
"SELECT 1 FROM ordo_process_instance WHERE definition_id = ? AND status = ? LIMIT 1";
@@ -124,8 +124,8 @@ public final class JdbcProcessInstanceRepository implements ProcessInstanceRepos
try {
long total = PageSupport.count(connection,
"SELECT COUNT(*) FROM ordo_process_instance" + where.sql(), where.params());
String sql = "SELECT id, definition_id, initiator, status, context_json, started_at, finished_at"
+ " FROM ordo_process_instance" + where.sql()
String sql = "SELECT id, definition_id, definition_version, initiator, status, context_json, started_at,"
+ " finished_at FROM ordo_process_instance" + where.sql()
+ " ORDER BY started_at DESC, id DESC LIMIT ? OFFSET ?";
List<ProcessInstance> content = new ArrayList<>();
try (PreparedStatement select = connection.prepareStatement(sql)) {
@@ -16,11 +16,12 @@ public final class ProcessInstanceMapper {
public static void bindInsert(PreparedStatement statement, ProcessInstance instance) throws SQLException {
statement.setString(1, instance.id());
statement.setString(2, instance.definitionId());
statement.setString(3, instance.initiator());
statement.setString(4, instance.status().name());
statement.setString(5, ProcessContextCodec.encode(instance.context()));
statement.setTimestamp(6, Timestamp.from(instance.startedAt()));
statement.setTimestamp(7, instance.finishedAt() == null ? null : Timestamp.from(instance.finishedAt()));
statement.setInt(3, instance.definitionVersion());
statement.setString(4, instance.initiator());
statement.setString(5, instance.status().name());
statement.setString(6, ProcessContextCodec.encode(instance.context()));
statement.setTimestamp(7, Timestamp.from(instance.startedAt()));
statement.setTimestamp(8, instance.finishedAt() == null ? null : Timestamp.from(instance.finishedAt()));
}
public static void bindUpdate(PreparedStatement statement, ProcessInstance instance) throws SQLException {
@@ -35,6 +36,7 @@ public final class ProcessInstanceMapper {
return new ProcessInstance(
resultSet.getString("id"),
resultSet.getString("definition_id"),
resultSet.getInt("definition_version"),
resultSet.getString("initiator"),
ProcessStatus.valueOf(resultSet.getString("status")),
startedAt.toInstant(),
@@ -0,0 +1,96 @@
CREATE TABLE ordo_process (
id VARCHAR(64) PRIMARY KEY,
current_version INTEGER NOT NULL,
name VARCHAR(255) NOT NULL
);
INSERT INTO ordo_process (id, current_version, name)
SELECT id, 1, name FROM ordo_process_definition;
ALTER TABLE ordo_process_instance DROP CONSTRAINT fk_process_instance_definition;
ALTER TABLE ordo_approval_step DROP CONSTRAINT fk_approval_step_definition;
ALTER TABLE ordo_step_candidate DROP CONSTRAINT fk_step_candidate_step;
ALTER TABLE ordo_step_transition DROP CONSTRAINT fk_transition_from;
ALTER TABLE ordo_step_transition DROP CONSTRAINT fk_transition_to;
CREATE TABLE ordo_process_definition_v7 (
id VARCHAR(64) NOT NULL,
version INTEGER NOT NULL,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id, version)
);
INSERT INTO ordo_process_definition_v7 (id, version, name)
SELECT id, 1, name FROM ordo_process_definition;
DROP TABLE ordo_process_definition;
ALTER TABLE ordo_process_definition_v7 RENAME TO ordo_process_definition;
CREATE TABLE ordo_approval_step_v7 (
definition_id VARCHAR(64) NOT NULL,
definition_version INTEGER NOT NULL,
step_id VARCHAR(64) NOT NULL,
step_name VARCHAR(255) NOT NULL,
step_order INTEGER NOT NULL,
policy VARCHAR(16) NOT NULL,
kind VARCHAR(16) NOT NULL,
action_key VARCHAR(255),
due_after VARCHAR(32),
due_then VARCHAR(16),
due_to VARCHAR(255),
due_action VARCHAR(255),
PRIMARY KEY (definition_id, definition_version, step_id),
CONSTRAINT fk_approval_step_definition FOREIGN KEY (definition_id, definition_version)
REFERENCES ordo_process_definition (id, version)
);
INSERT INTO ordo_approval_step_v7 (definition_id, definition_version, step_id, step_name, step_order, policy, kind,
action_key, due_after, due_then, due_to, due_action)
SELECT definition_id, 1, step_id, step_name, step_order, policy, kind, action_key, due_after, due_then, due_to,
due_action
FROM ordo_approval_step;
DROP TABLE ordo_approval_step;
ALTER TABLE ordo_approval_step_v7 RENAME TO ordo_approval_step;
CREATE TABLE ordo_step_candidate_v7 (
definition_id VARCHAR(64) NOT NULL,
definition_version INTEGER NOT NULL,
step_id VARCHAR(64) NOT NULL,
candidate VARCHAR(255) NOT NULL,
candidate_order INTEGER NOT NULL,
PRIMARY KEY (definition_id, definition_version, step_id, candidate),
CONSTRAINT fk_step_candidate_step FOREIGN KEY (definition_id, definition_version, step_id)
REFERENCES ordo_approval_step (definition_id, definition_version, step_id)
);
INSERT INTO ordo_step_candidate_v7 (definition_id, definition_version, step_id, candidate, candidate_order)
SELECT definition_id, 1, step_id, candidate, candidate_order FROM ordo_step_candidate;
DROP TABLE ordo_step_candidate;
ALTER TABLE ordo_step_candidate_v7 RENAME TO ordo_step_candidate;
CREATE TABLE ordo_step_transition_v7 (
definition_id VARCHAR(64) NOT NULL,
definition_version INTEGER NOT NULL,
from_step_id VARCHAR(64) NOT NULL,
to_step_id VARCHAR(64),
condition_key VARCHAR(255),
priority INTEGER NOT NULL,
PRIMARY KEY (definition_id, definition_version, from_step_id, priority),
CONSTRAINT fk_transition_from FOREIGN KEY (definition_id, definition_version, from_step_id)
REFERENCES ordo_approval_step (definition_id, definition_version, step_id),
CONSTRAINT fk_transition_to FOREIGN KEY (definition_id, definition_version, to_step_id)
REFERENCES ordo_approval_step (definition_id, definition_version, step_id)
);
INSERT INTO ordo_step_transition_v7 (definition_id, definition_version, from_step_id, to_step_id, condition_key, priority)
SELECT definition_id, 1, from_step_id, to_step_id, condition_key, priority FROM ordo_step_transition;
DROP TABLE ordo_step_transition;
ALTER TABLE ordo_step_transition_v7 RENAME TO ordo_step_transition;
ALTER TABLE ordo_process_instance ADD COLUMN definition_version INTEGER DEFAULT 1 NOT NULL;
ALTER TABLE ordo_process_instance ADD CONSTRAINT fk_process_instance_definition
FOREIGN KEY (definition_id, definition_version) REFERENCES ordo_process_definition (id, version);
@@ -28,9 +28,9 @@ class JdbcActionExecutionRepositoryTest {
void setUp() {
JdbcConnectionProvider connectionProvider = new JdbcConnectionProvider(JdbcTestSupport.newDataSource());
repository = new JdbcActionExecutionRepository(connectionProvider);
new JdbcProcessDefinitionRepository(connectionProvider).insertIfAbsent(ProcessDefinition.linear("leave",
new JdbcProcessDefinitionRepository(connectionProvider).publish(ProcessDefinition.linear("leave",
"Leave request", List.of(ApprovalStep.single("manager", "Manager approval", "maria"))));
new JdbcProcessInstanceRepository(connectionProvider).insert(new ProcessInstance("inst-1", "leave", "alice",
new JdbcProcessInstanceRepository(connectionProvider).insert(new ProcessInstance("inst-1", "leave", 1, "alice",
ProcessStatus.RUNNING, T0, null, ProcessContext.empty()));
}
@@ -42,12 +42,12 @@ class JdbcApprovalTaskRepositoryTest {
/** Tasks reference their instance, which references its definition; both parent rows must exist. */
private void insertFixtureData() {
new JdbcProcessDefinitionRepository(connectionProvider).insertIfAbsent(ProcessDefinition.linear("leave",
new JdbcProcessDefinitionRepository(connectionProvider).publish(ProcessDefinition.linear("leave",
"Leave request", List.of(ApprovalStep.single("manager", "Manager approval", "maria"))));
JdbcProcessInstanceRepository instanceRepository = new JdbcProcessInstanceRepository(connectionProvider);
instanceRepository.insert(new ProcessInstance("inst-1", "leave", "alice", ProcessStatus.RUNNING,
instanceRepository.insert(new ProcessInstance("inst-1", "leave", 1, "alice", ProcessStatus.RUNNING,
CREATED_AT, null, ProcessContext.empty()));
instanceRepository.insert(new ProcessInstance("inst-2", "leave", "alice", ProcessStatus.RUNNING,
instanceRepository.insert(new ProcessInstance("inst-2", "leave", 1, "alice", ProcessStatus.RUNNING,
CREATED_AT, null, ProcessContext.empty()));
}
@@ -101,10 +101,10 @@ class JdbcApprovalTaskRepositoryTest {
@Test
void queryFiltersPaginatesAndOrdersNewestFirst() {
new JdbcProcessDefinitionRepository(connectionProvider).insertIfAbsent(ProcessDefinition.linear("expense",
new JdbcProcessDefinitionRepository(connectionProvider).publish(ProcessDefinition.linear("expense",
"Expense request", List.of(ApprovalStep.single("finance", "Finance approval", "frank"))));
JdbcProcessInstanceRepository instanceRepository = new JdbcProcessInstanceRepository(connectionProvider);
instanceRepository.insert(new ProcessInstance("inst-3", "expense", "alice", ProcessStatus.RUNNING,
instanceRepository.insert(new ProcessInstance("inst-3", "expense", 1, "alice", ProcessStatus.RUNNING,
CREATED_AT, null, ProcessContext.empty()));
ApprovalTask t1 = pendingTask("task-1", "inst-1", "manager", "maria", CREATED_AT);
@@ -19,7 +19,6 @@ import com.jetlumen.ordo.api.StepKind;
import com.jetlumen.ordo.api.StepDue;
import com.jetlumen.ordo.api.StepTransition;
import com.jetlumen.ordo.api.TaskStatus;
import com.jetlumen.ordo.api.exception.DefinitionInUseException;
import com.jetlumen.ordo.api.exception.InstanceAlreadyCompletedException;
import com.jetlumen.ordo.api.exception.UnauthorizedInstanceOperationException;
import com.jetlumen.ordo.api.exception.NoRouteFoundException;
@@ -49,7 +48,7 @@ class JdbcOrdoEngineIntegrationTest {
void setUp() {
connectionProvider = new JdbcConnectionProvider(JdbcTestSupport.newDataSource());
engine = newEngine(AssigneeResolver.direct());
engine.register(ProcessDefinition.linear("leave", "Leave request", List.of(
engine.publish(ProcessDefinition.linear("leave", "Leave request", List.of(
ApprovalStep.single("manager", "Manager approval", "maria"),
ApprovalStep.single("hr", "HR approval", "henry"))));
}
@@ -136,7 +135,7 @@ class JdbcOrdoEngineIntegrationTest {
new JdbcProcessHistoryRepository(connectionProvider),
new JdbcActionExecutionRepository(connectionProvider),
List.of());
startEngine.register(dueDefinition);
startEngine.publish(dueDefinition);
ProcessInstance instance = startEngine.start("leave-due", "alice");
assertEquals(0, startEngine.processDue(10));
@@ -165,7 +164,7 @@ class JdbcOrdoEngineIntegrationTest {
}
return candidate;
});
failingEngine.register(ProcessDefinition.linear("leave2", "Leave request", List.of(
failingEngine.publish(ProcessDefinition.linear("leave2", "Leave request", List.of(
ApprovalStep.single("manager", "Manager approval", "maria"),
ApprovalStep.single("hr", "HR approval", "henry"))));
@@ -192,7 +191,7 @@ class JdbcOrdoEngineIntegrationTest {
new JdbcProcessHistoryRepository(connectionProvider),
new JdbcActionExecutionRepository(connectionProvider),
List.of());
failingEngine.register(new ProcessDefinition("leave-noroute", "Leave request",
failingEngine.publish(new ProcessDefinition("leave-noroute", "Leave request",
List.of(ApprovalStep.single("manager", "Manager approval", "maria")),
List.of(StepTransition.endWhen("manager", "never", 0))));
@@ -223,19 +222,15 @@ class JdbcOrdoEngineIntegrationTest {
}
@Test
void replaceSwapsTheGraphWhenNoInstanceIsRunning() {
engine.replace(ProcessDefinition.linear("leave", "Leave request v2", List.of(
void publishCreatesANewVersionWhileRunningInstancesKeepTheOldGraph() {
ProcessInstance running = engine.start("leave", "alice");
engine.publish(ProcessDefinition.linear("leave", "Leave request v2", List.of(
ApprovalStep.single("director", "Director approval", "diana"))));
ProcessInstance instance = engine.start("leave", "alice");
assertEquals("diana", engine.findPendingTasksByInstanceId(instance.id()).get(0).assignee());
}
@Test
void replaceIsRejectedWhileAnInstanceIsRunning() {
engine.start("leave", "alice");
assertThrows(DefinitionInUseException.class, () -> engine.replace(ProcessDefinition.linear(
"leave", "Leave request v2", List.of(ApprovalStep.single("director", "Director approval", "diana")))));
assertEquals("maria", engine.findPendingTasksByInstanceId(running.id()).get(0).assignee());
ProcessInstance next = engine.start("leave", "bob");
assertEquals(2, next.definitionVersion());
assertEquals("diana", engine.findPendingTasksByInstanceId(next.id()).get(0).assignee());
}
@Test
@@ -304,7 +299,7 @@ class JdbcOrdoEngineIntegrationTest {
new JdbcProcessHistoryRepository(connectionProvider),
new JdbcActionExecutionRepository(connectionProvider),
List.of());
actionEngine.register(new ProcessDefinition("leave-action", "Leave request", List.of(
actionEngine.publish(new ProcessDefinition("leave-action", "Leave request", List.of(
ApprovalStep.single("manager", "Manager approval", "maria"),
ApprovalStep.action("notify", "Notify", "ok-mail"),
ApprovalStep.action("fail", "Fail", "boom")),
@@ -104,7 +104,7 @@ class JdbcPostgresIntegrationTest {
@Test
void engineCompletesASequentialApprovalProcessOverPostgres() {
OrdoEngine engine = newEngine(AssigneeResolver.direct());
engine.register(ProcessDefinition.linear("leave-pg", "Leave request", List.of(
engine.publish(ProcessDefinition.linear("leave-pg", "Leave request", List.of(
ApprovalStep.single("manager", "Manager approval", "maria"),
ApprovalStep.single("hr", "HR approval", "henry"))));
@@ -122,23 +122,25 @@ class JdbcPostgresIntegrationTest {
}
@Test
void rejectsDuplicateDefinitionIdsViaTheDatabaseUniqueConstraint() {
void publishIsIdempotentForTheSameGraphAndVersionsAChangedGraph() {
JdbcProcessDefinitionRepository repository = new JdbcProcessDefinitionRepository(connectionProvider);
ProcessDefinition definition = ProcessDefinition.linear("leave-dup-pg", "Leave request",
List.of(ApprovalStep.single("manager", "Manager approval", "maria")));
assertTrue(repository.insertIfAbsent(definition));
assertFalse(repository.insertIfAbsent(ProcessDefinition.linear("leave-dup-pg", "Second attempt",
List.of(ApprovalStep.single("manager", "Manager approval", "maria")))));
assertEquals("Leave request", repository.findById("leave-dup-pg").orElseThrow().name());
assertEquals(1, repository.publish(definition).version());
assertEquals(1, repository.publish(definition).version());
assertEquals(2, repository.publish(ProcessDefinition.linear("leave-dup-pg", "Second attempt",
List.of(ApprovalStep.single("manager", "Manager approval", "maria")))).version());
assertEquals("Second attempt", repository.findLatest("leave-dup-pg").orElseThrow().name());
assertEquals("Leave request", repository.find("leave-dup-pg", 1).orElseThrow().name());
}
@Test
void rejectsDuplicateInstanceIdsViaTheDatabaseUniqueConstraint() {
new JdbcProcessDefinitionRepository(connectionProvider).insertIfAbsent(ProcessDefinition.linear(
new JdbcProcessDefinitionRepository(connectionProvider).publish(ProcessDefinition.linear(
"leave-dup-inst-pg", "Leave request", List.of(ApprovalStep.single("manager", "Manager approval", "maria"))));
JdbcProcessInstanceRepository repository = new JdbcProcessInstanceRepository(connectionProvider);
ProcessInstance instance = new ProcessInstance("inst-dup-pg", "leave-dup-inst-pg", "alice",
ProcessInstance instance = new ProcessInstance("inst-dup-pg", "leave-dup-inst-pg", 1, "alice",
ProcessStatus.RUNNING, NOW, null, ProcessContext.empty());
repository.insert(instance);
@@ -147,18 +149,18 @@ class JdbcPostgresIntegrationTest {
@Test
void roundsTimestampsToMicrosecondPrecision() {
new JdbcProcessDefinitionRepository(connectionProvider).insertIfAbsent(ProcessDefinition.linear(
new JdbcProcessDefinitionRepository(connectionProvider).publish(ProcessDefinition.linear(
"leave-time-pg", "Leave request", List.of(ApprovalStep.single("manager", "Manager approval", "maria"))));
JdbcProcessInstanceRepository repository = new JdbcProcessInstanceRepository(connectionProvider);
Instant microAligned = Instant.parse("2026-01-15T09:00:00.123456Z");
repository.insert(new ProcessInstance("inst-time-1", "leave-time-pg", "alice",
repository.insert(new ProcessInstance("inst-time-1", "leave-time-pg", 1, "alice",
ProcessStatus.RUNNING, microAligned, null, ProcessContext.empty()));
assertEquals(microAligned, repository.findById("inst-time-1").orElseThrow().startedAt());
// PostgreSQL's TIMESTAMP stores microseconds and rounds the fractional seconds
Instant withNanos = Instant.parse("2026-01-15T09:00:00.123456789Z");
repository.insert(new ProcessInstance("inst-time-2", "leave-time-pg", "alice",
repository.insert(new ProcessInstance("inst-time-2", "leave-time-pg", 1, "alice",
ProcessStatus.RUNNING, withNanos, null, ProcessContext.empty()));
assertEquals(Instant.parse("2026-01-15T09:00:00.123457Z"),
repository.findById("inst-time-2").orElseThrow().startedAt());
@@ -166,10 +168,10 @@ class JdbcPostgresIntegrationTest {
@Test
void onlyOneOfTwoConcurrentCompletionsWinsOnPostgres() throws Exception {
new JdbcProcessDefinitionRepository(connectionProvider).insertIfAbsent(ProcessDefinition.linear(
new JdbcProcessDefinitionRepository(connectionProvider).publish(ProcessDefinition.linear(
"leave-race-pg", "Leave request", List.of(ApprovalStep.single("manager", "Manager approval", "maria"))));
JdbcProcessInstanceRepository instanceRepository = new JdbcProcessInstanceRepository(connectionProvider);
instanceRepository.insert(new ProcessInstance("inst-race-pg", "leave-race-pg", "alice",
instanceRepository.insert(new ProcessInstance("inst-race-pg", "leave-race-pg", 1, "alice",
ProcessStatus.RUNNING, NOW, null, ProcessContext.empty()));
JdbcApprovalTaskRepository taskRepository = new JdbcApprovalTaskRepository(connectionProvider);
@@ -202,7 +204,7 @@ class JdbcPostgresIntegrationTest {
}
return candidate;
});
failingEngine.register(ProcessDefinition.linear("leave-rollback-pg", "Leave request", List.of(
failingEngine.publish(ProcessDefinition.linear("leave-rollback-pg", "Leave request", List.of(
ApprovalStep.single("manager", "Manager approval", "maria"),
ApprovalStep.single("hr", "HR approval", "henry"))));
@@ -32,35 +32,39 @@ class JdbcProcessDefinitionRepositoryTest {
ApprovalStep.single("manager", "Manager approval", "maria"),
ApprovalStep.single("hr", "HR approval", "henry")));
assertTrue(repository.insertIfAbsent(definition));
assertEquals(definition, repository.findById("leave").orElseThrow());
ProcessDefinition published = repository.publish(definition);
assertEquals(1, published.version());
assertEquals(definition.withVersion(1), repository.findLatest("leave").orElseThrow());
}
@Test
void rejectsAnExistingDefinitionId() {
assertTrue(repository.insertIfAbsent(definition("leave", "Leave request v1")));
assertFalse(repository.insertIfAbsent(definition("leave", "Leave request v2")));
assertEquals("Leave request v1", repository.findById("leave").orElseThrow().name());
void publishIsIdempotentWhenTheGraphIsUnchanged() {
ProcessDefinition first = repository.publish(definition("leave", "Leave request v1"));
ProcessDefinition second = repository.publish(definition("leave", "Leave request v1"));
assertEquals(first, second);
assertEquals(1, repository.findVersions("leave", new PageRequest(0, 10)).totalElements());
}
@Test
void upsertReplacesNameStepsCandidatesAndTransitions() {
repository.insertIfAbsent(ProcessDefinition.linear("leave", "Leave request v1", List.of(
void publishInsertsANewVersionWithoutRewritingTheOldGraph() {
repository.publish(ProcessDefinition.linear("leave", "Leave request v1", List.of(
ApprovalStep.single("manager", "Manager approval", "maria"),
ApprovalStep.single("hr", "HR approval", "henry"))));
ProcessDefinition replacement = new ProcessDefinition("leave", "Leave request v2", List.of(
ApprovalStep.single("director", "Director approval", "diana")),
List.of(StepTransition.end("director")));
repository.upsert(replacement);
ProcessDefinition published = repository.publish(replacement);
assertEquals(replacement, repository.findById("leave").orElseThrow());
assertEquals(replacement.withVersion(2), published);
assertEquals(published, repository.findLatest("leave").orElseThrow());
assertEquals("Leave request v1", repository.find("leave", 1).orElseThrow().name());
}
@Test
void returnsEmptyForAnUnknownDefinition() {
assertTrue(repository.findById("missing").isEmpty());
assertTrue(repository.findLatest("missing").isEmpty());
assertTrue(repository.find("missing", 1).isEmpty());
}
@Test
@@ -73,8 +77,8 @@ class JdbcProcessDefinitionRepositoryTest {
new StepTransition("manager", null, null, 1),
StepTransition.end("director")));
assertTrue(repository.insertIfAbsent(definition));
assertEquals(definition, repository.findById("expense").orElseThrow());
assertEquals(definition.withVersion(1), repository.publish(definition));
assertEquals(definition.withVersion(1), repository.findLatest("expense").orElseThrow());
}
@Test
@@ -86,10 +90,10 @@ class JdbcProcessDefinitionRepositoryTest {
StepTransition.always("manager", "mail"),
StepTransition.end("mail")));
assertTrue(repository.insertIfAbsent(definition));
assertEquals(definition, repository.findById("notify").orElseThrow());
assertEquals(StepKind.ACTION, repository.findById("notify").orElseThrow().steps().get(1).kind());
assertEquals("leave-approved-mail", repository.findById("notify").orElseThrow().steps().get(1).actionKey());
repository.publish(definition);
assertEquals(definition.withVersion(1), repository.findLatest("notify").orElseThrow());
assertEquals(StepKind.ACTION, repository.findLatest("notify").orElseThrow().steps().get(1).kind());
assertEquals("leave-approved-mail", repository.findLatest("notify").orElseThrow().steps().get(1).actionKey());
}
private static ProcessDefinition definition(String id, String name) {
@@ -97,28 +101,41 @@ class JdbcProcessDefinitionRepositoryTest {
}
@Test
void findAllPaginatesDefinitionsOrderedById() {
repository.insertIfAbsent(definition("c-def", "C"));
repository.insertIfAbsent(definition("a-def", "A"));
repository.insertIfAbsent(definition("b-def", "B"));
void findAllPaginatesLatestDefinitionsOrderedById() {
repository.publish(definition("c-def", "C"));
repository.publish(definition("a-def", "A"));
repository.publish(definition("b-def", "B"));
repository.publish(definition("a-def", "A2"));
Page<ProcessDefinition> pageOne = repository.findAll(new PageRequest(0, 2));
assertEquals(3, pageOne.totalElements());
assertEquals(2, pageOne.totalPages());
assertTrue(pageOne.hasNext());
assertEquals(List.of("a-def", "b-def"), pageOne.content().stream().map(ProcessDefinition::id).toList());
assertEquals(2, pageOne.content().get(0).version());
Page<ProcessDefinition> pageTwo = repository.findAll(new PageRequest(1, 2));
assertEquals(List.of("c-def"), pageTwo.content().stream().map(ProcessDefinition::id).toList());
assertFalse(pageTwo.hasNext());
}
@Test
void findVersionsPaginatesNewestFirst() {
repository.publish(definition("leave", "v1"));
repository.publish(definition("leave", "v2"));
repository.publish(definition("leave", "v3"));
Page<ProcessDefinition> page = repository.findVersions("leave", new PageRequest(0, 2));
assertEquals(3, page.totalElements());
assertEquals(List.of(3, 2), page.content().stream().map(ProcessDefinition::version).toList());
}
@Test
void insertsAndReadsBackStepDue() {
ProcessDefinition definition = ProcessDefinition.linear("leave-due", "Leave request", List.of(
new ApprovalStep("manager", "Manager approval", List.of("maria"), ApprovalPolicy.ANY,
StepKind.APPROVAL, null, StepDue.reassign(java.time.Duration.parse("PT48H"), "director"))));
assertTrue(repository.insertIfAbsent(definition));
assertEquals(definition, repository.findById("leave-due").orElseThrow());
repository.publish(definition);
assertEquals(definition.withVersion(1), repository.findLatest("leave-due").orElseThrow());
}
}
@@ -27,11 +27,11 @@ class JdbcProcessHistoryRepositoryTest {
void setUp() {
JdbcConnectionProvider connectionProvider = new JdbcConnectionProvider(JdbcTestSupport.newDataSource());
repository = new JdbcProcessHistoryRepository(connectionProvider);
new JdbcProcessDefinitionRepository(connectionProvider).insertIfAbsent(ProcessDefinition.linear("leave",
new JdbcProcessDefinitionRepository(connectionProvider).publish(ProcessDefinition.linear("leave",
"Leave request", List.of(ApprovalStep.single("manager", "Manager approval", "maria"))));
new JdbcProcessInstanceRepository(connectionProvider).insert(new ProcessInstance("inst-1", "leave", "alice",
new JdbcProcessInstanceRepository(connectionProvider).insert(new ProcessInstance("inst-1", "leave", 1, "alice",
ProcessStatus.RUNNING, T0, null, ProcessContext.empty()));
new JdbcProcessInstanceRepository(connectionProvider).insert(new ProcessInstance("inst-2", "leave", "bob",
new JdbcProcessInstanceRepository(connectionProvider).insert(new ProcessInstance("inst-2", "leave", 1, "bob",
ProcessStatus.RUNNING, T0, null, ProcessContext.empty()));
}
@@ -31,7 +31,7 @@ class JdbcProcessInstanceRepositoryTest {
connectionProvider = new JdbcConnectionProvider(JdbcTestSupport.newDataSource());
repository = new JdbcProcessInstanceRepository(connectionProvider);
// instances reference their definition, so the parent row must exist
new JdbcProcessDefinitionRepository(connectionProvider).insertIfAbsent(ProcessDefinition.linear("leave",
new JdbcProcessDefinitionRepository(connectionProvider).publish(ProcessDefinition.linear("leave",
"Leave request", List.of(ApprovalStep.single("manager", "Manager approval", "maria"))));
}
@@ -43,7 +43,7 @@ class JdbcProcessInstanceRepositoryTest {
"urgent", true,
"candidates", List.of("maria", "henry"),
"meta", Map.of("priority", "high", "retries", 2)));
ProcessInstance instance = new ProcessInstance("inst-1", "leave", "alice", ProcessStatus.RUNNING,
ProcessInstance instance = new ProcessInstance("inst-1", "leave", 1, "alice", ProcessStatus.RUNNING,
STARTED_AT, null, context);
repository.insert(instance);
@@ -53,11 +53,11 @@ class JdbcProcessInstanceRepositoryTest {
@Test
void updatesStatusAndFinishedAt() {
repository.insert(new ProcessInstance("inst-1", "leave", "alice", ProcessStatus.RUNNING,
repository.insert(new ProcessInstance("inst-1", "leave", 1, "alice", ProcessStatus.RUNNING,
STARTED_AT, null, ProcessContext.empty()));
Instant finishedAt = STARTED_AT.plusSeconds(300);
repository.update(new ProcessInstance("inst-1", "leave", "alice", ProcessStatus.APPROVED,
repository.update(new ProcessInstance("inst-1", "leave", 1, "alice", ProcessStatus.APPROVED,
STARTED_AT, finishedAt, ProcessContext.empty()));
ProcessInstance updated = repository.findById("inst-1").orElseThrow();
@@ -74,51 +74,51 @@ class JdbcProcessInstanceRepositoryTest {
void existsRunningOnlyCountsRunningInstancesOfThatDefinition() {
assertFalse(repository.existsRunning("leave"));
repository.insert(new ProcessInstance("inst-1", "leave", "alice", ProcessStatus.RUNNING,
repository.insert(new ProcessInstance("inst-1", "leave", 1, "alice", ProcessStatus.RUNNING,
STARTED_AT, null, ProcessContext.empty()));
assertTrue(repository.existsRunning("leave"));
assertFalse(repository.existsRunning("other"));
repository.update(new ProcessInstance("inst-1", "leave", "alice", ProcessStatus.APPROVED,
repository.update(new ProcessInstance("inst-1", "leave", 1, "alice", ProcessStatus.APPROVED,
STARTED_AT, STARTED_AT.plusSeconds(60), ProcessContext.empty()));
assertFalse(repository.existsRunning("leave"));
}
@Test
void completeIfRunningOnlyUpdatesARunningInstance() {
ProcessInstance running = new ProcessInstance("inst-1", "leave", "alice", ProcessStatus.RUNNING,
ProcessInstance running = new ProcessInstance("inst-1", "leave", 1, "alice", ProcessStatus.RUNNING,
STARTED_AT, null, ProcessContext.empty());
repository.insert(running);
Instant finishedAt = STARTED_AT.plusSeconds(30);
ProcessInstance withdrawn = new ProcessInstance("inst-1", "leave", "alice", ProcessStatus.WITHDRAWN,
ProcessInstance withdrawn = new ProcessInstance("inst-1", "leave", 1, "alice", ProcessStatus.WITHDRAWN,
STARTED_AT, finishedAt, ProcessContext.empty());
assertTrue(repository.completeIfRunning(withdrawn));
assertEquals(ProcessStatus.WITHDRAWN, repository.findById("inst-1").orElseThrow().status());
assertEquals(finishedAt, repository.findById("inst-1").orElseThrow().finishedAt());
assertFalse(repository.completeIfRunning(new ProcessInstance("inst-1", "leave", "alice",
assertFalse(repository.completeIfRunning(new ProcessInstance("inst-1", "leave", 1, "alice",
ProcessStatus.APPROVED, STARTED_AT, finishedAt, ProcessContext.empty())));
assertFalse(repository.completeIfRunning(new ProcessInstance("missing", "leave", "alice",
assertFalse(repository.completeIfRunning(new ProcessInstance("missing", "leave", 1, "alice",
ProcessStatus.WITHDRAWN, STARTED_AT, finishedAt, ProcessContext.empty())));
}
@Test
void updateOfAnUnknownInstanceFails() {
assertThrows(IllegalStateException.class, () -> repository.update(new ProcessInstance("missing", "leave",
"alice", ProcessStatus.APPROVED, STARTED_AT, STARTED_AT, ProcessContext.empty())));
1, "alice", ProcessStatus.APPROVED, STARTED_AT, STARTED_AT, ProcessContext.empty())));
}
@Test
void queryFiltersPaginatesAndOrdersNewestFirst() {
new JdbcProcessDefinitionRepository(connectionProvider).insertIfAbsent(ProcessDefinition.linear("expense",
new JdbcProcessDefinitionRepository(connectionProvider).publish(ProcessDefinition.linear("expense",
"Expense request", List.of(ApprovalStep.single("finance", "Finance approval", "frank"))));
ProcessInstance i1 = new ProcessInstance("inst-1", "leave", "alice", ProcessStatus.RUNNING,
ProcessInstance i1 = new ProcessInstance("inst-1", "leave", 1, "alice", ProcessStatus.RUNNING,
STARTED_AT, null, ProcessContext.empty());
ProcessInstance i2 = new ProcessInstance("inst-2", "leave", "bob", ProcessStatus.RUNNING,
ProcessInstance i2 = new ProcessInstance("inst-2", "leave", 1, "bob", ProcessStatus.RUNNING,
STARTED_AT.plusSeconds(5), null, ProcessContext.empty());
ProcessInstance i3 = new ProcessInstance("inst-3", "expense", "alice", ProcessStatus.RUNNING,
ProcessInstance i3 = new ProcessInstance("inst-3", "expense", 1, "alice", ProcessStatus.RUNNING,
STARTED_AT.plusSeconds(10), null, ProcessContext.empty());
repository.insert(i1);
repository.insert(i2);
@@ -19,7 +19,8 @@ final class JdbcTestSupport {
"/db/migration/V3__add_step_transitions.sql",
"/db/migration/V4__add_step_kind_and_action_key.sql",
"/db/migration/V5__add_process_event_and_action_execution.sql",
"/db/migration/V6__add_step_due.sql"
"/db/migration/V6__add_step_due.sql",
"/db/migration/V7__definition_versions.sql"
};
private static final String[] SCHEMA_SQL = loadSchemas();
@@ -60,7 +60,7 @@ class JdbcTransactionExecutorTest {
private void insertDefinition(String id, String name) {
Connection connection = connectionProvider.getConnection();
try (PreparedStatement insert = connection.prepareStatement(
"INSERT INTO ordo_process_definition (id, name) VALUES (?, ?)")) {
"INSERT INTO ordo_process_definition (id, version, name, created_at) VALUES (?, 1, ?, CURRENT_TIMESTAMP)")) {
insert.setString(1, id);
insert.setString(2, name);
insert.executeUpdate();