donau

Donation authority for GNU Taler (experimental)
Log | Files | Refs | Submodules | README | LICENSE

commit a13d30ecd89da7cf234f6a2bab9d16d5a785d553
parent 41b3950cde9ea9a6fe7e0d5c668c7a768b3b6ad4
Author: Christian Grothoff <christian@grothoff.org>
Date:   Fri,  7 Aug 2026 00:17:59 +0200

fix history PK, also give clear indication of unique constraint violation on insert

Diffstat:
Asrc/donaudb/donau-0003.sql | 34++++++++++++++++++++++++++++++++++
Msrc/donaudb/insert_history_entry.c | 21+++++++++++++++++----
Msrc/donaudb/meson.build | 1+
Msrc/donaudb/test_donaudb.c | 36++++++++++++++++++++++++++++++++++++
Msrc/include/donau-database/insert_history_entry.h | 5++++-
5 files changed, 92 insertions(+), 5 deletions(-)

diff --git a/src/donaudb/donau-0003.sql b/src/donaudb/donau-0003.sql @@ -0,0 +1,34 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2026 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER is distributed in the hope that it will be useful, but WITHOUT ANY +-- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +-- A PARTICULAR PURPOSE. See the GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +BEGIN; + +SELECT _v.register_patch('donau-0003', NULL, NULL); +SET search_path TO donau; + +-- The `history' table is documented as holding "the yearly donation amount +-- for each charity", but its primary key was `charity_id' alone, so it could +-- hold at most ONE year per charity: the second year was a unique violation. +-- The key must include the year. +ALTER TABLE history + DROP CONSTRAINT history_pkey; +ALTER TABLE history + ADD CONSTRAINT history_pkey PRIMARY KEY (charity_id, donation_year); + +COMMENT ON COLUMN history.donation_year + IS 'Year the final_amount was accumulated in; part of the primary key.'; + +COMMIT; diff --git a/src/donaudb/insert_history_entry.c b/src/donaudb/insert_history_entry.c @@ -38,7 +38,17 @@ DONAUDB_insert_history_entry (struct DONAUDB_PostgresContext *ctx, GNUNET_PQ_query_param_uint64 (&donation_year), GNUNET_PQ_query_param_end }; + uint64_t out_charity_id; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_uint64 ("charity_id", + &out_charity_id), + GNUNET_PQ_result_spec_end + }; + /* The RETURNING clause is what makes "written" distinguishable from + "a row for this (charity,year) already exists": without it, the + unique violation would be mapped to SUCCESS_NO_RESULTS, which is + also what a caller checking only for `qs < 0' reads as success. */ PREPARE (ctx, "insert_history_entry", "INSERT INTO history " @@ -46,8 +56,11 @@ DONAUDB_insert_history_entry (struct DONAUDB_PostgresContext *ctx, ",final_amount" ",donation_year" ") VALUES " - "($1, $2, $3);"); - return GNUNET_PQ_eval_prepared_non_select (ctx->conn, - "insert_history_entry", - params); + "($1, $2, $3)" + " ON CONFLICT (charity_id, donation_year) DO NOTHING" + " RETURNING charity_id;"); + return GNUNET_PQ_eval_prepared_singleton_select (ctx->conn, + "insert_history_entry", + params, + rs); } diff --git a/src/donaudb/meson.build b/src/donaudb/meson.build @@ -30,6 +30,7 @@ generated_sql = [ ['procedures.sql', procedures_sql], ['donau-0001.sql', ['donau-0001.sql']], ['donau-0002.sql', donau_0002], + ['donau-0003.sql', ['donau-0003.sql']], ] diff --git a/src/donaudb/test_donaudb.c b/src/donaudb/test_donaudb.c @@ -474,6 +474,42 @@ run (void *cls) DONAUDB_commit (ctx)); } + /* D-5: `history' is documented as holding the *yearly* total per + charity, so it must be able to hold more than one year per charity, + and a repeated write for the same year must be distinguishable from + a successful one. */ + { + uint64_t hist_charity_id; + struct TALER_Amount y1; + struct TALER_Amount y2; + + FAILIF (GNUNET_OK != + make_charity (CURRENCY ":1000", + &hist_charity_id)); + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (CURRENCY ":90", + &y1)); + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (CURRENCY ":75", + &y2)); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + DONAUDB_insert_history_entry (ctx, + hist_charity_id, + &y1, + 2026)); + FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != + DONAUDB_insert_history_entry (ctx, + hist_charity_id, + &y2, + 2027)); + /* Re-recording the same year must report "nothing written". */ + FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != + DONAUDB_insert_history_entry (ctx, + hist_charity_id, + &y2, + 2027)); + } + result = 0; drop: diff --git a/src/include/donau-database/insert_history_entry.h b/src/include/donau-database/insert_history_entry.h @@ -31,7 +31,10 @@ * @param charity_id charity id * @param final_amount final donation amount at the end of the donation year * @param donation_year year of the donations - * @return transaction status code + * @return transaction status code; + * #GNUNET_DB_STATUS_SUCCESS_ONE_RESULT if the entry was written, + * #GNUNET_DB_STATUS_SUCCESS_NO_RESULTS if an entry for that + * charity and year already existed (nothing was written) */ enum GNUNET_DB_QueryStatus DONAUDB_insert_history_entry (