From 1643582582141a8395e3d94b85eaf0b0632a82d5 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 10 Aug 2026 12:41:42 -0400 Subject: [PATCH 1/5] Implement SQL Repos --- .../api/match/db/models/MatchCycle.java | 15 +- .../db/repos/MatchCycleFilterCriteria.java | 4 +- .../api/match/db/repos/MatchCycleRepo.java | 8 +- .../api/match/db/repos/MatchCycleSqlRepo.java | 156 ++++++++++++++++ .../api/match/db/repos/MatchRepo.java | 4 - .../api/match/db/repos/MatchSqlRepo.java | 169 ++++++++++++++++++ 6 files changed, 340 insertions(+), 16 deletions(-) create mode 100644 src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java create mode 100644 src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchSqlRepo.java diff --git a/src/main/java/org/patinanetwork/patchats/api/match/db/models/MatchCycle.java b/src/main/java/org/patinanetwork/patchats/api/match/db/models/MatchCycle.java index 54c75d7e..60e7fe06 100644 --- a/src/main/java/org/patinanetwork/patchats/api/match/db/models/MatchCycle.java +++ b/src/main/java/org/patinanetwork/patchats/api/match/db/models/MatchCycle.java @@ -8,16 +8,19 @@ import lombok.ToString; @Getter -@Builder @ToString +@Builder @EqualsAndHashCode(of = "id") public class MatchCycle { + private Integer id; - private Integer id; + @Setter + private String period; - @Setter - private String period; + @Setter + private Instant runAt; - @Setter - private Instant runAt; + @Setter + @Builder.Default + private boolean isDraft = true; } diff --git a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleFilterCriteria.java b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleFilterCriteria.java index 6654f1ac..dcc24387 100644 --- a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleFilterCriteria.java +++ b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleFilterCriteria.java @@ -4,9 +4,9 @@ import java.util.Optional; public record MatchCycleFilterCriteria( - Optional period, Optional startTime, Optional endTime) { + Optional period, Optional startTime, Optional endTime, Optional isDraft) { public static MatchCycleFilterCriteria empty() { - return new MatchCycleFilterCriteria(Optional.empty(), Optional.empty(), Optional.empty()); + return new MatchCycleFilterCriteria(Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()); } } diff --git a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleRepo.java b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleRepo.java index c870e14d..84959aef 100644 --- a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleRepo.java +++ b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleRepo.java @@ -15,8 +15,7 @@ public interface MatchCycleRepo { * Optional fields: *
    *
  • period - *
  • totalMembers - *
  • totalMatched + *
  • isDraft *
* The id field will be auto-generated by the database. */ @@ -28,14 +27,15 @@ public interface MatchCycleRepo { *
    *
  • period *
  • runAt - *
  • totalMembers - *
  • totalMatched + *
  • isDraft *
*/ Optional updateMatchCycle(MatchCycle matchCycle); Optional getMatchCycleById(Integer id); + Optional setMatchCycleDraft(Integer id, boolean isDraft); + Optional deleteMatchCycleById(Integer id); List filterMatchCycles(MatchCycleFilterCriteria criteria); diff --git a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java new file mode 100644 index 00000000..14b64afe --- /dev/null +++ b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java @@ -0,0 +1,156 @@ +package org.patinanetwork.patchats.api.match.db.repos; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.patinanetwork.patchats.api.match.db.models.MatchCycle; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.simple.JdbcClient; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class MatchCycleSqlRepo implements MatchCycleRepo { + private final JdbcClient jdbc; + + private static final String TOTAL_MATCHED_SQL = "(SELECT count(*) FROM matches m WHERE m.cycle_id = match_cycles.id) AS total_matched"; + + private MatchCycle parseResultSetToMatchCycle(final ResultSet rs) throws SQLException { + return MatchCycle.builder() + .id(rs.getInt("id")) + .period(rs.getString("period")) + .runAt(rs.getObject("run_at", Instant.class)) + .isDraft(rs.getBoolean("is_draft")) + .build(); + } + + @Override + public MatchCycle createMatchCycle(MatchCycle matchCycle) { + String sql = """ + INSERT INTO "match_cycles" ( + "period", + "run_at", + "is_draft" + ) + VALUES( + :period, + :run_at, + :is_draft + ) + RETURNING + *, + %s + """.formatted(TOTAL_MATCHED_SQL); + + return jdbc.sql(sql) + .param("period", matchCycle.getPeriod()) + .param("run_at", matchCycle.getRunAt()) + .param("is_draft", matchCycle.isDraft()) + .query((rs, rowNum) -> parseResultSetToMatchCycle(rs)) + .single(); + } + + @Override + public Optional updateMatchCycle(MatchCycle matchCycle) { + String sql = """ + UPDATE "match_cycles" SET + "period" = :period, + "run_at" = :run_at, + "is_draft" = :is_draft + WHERE "id" = :id + RETURNING + *, + %s + """.formatted(TOTAL_MATCHED_SQL); + + return jdbc.sql(sql) + .param("id", matchCycle.getId()) + .param("period", matchCycle.getPeriod()) + .param("run_at", matchCycle.getRunAt()) + .param("is_draft", matchCycle.isDraft()) + .query((rs, rowNum) -> parseResultSetToMatchCycle(rs)) + .optional(); + } + + @Override + public Optional getMatchCycleById(Integer id) { + String sql = """ + SELECT + *, + %s + FROM match_cycles + WHERE id = :id + """.formatted(TOTAL_MATCHED_SQL); + return jdbc.sql(sql) + .param("id", id) + .query((rs, rowNum) -> parseResultSetToMatchCycle(rs)) + .optional(); + } + + @Override + public Optional setMatchCycleDraft(Integer id, boolean isDraft) { + String sql = """ + UPDATE "match_cycles" SET + "is_draft" = :is_draft + WHERE "id" = :id + RETURNING + *, + %s + """.formatted(TOTAL_MATCHED_SQL); + + return jdbc.sql(sql) + .param("id", id) + .param("is_draft", isDraft) + .query((rs, rowNum) -> parseResultSetToMatchCycle(rs)) + .optional(); + } + + @Override + public Optional deleteMatchCycleById(Integer id) { + String sql = """ + DELETE FROM match_cycles + WHERE id = :id + RETURNING + *, + 0 AS total_matched + """; + return jdbc.sql(sql) + .param("id", id) + .query((rs, rowNum) -> parseResultSetToMatchCycle(rs)) + .optional(); + } + + @Override + public List filterMatchCycles(MatchCycleFilterCriteria criteria) { + StringBuilder sql = new StringBuilder("SELECT *, " + TOTAL_MATCHED_SQL + " FROM match_cycles WHERE 1=1"); + MapSqlParameterSource params = new MapSqlParameterSource(); + + criteria.period().ifPresent(period -> { + sql.append(" AND period = :period"); + params.addValue("period", period); + }); + + criteria.startTime().ifPresent(start -> { + sql.append(" AND run_at >= :start_time"); + params.addValue("start_time", start); + }); + + criteria.endTime().ifPresent(end -> { + sql.append(" AND run_at <= :end_time"); + params.addValue("end_time", end); + }); + + criteria.isDraft().ifPresent(isDraft -> { + sql.append(" AND is_draft = :is_draft"); + params.addValue("is_draft", isDraft); + }); + + return jdbc.sql(sql.toString()) + .paramSource(params) + .query((rs, rowNum) -> parseResultSetToMatchCycle(rs)) + .list(); + } +} diff --git a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchRepo.java b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchRepo.java index 8de182e7..0d31f673 100644 --- a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchRepo.java +++ b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchRepo.java @@ -27,8 +27,6 @@ public interface MatchRepo { *
  • matchCycleId *
  • matchScore *
  • status - *
  • feedbackA - *
  • feedbackB * */ Optional updateMatch(Match match); @@ -39,7 +37,5 @@ public interface MatchRepo { Optional deleteMatchById(UUID id); - Optional recordFeedback(UUID id, UUID memberId, String feedback); - List filterMatches(MatchFilterCriteria criteria); } diff --git a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchSqlRepo.java b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchSqlRepo.java new file mode 100644 index 00000000..fa90dfdd --- /dev/null +++ b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchSqlRepo.java @@ -0,0 +1,169 @@ +package org.patinanetwork.patchats.api.match.db.repos; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.patinanetwork.patchats.api.match.db.models.Match; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.simple.JdbcClient; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class MatchSqlRepo implements MatchRepo { + private final JdbcClient jdbc; + + private Match parseResultSetToMatch(final ResultSet rs) throws SQLException { + return Match.builder() + .id(UUID.fromString(rs.getString("id"))) + .memberAId(UUID.fromString(rs.getString("member_a_id"))) + .memberBId(UUID.fromString(rs.getString("member_b_id"))) + .matchCycleId(rs.getInt("cycle_id")) + .matchScore(rs.getObject("match_score", Double.class)) + .status(rs.getString("status")) + .createdAt(rs.getObject("created_at", Instant.class)) + .build(); + } + + @Override + public Match createMatch(Match match) { + String sql = """ + INSERT INTO "matches" ( + "id", + "member_a_id", + "member_b_id", + "cycle_id", + "match_score", + "status" + ) + VALUES( + :id, + :member_a_id, + :member_b_id, + :cycle_id, + :match_score, + :status + ) + RETURNING + * + """; + return jdbc.sql(sql) + .param("id", match.getId()) + .param("member_a_id", match.getMemberAId()) + .param("member_b_id", match.getMemberBId()) + .param("cycle_id", match.getMatchCycleId()) + .param("match_score", match.getMatchScore()) + .param("status", match.getStatus()) + .query((rs, rowNum) -> parseResultSetToMatch(rs)) + .single(); + } + + @Override + public Optional updateMatch(Match match) { + String sql = """ + UPDATE "matches" SET + "member_a_id" = :member_a_id, + "member_b_id" = :member_b_id, + "cycle_id" = :cycle_id, + "match_score" = :match_score, + "status" = :status + WHERE "id" = :id + RETURNING * + """; + return jdbc.sql(sql) + .param("id", match.getId()) + .param("member_a_id", match.getMemberAId()) + .param("member_b_id", match.getMemberBId()) + .param("cycle_id", match.getMatchCycleId()) + .param("match_score", match.getMatchScore()) + .param("status", match.getStatus()) + .query((rs, rowNum) -> parseResultSetToMatch(rs)) + .optional(); + } + + @Override + public Optional getMatchById(UUID id) { + String sql = "SELECT * FROM matches WHERE id = :id"; + return jdbc.sql(sql) + .param("id", id) + .query((rs, rowNum) -> parseResultSetToMatch(rs)) + .optional(); + } + + @Override + public Optional setMatchStatus(UUID id, String status) { + String sql = """ + UPDATE "matches" SET "status" = :status + WHERE "id" = :id + RETURNING * + """; + return jdbc.sql(sql) + .param("id", id) + .param("status", status) + .query((rs, rowNum) -> parseResultSetToMatch(rs)) + .optional(); + } + + @Override + public Optional deleteMatchById(UUID id) { + String sql = "DELETE FROM matches WHERE id = :id RETURNING *"; + return jdbc.sql(sql) + .param("id", id) + .query((rs, rowNum) -> parseResultSetToMatch(rs)) + .optional(); + } + + @Override + public List filterMatches(MatchFilterCriteria criteria) { + StringBuilder sql = new StringBuilder("SELECT * FROM matches WHERE 1=1"); + MapSqlParameterSource params = new MapSqlParameterSource(); + + criteria.status().ifPresent(status -> { + sql.append(" AND status = :status"); + params.addValue("status", status); + }); + + criteria.memberId().ifPresent(memberId -> { + sql.append(" AND (member_a_id = :member_id OR member_b_id = :member_id)"); + params.addValue("member_id", memberId); + }); + + criteria.matchCycleId().ifPresent(cycleId -> { + sql.append(" AND cycle_id = :cycle_id"); + params.addValue("cycle_id", cycleId); + }); + + criteria.startTime().ifPresent(start -> { + sql.append(" AND created_at >= :start_time"); + params.addValue("start_time", start); + }); + + criteria.endTime().ifPresent(end -> { + sql.append(" AND created_at <= :end_time"); + params.addValue("end_time", end); + }); + + criteria.period().ifPresent(period -> { + sql.append(" AND cycle_id IN (SELECT id FROM match_cycles WHERE period = :period)"); + params.addValue("period", period); + }); + + criteria.memberIndustry().ifPresent(memberIndustry -> { + sql.append(" AND ("); + sql.append("member_a_id IN (SELECT id FROM members WHERE industry_pref = :member_industry)"); + sql.append(" OR "); + sql.append("member_b_id IN (SELECT id FROM members WHERE industry_pref = :member_industry)"); + sql.append(")"); + params.addValue("member_industry", memberIndustry); + }); + + return jdbc.sql(sql.toString()) + .paramSource(params) + .query((rs, rowNum) -> parseResultSetToMatch(rs)) + .list(); + } +} From 556aae69057461a3787a916543df0b0331bb6b33 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 17 Aug 2026 15:07:24 -0400 Subject: [PATCH 2/5] Keep total_members on MatchCycle and store it on create --- .../patchats/api/match/db/repos/MatchCycleSqlRepo.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java index 14b64afe..04815e89 100644 --- a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java +++ b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java @@ -33,12 +33,14 @@ public MatchCycle createMatchCycle(MatchCycle matchCycle) { INSERT INTO "match_cycles" ( "period", "run_at", - "is_draft" + "is_draft", + "total_members" ) VALUES( :period, :run_at, - :is_draft + :is_draft, + :total_members ) RETURNING *, From 2513b3b2b123c461d3709131c78cf2bf86b69e69 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 24 Aug 2026 13:22:57 -0400 Subject: [PATCH 3/5] Revert "Keep total_members on MatchCycle and store it on create" This reverts commit fd90b93e005252cb8df6c460f7d0594a1c40cae7. --- .../patchats/api/match/db/repos/MatchCycleSqlRepo.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java index 04815e89..14b64afe 100644 --- a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java +++ b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java @@ -33,14 +33,12 @@ public MatchCycle createMatchCycle(MatchCycle matchCycle) { INSERT INTO "match_cycles" ( "period", "run_at", - "is_draft", - "total_members" + "is_draft" ) VALUES( :period, :run_at, - :is_draft, - :total_members + :is_draft ) RETURNING *, From e5eab5b4e5fef0de316306aac49c78816e32368e Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 24 Aug 2026 13:23:17 -0400 Subject: [PATCH 4/5] Run spotless on match repos --- .../api/match/db/models/MatchCycle.java | 16 +- .../api/match/db/repos/MatchCycleSqlRepo.java | 220 +++++++++--------- .../api/match/db/repos/MatchRepo.java | 2 + .../api/match/db/repos/MatchSqlRepo.java | 19 +- 4 files changed, 131 insertions(+), 126 deletions(-) diff --git a/src/main/java/org/patinanetwork/patchats/api/match/db/models/MatchCycle.java b/src/main/java/org/patinanetwork/patchats/api/match/db/models/MatchCycle.java index 60e7fe06..0182ff5a 100644 --- a/src/main/java/org/patinanetwork/patchats/api/match/db/models/MatchCycle.java +++ b/src/main/java/org/patinanetwork/patchats/api/match/db/models/MatchCycle.java @@ -12,15 +12,15 @@ @Builder @EqualsAndHashCode(of = "id") public class MatchCycle { - private Integer id; + private Integer id; - @Setter - private String period; + @Setter + private String period; - @Setter - private Instant runAt; + @Setter + private Instant runAt; - @Setter - @Builder.Default - private boolean isDraft = true; + @Setter + @Builder.Default + private boolean isDraft = true; } diff --git a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java index 14b64afe..e3250be7 100644 --- a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java +++ b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java @@ -14,22 +14,20 @@ @Repository @RequiredArgsConstructor public class MatchCycleSqlRepo implements MatchCycleRepo { - private final JdbcClient jdbc; - - private static final String TOTAL_MATCHED_SQL = "(SELECT count(*) FROM matches m WHERE m.cycle_id = match_cycles.id) AS total_matched"; - - private MatchCycle parseResultSetToMatchCycle(final ResultSet rs) throws SQLException { - return MatchCycle.builder() - .id(rs.getInt("id")) - .period(rs.getString("period")) - .runAt(rs.getObject("run_at", Instant.class)) - .isDraft(rs.getBoolean("is_draft")) - .build(); - } - - @Override - public MatchCycle createMatchCycle(MatchCycle matchCycle) { - String sql = """ + private final JdbcClient jdbc; + + private MatchCycle parseResultSetToMatchCycle(final ResultSet rs) throws SQLException { + return MatchCycle.builder() + .id(rs.getInt("id")) + .period(rs.getString("period")) + .runAt(rs.getObject("run_at", Instant.class)) + .isDraft(rs.getBoolean("is_draft")) + .build(); + } + + @Override + public MatchCycle createMatchCycle(MatchCycle matchCycle) { + String sql = """ INSERT INTO "match_cycles" ( "period", "run_at", @@ -40,117 +38,107 @@ public MatchCycle createMatchCycle(MatchCycle matchCycle) { :run_at, :is_draft ) - RETURNING - *, - %s - """.formatted(TOTAL_MATCHED_SQL); - - return jdbc.sql(sql) - .param("period", matchCycle.getPeriod()) - .param("run_at", matchCycle.getRunAt()) - .param("is_draft", matchCycle.isDraft()) - .query((rs, rowNum) -> parseResultSetToMatchCycle(rs)) - .single(); - } - - @Override - public Optional updateMatchCycle(MatchCycle matchCycle) { - String sql = """ + RETURNING * + """; + + return jdbc.sql(sql) + .param("period", matchCycle.getPeriod()) + .param("run_at", matchCycle.getRunAt()) + .param("is_draft", matchCycle.isDraft()) + .query((rs, rowNum) -> parseResultSetToMatchCycle(rs)) + .single(); + } + + @Override + public Optional updateMatchCycle(MatchCycle matchCycle) { + String sql = """ UPDATE "match_cycles" SET "period" = :period, "run_at" = :run_at, "is_draft" = :is_draft WHERE "id" = :id - RETURNING - *, - %s - """.formatted(TOTAL_MATCHED_SQL); - - return jdbc.sql(sql) - .param("id", matchCycle.getId()) - .param("period", matchCycle.getPeriod()) - .param("run_at", matchCycle.getRunAt()) - .param("is_draft", matchCycle.isDraft()) - .query((rs, rowNum) -> parseResultSetToMatchCycle(rs)) - .optional(); - } - - @Override - public Optional getMatchCycleById(Integer id) { - String sql = """ - SELECT - *, - %s + RETURNING * + """; + + return jdbc.sql(sql) + .param("id", matchCycle.getId()) + .param("period", matchCycle.getPeriod()) + .param("run_at", matchCycle.getRunAt()) + .param("is_draft", matchCycle.isDraft()) + .query((rs, rowNum) -> parseResultSetToMatchCycle(rs)) + .optional(); + } + + @Override + public Optional getMatchCycleById(Integer id) { + String sql = """ + SELECT * FROM match_cycles WHERE id = :id - """.formatted(TOTAL_MATCHED_SQL); - return jdbc.sql(sql) - .param("id", id) - .query((rs, rowNum) -> parseResultSetToMatchCycle(rs)) - .optional(); - } - - @Override - public Optional setMatchCycleDraft(Integer id, boolean isDraft) { - String sql = """ + """; + return jdbc.sql(sql) + .param("id", id) + .query((rs, rowNum) -> parseResultSetToMatchCycle(rs)) + .optional(); + } + + @Override + public Optional setMatchCycleDraft(Integer id, boolean isDraft) { + String sql = """ UPDATE "match_cycles" SET "is_draft" = :is_draft WHERE "id" = :id - RETURNING - *, - %s - """.formatted(TOTAL_MATCHED_SQL); - - return jdbc.sql(sql) - .param("id", id) - .param("is_draft", isDraft) - .query((rs, rowNum) -> parseResultSetToMatchCycle(rs)) - .optional(); - } - - @Override - public Optional deleteMatchCycleById(Integer id) { - String sql = """ + RETURNING * + """; + + return jdbc.sql(sql) + .param("id", id) + .param("is_draft", isDraft) + .query((rs, rowNum) -> parseResultSetToMatchCycle(rs)) + .optional(); + } + + @Override + public Optional deleteMatchCycleById(Integer id) { + String sql = """ DELETE FROM match_cycles WHERE id = :id - RETURNING - *, - 0 AS total_matched + RETURNING * """; - return jdbc.sql(sql) - .param("id", id) - .query((rs, rowNum) -> parseResultSetToMatchCycle(rs)) - .optional(); - } - - @Override - public List filterMatchCycles(MatchCycleFilterCriteria criteria) { - StringBuilder sql = new StringBuilder("SELECT *, " + TOTAL_MATCHED_SQL + " FROM match_cycles WHERE 1=1"); - MapSqlParameterSource params = new MapSqlParameterSource(); - - criteria.period().ifPresent(period -> { - sql.append(" AND period = :period"); - params.addValue("period", period); - }); - - criteria.startTime().ifPresent(start -> { - sql.append(" AND run_at >= :start_time"); - params.addValue("start_time", start); - }); - - criteria.endTime().ifPresent(end -> { - sql.append(" AND run_at <= :end_time"); - params.addValue("end_time", end); - }); - - criteria.isDraft().ifPresent(isDraft -> { - sql.append(" AND is_draft = :is_draft"); - params.addValue("is_draft", isDraft); - }); - - return jdbc.sql(sql.toString()) - .paramSource(params) - .query((rs, rowNum) -> parseResultSetToMatchCycle(rs)) - .list(); - } + return jdbc.sql(sql) + .param("id", id) + .query((rs, rowNum) -> parseResultSetToMatchCycle(rs)) + .optional(); + } + + @Override + public List filterMatchCycles(MatchCycleFilterCriteria criteria) { + StringBuilder sql = new StringBuilder("SELECT * FROM match_cycles WHERE 1=1"); + MapSqlParameterSource params = new MapSqlParameterSource(); + + criteria.period().ifPresent(period -> { + sql.append(" AND period = :period"); + params.addValue("period", period); + }); + + criteria.startTime().ifPresent(start -> { + sql.append(" AND run_at >= :start_time"); + params.addValue("start_time", start); + }); + + criteria.endTime().ifPresent(end -> { + sql.append(" AND run_at <= :end_time"); + params.addValue("end_time", end); + }); + + criteria.isDraft().ifPresent(isDraft -> { + sql.append(" AND is_draft = :is_draft"); + params.addValue("is_draft", isDraft); + }); + + return jdbc.sql(sql.toString()) + .paramSource(params) + .query((rs, rowNum) -> parseResultSetToMatchCycle(rs)) + .list(); + } } diff --git a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchRepo.java b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchRepo.java index 0d31f673..29408487 100644 --- a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchRepo.java +++ b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchRepo.java @@ -35,6 +35,8 @@ public interface MatchRepo { Optional setMatchStatus(UUID id, String status); + Optional setMatchScore(UUID id, Integer score); + Optional deleteMatchById(UUID id); List filterMatches(MatchFilterCriteria criteria); diff --git a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchSqlRepo.java b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchSqlRepo.java index fa90dfdd..de44822b 100644 --- a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchSqlRepo.java +++ b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchSqlRepo.java @@ -48,9 +48,9 @@ public Match createMatch(Match match) { :match_score, :status ) - RETURNING - * + RETURNING * """; + return jdbc.sql(sql) .param("id", match.getId()) .param("member_a_id", match.getMemberAId()) @@ -74,6 +74,7 @@ public Optional updateMatch(Match match) { WHERE "id" = :id RETURNING * """; + return jdbc.sql(sql) .param("id", match.getId()) .param("member_a_id", match.getMemberAId()) @@ -108,6 +109,20 @@ public Optional setMatchStatus(UUID id, String status) { .optional(); } + @Override + public Optional setMatchScore(UUID id, Integer score) { + String sql = """ + UPDATE "matches" SET "match_score" = :score + WHERE "id" = :id + RETURNING * + """; + return jdbc.sql(sql) + .param("id", id) + .param("score", score) + .query((rs, rowNum) -> parseResultSetToMatch(rs)) + .optional(); + } + @Override public Optional deleteMatchById(UUID id) { String sql = "DELETE FROM matches WHERE id = :id RETURNING *"; From 2a21363d61cf8edb9fa47f2c5a5b45b704326354 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 31 Aug 2026 13:11:48 -0400 Subject: [PATCH 5/5] Fix pgjdbc type-conversion bugs in match repos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Row mappers: Instant→OffsetDateTime.toInstant() for timestamptz columns - Row mapper: Float.class for REAL match_score (was Double.class, unsupported) - setMatchScore: Integer→Double to match model type - Remove @Builder.Default isDraft=true to prevent silent flip on update --- .../api/match/db/models/MatchCycle.java | 3 +- .../api/match/db/repos/MatchCycleSqlRepo.java | 4 +- .../api/match/db/repos/MatchRepo.java | 2 +- .../api/match/db/repos/MatchSqlRepo.java | 11 +- .../match/db/repos/MatchCycleSqlRepoTest.java | 189 ++++++++++++++ .../api/match/db/repos/MatchSqlRepoTest.java | 236 ++++++++++++++++++ 6 files changed, 436 insertions(+), 9 deletions(-) create mode 100644 src/test/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepoTest.java create mode 100644 src/test/java/org/patinanetwork/patchats/api/match/db/repos/MatchSqlRepoTest.java diff --git a/src/main/java/org/patinanetwork/patchats/api/match/db/models/MatchCycle.java b/src/main/java/org/patinanetwork/patchats/api/match/db/models/MatchCycle.java index 0182ff5a..9bc1c2b6 100644 --- a/src/main/java/org/patinanetwork/patchats/api/match/db/models/MatchCycle.java +++ b/src/main/java/org/patinanetwork/patchats/api/match/db/models/MatchCycle.java @@ -21,6 +21,5 @@ public class MatchCycle { private Instant runAt; @Setter - @Builder.Default - private boolean isDraft = true; + private boolean isDraft; } diff --git a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java index e3250be7..6c296647 100644 --- a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java +++ b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepo.java @@ -2,7 +2,7 @@ import java.sql.ResultSet; import java.sql.SQLException; -import java.time.Instant; +import java.time.OffsetDateTime; import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; @@ -20,7 +20,7 @@ private MatchCycle parseResultSetToMatchCycle(final ResultSet rs) throws SQLExce return MatchCycle.builder() .id(rs.getInt("id")) .period(rs.getString("period")) - .runAt(rs.getObject("run_at", Instant.class)) + .runAt(rs.getObject("run_at", OffsetDateTime.class).toInstant()) .isDraft(rs.getBoolean("is_draft")) .build(); } diff --git a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchRepo.java b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchRepo.java index 29408487..0fbac632 100644 --- a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchRepo.java +++ b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchRepo.java @@ -35,7 +35,7 @@ public interface MatchRepo { Optional setMatchStatus(UUID id, String status); - Optional setMatchScore(UUID id, Integer score); + Optional setMatchScore(UUID id, Double score); Optional deleteMatchById(UUID id); diff --git a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchSqlRepo.java b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchSqlRepo.java index de44822b..8782bd41 100644 --- a/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchSqlRepo.java +++ b/src/main/java/org/patinanetwork/patchats/api/match/db/repos/MatchSqlRepo.java @@ -2,7 +2,7 @@ import java.sql.ResultSet; import java.sql.SQLException; -import java.time.Instant; +import java.time.OffsetDateTime; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -23,9 +23,12 @@ private Match parseResultSetToMatch(final ResultSet rs) throws SQLException { .memberAId(UUID.fromString(rs.getString("member_a_id"))) .memberBId(UUID.fromString(rs.getString("member_b_id"))) .matchCycleId(rs.getInt("cycle_id")) - .matchScore(rs.getObject("match_score", Double.class)) + .matchScore( + rs.getObject("match_score", Float.class) == null + ? null + : rs.getObject("match_score", Float.class).doubleValue()) .status(rs.getString("status")) - .createdAt(rs.getObject("created_at", Instant.class)) + .createdAt(rs.getObject("created_at", OffsetDateTime.class).toInstant()) .build(); } @@ -110,7 +113,7 @@ public Optional setMatchStatus(UUID id, String status) { } @Override - public Optional setMatchScore(UUID id, Integer score) { + public Optional setMatchScore(UUID id, Double score) { String sql = """ UPDATE "matches" SET "match_score" = :score WHERE "id" = :id diff --git a/src/test/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepoTest.java b/src/test/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepoTest.java new file mode 100644 index 00000000..ec7cfaa0 --- /dev/null +++ b/src/test/java/org/patinanetwork/patchats/api/match/db/repos/MatchCycleSqlRepoTest.java @@ -0,0 +1,189 @@ +package org.patinanetwork.patchats.api.match.db.repos; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.patinanetwork.patchats.api.match.db.models.MatchCycle; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.simple.JdbcClient; + +@SuppressWarnings("unchecked") +class MatchCycleSqlRepoTest { + + @Test + void createMatchCycleReturnsRowFromDatabase() { + final JdbcClient jdbc = mock(JdbcClient.class); + // What is jdbc exactly? + final JdbcClient.StatementSpec statement = mock(JdbcClient.StatementSpec.class); + final JdbcClient.MappedQuerySpec query = mock(JdbcClient.MappedQuerySpec.class); + final MatchCycle matchCycle = MatchCycle.builder() + .id(1) + .period("2025-Q1") + .runAt(Instant.parse("2025-01-15T12:00:00Z")) + .isDraft(true) + .build(); + + when(jdbc.sql(ArgumentMatchers.anyString())).thenReturn(statement); + when(statement.param(ArgumentMatchers.anyString(), ArgumentMatchers.any())) + .thenReturn(statement); + when(statement.query(ArgumentMatchers.>any())).thenReturn(query); + when(query.single()).thenReturn(matchCycle); + + final MatchCycle result = new MatchCycleSqlRepo(jdbc).createMatchCycle(matchCycle); + + assertEquals(matchCycle, result); + verify(jdbc).sql(ArgumentMatchers.anyString()); + verify(statement).param("period", matchCycle.getPeriod()); + verify(statement).param("run_at", matchCycle.getRunAt()); + verify(statement).param("is_draft", matchCycle.isDraft()); + verify(query).single(); + } + + @Test + void updateMatchCycleBindsAllFields() { + final JdbcClient jdbc = mock(JdbcClient.class); + final JdbcClient.StatementSpec statement = mock(JdbcClient.StatementSpec.class); + final JdbcClient.MappedQuerySpec query = mock(JdbcClient.MappedQuerySpec.class); + final MatchCycle matchCycle = MatchCycle.builder() + .id(1) + .period("2025-Q2") + .runAt(Instant.parse("2025-04-15T12:00:00Z")) + .isDraft(false) + .build(); + + when(jdbc.sql(ArgumentMatchers.anyString())).thenReturn(statement); + when(statement.param(ArgumentMatchers.anyString(), ArgumentMatchers.any())) + .thenReturn(statement); + when(statement.query(ArgumentMatchers.>any())).thenReturn(query); + when(query.optional()).thenReturn(Optional.of(matchCycle)); + + final Optional result = new MatchCycleSqlRepo(jdbc).updateMatchCycle(matchCycle); + + assertTrue(result.isPresent()); + assertEquals(matchCycle, result.get()); + verify(statement).param("id", matchCycle.getId()); + verify(statement).param("period", matchCycle.getPeriod()); + verify(statement).param("run_at", matchCycle.getRunAt()); + verify(statement).param("is_draft", matchCycle.isDraft()); + verify(query).optional(); + } + + @Test + void getMatchCycleByIdBindsId() { + final JdbcClient jdbc = mock(JdbcClient.class); + final JdbcClient.StatementSpec statement = mock(JdbcClient.StatementSpec.class); + final JdbcClient.MappedQuerySpec query = mock(JdbcClient.MappedQuerySpec.class); + final MatchCycle matchCycle = MatchCycle.builder().id(1).build(); + + when(jdbc.sql(ArgumentMatchers.anyString())).thenReturn(statement); + when(statement.param(ArgumentMatchers.anyString(), ArgumentMatchers.any())) + .thenReturn(statement); + when(statement.query(ArgumentMatchers.>any())).thenReturn(query); + when(query.optional()).thenReturn(Optional.of(matchCycle)); + + final Optional result = new MatchCycleSqlRepo(jdbc).getMatchCycleById(1); + + assertTrue(result.isPresent()); + assertEquals(matchCycle, result.get()); + verify(statement).param("id", 1); + verify(query).optional(); + } + + @Test + void setMatchCycleDraftBindsDraft() { + final JdbcClient jdbc = mock(JdbcClient.class); + final JdbcClient.StatementSpec statement = mock(JdbcClient.StatementSpec.class); + final JdbcClient.MappedQuerySpec query = mock(JdbcClient.MappedQuerySpec.class); + final MatchCycle matchCycle = MatchCycle.builder().id(1).isDraft(false).build(); + + when(jdbc.sql(ArgumentMatchers.anyString())).thenReturn(statement); + when(statement.param(ArgumentMatchers.anyString(), ArgumentMatchers.any())) + .thenReturn(statement); + when(statement.query(ArgumentMatchers.>any())).thenReturn(query); + when(query.optional()).thenReturn(Optional.of(matchCycle)); + + final Optional result = new MatchCycleSqlRepo(jdbc).setMatchCycleDraft(1, false); + + assertTrue(result.isPresent()); + assertEquals(matchCycle, result.get()); + verify(statement).param("id", 1); + verify(statement).param("is_draft", false); + verify(query).optional(); + } + + @Test + void deleteMatchCycleByIdBindsId() { + final JdbcClient jdbc = mock(JdbcClient.class); + final JdbcClient.StatementSpec statement = mock(JdbcClient.StatementSpec.class); + final JdbcClient.MappedQuerySpec query = mock(JdbcClient.MappedQuerySpec.class); + final MatchCycle matchCycle = MatchCycle.builder().id(1).build(); + + when(jdbc.sql(ArgumentMatchers.anyString())).thenReturn(statement); + when(statement.param(ArgumentMatchers.anyString(), ArgumentMatchers.any())) + .thenReturn(statement); + when(statement.query(ArgumentMatchers.>any())).thenReturn(query); + when(query.optional()).thenReturn(Optional.of(matchCycle)); + + final Optional result = new MatchCycleSqlRepo(jdbc).deleteMatchCycleById(1); + + assertTrue(result.isPresent()); + assertEquals(matchCycle, result.get()); + verify(statement).param("id", 1); + verify(query).optional(); + } + + @Test + void filterMatchCyclesAppliesEveryProvidedCriterion() { + final JdbcClient jdbc = mock(JdbcClient.class); + final JdbcClient.StatementSpec statement = mock(JdbcClient.StatementSpec.class); + final JdbcClient.MappedQuerySpec query = mock(JdbcClient.MappedQuerySpec.class); + final Instant start = Instant.parse("2025-01-01T00:00:00Z"); + final Instant end = Instant.parse("2025-06-30T23:59:59Z"); + final MatchCycleFilterCriteria criteria = new MatchCycleFilterCriteria( + Optional.of("2025-Q1"), Optional.of(start), Optional.of(end), Optional.of(true)); + + when(jdbc.sql(ArgumentMatchers.anyString())).thenReturn(statement); + when(statement.paramSource(ArgumentMatchers.any(MapSqlParameterSource.class))) + .thenReturn(statement); + when(statement.query(ArgumentMatchers.>any())).thenReturn(query); + when(query.list()).thenReturn(java.util.List.of()); + + final java.util.List result = new MatchCycleSqlRepo(jdbc).filterMatchCycles(criteria); + + assertEquals(java.util.List.of(), result); + verify(jdbc).sql(ArgumentMatchers.anyString()); + verify(query).list(); + } + + @Test + void filterMatchCyclesOnlyIncludesProvidedCriteriaInWhereClause() { + final JdbcClient jdbc = mock(JdbcClient.class); + final JdbcClient.StatementSpec statement = mock(JdbcClient.StatementSpec.class); + final JdbcClient.MappedQuerySpec query = mock(JdbcClient.MappedQuerySpec.class); + final MatchCycleFilterCriteria criteria = new MatchCycleFilterCriteria( + Optional.of("2025-Q1"), Optional.empty(), Optional.empty(), Optional.of(true)); + final ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class); + + when(jdbc.sql(ArgumentMatchers.anyString())).thenReturn(statement); + when(statement.paramSource(ArgumentMatchers.any(MapSqlParameterSource.class))) + .thenReturn(statement); + when(statement.query(ArgumentMatchers.>any())).thenReturn(query); + when(query.list()).thenReturn(java.util.List.of()); + + new MatchCycleSqlRepo(jdbc).filterMatchCycles(criteria); + + verify(jdbc).sql(sqlCaptor.capture()); + assertEquals( + "SELECT * FROM match_cycles WHERE 1=1 AND period = :period AND is_draft = :is_draft", + sqlCaptor.getValue()); + } +} diff --git a/src/test/java/org/patinanetwork/patchats/api/match/db/repos/MatchSqlRepoTest.java b/src/test/java/org/patinanetwork/patchats/api/match/db/repos/MatchSqlRepoTest.java new file mode 100644 index 00000000..cb837e25 --- /dev/null +++ b/src/test/java/org/patinanetwork/patchats/api/match/db/repos/MatchSqlRepoTest.java @@ -0,0 +1,236 @@ +package org.patinanetwork.patchats.api.match.db.repos; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.Instant; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.patinanetwork.patchats.api.match.db.models.Match; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.simple.JdbcClient; + +@SuppressWarnings("unchecked") +class MatchSqlRepoTest { + + @Test + void createMatchReturnsRowFromDatabase() { + final JdbcClient jdbc = mock(JdbcClient.class); + final JdbcClient.StatementSpec statement = mock(JdbcClient.StatementSpec.class); + final JdbcClient.MappedQuerySpec query = mock(JdbcClient.MappedQuerySpec.class); + final Match match = Match.builder() + .id(UUID.randomUUID()) + .memberAId(UUID.randomUUID()) + .memberBId(UUID.randomUUID()) + .matchCycleId(1) + .matchScore(8.5) + .status("pending") + .build(); + + when(jdbc.sql(ArgumentMatchers.anyString())).thenReturn(statement); + when(statement.param(ArgumentMatchers.anyString(), ArgumentMatchers.any())) + .thenReturn(statement); + when(statement.query(ArgumentMatchers.>any())).thenReturn(query); + when(query.single()).thenReturn(match); + + final Match result = new MatchSqlRepo(jdbc).createMatch(match); + + assertEquals(match, result); + verify(jdbc).sql(ArgumentMatchers.anyString()); + verify(statement).param("id", match.getId()); + verify(statement).param("member_a_id", match.getMemberAId()); + verify(statement).param("member_b_id", match.getMemberBId()); + verify(statement).param("cycle_id", match.getMatchCycleId()); + verify(statement).param("match_score", match.getMatchScore()); + verify(statement).param("status", match.getStatus()); + verify(query).single(); + } + + @Test + void updateMatchBindsAllFields() { + final JdbcClient jdbc = mock(JdbcClient.class); + final JdbcClient.StatementSpec statement = mock(JdbcClient.StatementSpec.class); + final JdbcClient.MappedQuerySpec query = mock(JdbcClient.MappedQuerySpec.class); + final Match match = Match.builder() + .id(UUID.randomUUID()) + .memberAId(UUID.randomUUID()) + .memberBId(UUID.randomUUID()) + .matchCycleId(2) + .matchScore(9.0) + .status("confirmed") + .build(); + + when(jdbc.sql(ArgumentMatchers.anyString())).thenReturn(statement); + when(statement.param(ArgumentMatchers.anyString(), ArgumentMatchers.any())) + .thenReturn(statement); + when(statement.query(ArgumentMatchers.>any())).thenReturn(query); + when(query.optional()).thenReturn(Optional.of(match)); + + final Optional result = new MatchSqlRepo(jdbc).updateMatch(match); + + assertTrue(result.isPresent()); + assertEquals(match, result.get()); + verify(statement).param("id", match.getId()); + verify(statement).param("member_a_id", match.getMemberAId()); + verify(statement).param("member_b_id", match.getMemberBId()); + verify(statement).param("cycle_id", match.getMatchCycleId()); + verify(statement).param("match_score", match.getMatchScore()); + verify(statement).param("status", match.getStatus()); + verify(query).optional(); + } + + @Test + void getMatchByIdBindsId() { + final JdbcClient jdbc = mock(JdbcClient.class); + final JdbcClient.StatementSpec statement = mock(JdbcClient.StatementSpec.class); + final JdbcClient.MappedQuerySpec query = mock(JdbcClient.MappedQuerySpec.class); + final UUID id = UUID.randomUUID(); + final Match match = Match.builder().id(id).build(); + + when(jdbc.sql(ArgumentMatchers.anyString())).thenReturn(statement); + when(statement.param(ArgumentMatchers.anyString(), ArgumentMatchers.any())) + .thenReturn(statement); + when(statement.query(ArgumentMatchers.>any())).thenReturn(query); + when(query.optional()).thenReturn(Optional.of(match)); + + final Optional result = new MatchSqlRepo(jdbc).getMatchById(id); + + assertTrue(result.isPresent()); + assertEquals(match, result.get()); + verify(statement).param("id", id); + verify(query).optional(); + } + + @Test + void setMatchStatusBindsStatus() { + final JdbcClient jdbc = mock(JdbcClient.class); + final JdbcClient.StatementSpec statement = mock(JdbcClient.StatementSpec.class); + final JdbcClient.MappedQuerySpec query = mock(JdbcClient.MappedQuerySpec.class); + final UUID id = UUID.randomUUID(); + final Match match = Match.builder().id(id).status("completed").build(); + + when(jdbc.sql(ArgumentMatchers.anyString())).thenReturn(statement); + when(statement.param(ArgumentMatchers.anyString(), ArgumentMatchers.any())) + .thenReturn(statement); + when(statement.query(ArgumentMatchers.>any())).thenReturn(query); + when(query.optional()).thenReturn(Optional.of(match)); + + final Optional result = new MatchSqlRepo(jdbc).setMatchStatus(id, "completed"); + + assertTrue(result.isPresent()); + assertEquals(match, result.get()); + verify(statement).param("id", id); + verify(statement).param("status", "completed"); + verify(query).optional(); + } + + @Test + void setMatchScoreBindsScore() { + final JdbcClient jdbc = mock(JdbcClient.class); + final JdbcClient.StatementSpec statement = mock(JdbcClient.StatementSpec.class); + final JdbcClient.MappedQuerySpec query = mock(JdbcClient.MappedQuerySpec.class); + final UUID id = UUID.randomUUID(); + final Match match = Match.builder().id(id).matchScore(7.25).build(); + + when(jdbc.sql(ArgumentMatchers.anyString())).thenReturn(statement); + when(statement.param(ArgumentMatchers.anyString(), ArgumentMatchers.any())) + .thenReturn(statement); + when(statement.query(ArgumentMatchers.>any())).thenReturn(query); + when(query.optional()).thenReturn(Optional.of(match)); + + final Optional result = new MatchSqlRepo(jdbc).setMatchScore(id, 7.25); + + assertTrue(result.isPresent()); + assertEquals(match, result.get()); + verify(statement).param("id", id); + verify(statement).param("score", 7.25); + verify(query).optional(); + } + + @Test + void deleteMatchByIdBindsId() { + final JdbcClient jdbc = mock(JdbcClient.class); + final JdbcClient.StatementSpec statement = mock(JdbcClient.StatementSpec.class); + final JdbcClient.MappedQuerySpec query = mock(JdbcClient.MappedQuerySpec.class); + final UUID id = UUID.randomUUID(); + final Match match = Match.builder().id(id).build(); + + when(jdbc.sql(ArgumentMatchers.anyString())).thenReturn(statement); + when(statement.param(ArgumentMatchers.anyString(), ArgumentMatchers.any())) + .thenReturn(statement); + when(statement.query(ArgumentMatchers.>any())).thenReturn(query); + when(query.optional()).thenReturn(Optional.of(match)); + + final Optional result = new MatchSqlRepo(jdbc).deleteMatchById(id); + + assertTrue(result.isPresent()); + assertEquals(match, result.get()); + verify(statement).param("id", id); + verify(query).optional(); + } + + @Test + void filterMatchesAppliesEveryProvidedCriterion() { + final JdbcClient jdbc = mock(JdbcClient.class); + final JdbcClient.StatementSpec statement = mock(JdbcClient.StatementSpec.class); + final JdbcClient.MappedQuerySpec query = mock(JdbcClient.MappedQuerySpec.class); + final Instant start = Instant.parse("2025-01-01T00:00:00Z"); + final Instant end = Instant.parse("2025-06-30T23:59:59Z"); + final MatchFilterCriteria criteria = new MatchFilterCriteria( + Optional.of(start), + Optional.of(end), + Optional.of("2025-Q1"), + Optional.of(UUID.randomUUID()), + Optional.of(1), + Optional.of("Technology"), + Optional.of("confirmed")); + + when(jdbc.sql(ArgumentMatchers.anyString())).thenReturn(statement); + when(statement.paramSource(ArgumentMatchers.any(MapSqlParameterSource.class))) + .thenReturn(statement); + when(statement.query(ArgumentMatchers.>any())).thenReturn(query); + when(query.list()).thenReturn(java.util.List.of()); + + final java.util.List result = new MatchSqlRepo(jdbc).filterMatches(criteria); + + assertEquals(java.util.List.of(), result); + verify(jdbc).sql(ArgumentMatchers.anyString()); + verify(query).list(); + } + + @Test + void filterMatchesOnlyIncludesProvidedCriteriaInWhereClause() { + final JdbcClient jdbc = mock(JdbcClient.class); + final JdbcClient.StatementSpec statement = mock(JdbcClient.StatementSpec.class); + final JdbcClient.MappedQuerySpec query = mock(JdbcClient.MappedQuerySpec.class); + final MatchFilterCriteria criteria = new MatchFilterCriteria( + Optional.empty(), + Optional.empty(), + Optional.of("2025-Q1"), + Optional.empty(), + Optional.empty(), + Optional.empty(), + Optional.of("pending")); + final ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class); + + when(jdbc.sql(ArgumentMatchers.anyString())).thenReturn(statement); + when(statement.paramSource(ArgumentMatchers.any(MapSqlParameterSource.class))) + .thenReturn(statement); + when(statement.query(ArgumentMatchers.>any())).thenReturn(query); + when(query.list()).thenReturn(java.util.List.of()); + + new MatchSqlRepo(jdbc).filterMatches(criteria); + + verify(jdbc).sql(sqlCaptor.capture()); + assertEquals( + "SELECT * FROM matches WHERE 1=1 AND status = :status AND cycle_id IN (SELECT id FROM match_cycles WHERE period = :period)", + sqlCaptor.getValue()); + } +}