Hi, I noticed that contrib/fuzzystrmatch has poor test coverage. The proposed patch fixes [1] this:
dmetaphone.c: 17.8% -> 94.9% fuzzystrmatch.c: 49.5% -> 98.9% daitch_mokotoff.c: 95.7% -> 98.8% Feedback is most welcome. ### Note for the reviewers ### You can verify the patch like this (change -Dprefix): $ time -p sh -c 'git clean -df && (rm -r build || true) && meson setup --buildtype debug -Db_coverage=true -Dcassert=true -Dinjection_points=true -Dtap_tests=enabled -Dldap=disabled -Dicu=disabled -DPG_TEST_EXTRA="kerberos ldap libpq_encryption load_balance oauth regress_dump_restore ssl wal_consistency_checking xid_wraparound" -Dprefix=/home/eax/pginstall build && ninja -C build && meson test -j 8 -C build && ninja -j 8 -C build coverage-html && open build/meson-logs/coveragereport/contrib/fuzzystrmatch/index.html' Better to use lcov <= 1.16. We had some problems with newer versions last time I checked. [1]: https://gist.github.com/afiskon/b26d9b3e46524ba16d8f4ba9004cea66 -- Best regards, Aleksander Alekseev
From e8afb7bae87cdedfff13e4f2cc3153c540a5eddc Mon Sep 17 00:00:00 2001 From: Aleksander Alekseev <[email protected]> Date: Fri, 30 Oct 2026 15:53:10 +0300 Subject: [PATCH v1] contrib/fuzzystrmatch: imporve test coverage Author: Aleksander Alekseev <[email protected]> Reviewed-by: TODO FIXME Discussion: TODO FIXME --- .../fuzzystrmatch/expected/fuzzystrmatch.out | 311 ++++++++++++++++++ contrib/fuzzystrmatch/sql/fuzzystrmatch.sql | 123 +++++++ 2 files changed, 434 insertions(+) diff --git a/contrib/fuzzystrmatch/expected/fuzzystrmatch.out b/contrib/fuzzystrmatch/expected/fuzzystrmatch.out index aeab031a2ed..d284e7a164b 100644 --- a/contrib/fuzzystrmatch/expected/fuzzystrmatch.out +++ b/contrib/fuzzystrmatch/expected/fuzzystrmatch.out @@ -29,6 +29,25 @@ SELECT soundex(''), difference('', ''); | 4 (1 row) +-- Leading non-alphabetic characters are skipped +SELECT soundex('-hello'); + soundex +--------- + H400 +(1 row) + +SELECT soundex('3M'); + soundex +--------- + M000 +(1 row) + +SELECT soundex('!@#'); + soundex +--------- + +(1 row) + SELECT levenshtein('GUMBO', 'GAMBOL'); levenshtein ------------- @@ -73,6 +92,126 @@ SELECT metaphone('GUMBO', 4); KM (1 row) +-- Edge cases and errors +SELECT metaphone('', 4); + metaphone +----------- + +(1 row) + +SELECT metaphone('-', 10); + metaphone +----------- + +(1 row) + +SELECT metaphone(repeat('a', 256), 4); +ERROR: argument exceeds the maximum length of 255 bytes +SELECT metaphone('gumbo', 256); +ERROR: output exceeds the maximum length of 255 bytes +SELECT metaphone('gumbo', 0); +ERROR: output cannot be empty string +SELECT metaphone('gumbo', -1); +ERROR: output cannot be empty string +-- The first letter of the word is handled specially +SELECT w, metaphone(w, 10) FROM unnest(ARRAY[ + 'aero', -- AE becomes E + 'anne', -- other leading vowels are preserved + 'gnome', -- GN becomes N + 'knight', -- KN becomes N + 'pneumatic', -- PN becomes N + 'white', -- WH becomes H + 'wrong', -- WR becomes R + 'water', -- W before a vowel is kept + 'wto', -- W not before a vowel is dropped + 'xray', -- X becomes S + 'eat', 'igloo', 'open', 'under' +]) AS w; + w | metaphone +-----------+----------- + aero | ER + anne | AN + gnome | NM + knight | NFT + pneumatic | NMTK + white | HT + wrong | RNK + water | WTR + wto | T + xray | SR + eat | ET + igloo | IKL + open | OPN + under | UNTR +(14 rows) + +-- The rest of the word +SELECT w, metaphone(w, 10) FROM unnest(ARRAY[ + 'bob', -- B + 'dumb', -- B in MB is dropped + 'special', -- CIA becomes X + 'science', -- C in SCI is dropped, C before E becomes S + 'christ', -- CH before R becomes K + 'school', -- CH after S becomes K + 'anchor', -- CH elsewhere in the word becomes X + 'chair', -- CH otherwise becomes X + 'judge', -- DG before E becomes J + 'daughter', -- GH after D is silent + 'knight', -- GH otherwise becomes F + 'alright', -- GH far enough into the word to look four letters back + 'sign', -- GN at the end of a word is dropped + 'signed', -- GNED is dropped + 'magnet', -- GN otherwise becomes K + 'george', -- G before E becomes J + 'behave', -- H before a vowel is kept, V becomes F + 'phone', -- PH becomes F, H after P is dropped + 'black', -- K after C is dropped + 'kite', -- K otherwise is kept + 'queen', -- Q becomes K + 'vision', -- SIO becomes X + 'shoe', -- SH becomes X + 'schwa', -- SCHW becomes X + 'nation', -- TIO becomes X + 'thing', -- TH becomes 0 + 'boxer', -- X becomes KS + 'yellow', -- Y before a vowel is kept + 'zebra', -- Z becomes S + 'o''brien' -- non-alphabetic characters are ignored +]) AS w; + w | metaphone +----------+----------- + bob | BB + dumb | TM + special | SPXL + science | SNS + christ | KRST + school | SKL + anchor | ANXR + chair | XR + judge | JJ + daughter | TTR + knight | NFT + alright | ALRFT + sign | SN + signed | SNT + magnet | MKNT + george | JRJ + behave | BHF + phone | FN + black | BLK + kite | KT + queen | KN + vision | FXN + shoe | X + schwa | XW + nation | NXN + thing | 0NK + boxer | BKSR + yellow | YL + zebra | SBR + o'brien | OBRN +(30 rows) + SELECT dmetaphone('gumbo'); dmetaphone ------------ @@ -85,6 +224,158 @@ SELECT dmetaphone_alt('gumbo'); KMP (1 row) +SELECT dmetaphone(''), dmetaphone_alt(''); + dmetaphone | dmetaphone_alt +------------+---------------- + | +(1 row) + +SELECT w, dmetaphone(w) AS dm, dmetaphone_alt(w) AS alt FROM unnest(ARRAY[ + 'Smith', 'Schmidt', 'Wagner', 'Kowalski', 'Czerny', 'McCarthy', + 'Caesar', 'Chianti', 'Michael', 'Chorus', 'Architect', 'Orchestra', + 'Bacher', 'Wachter', 'Ghislaine', 'Hugh', 'Laugh', 'Gnome', 'Judge', + 'Sugar', 'Island', 'Jose', 'Jackson', 'Villa', 'Pfeiffer', 'Thomas', + 'Xavier', 'Zhao', 'Bordeaux', 'Dumb', 'Knight', 'Wright', 'Gonzalez', + 'Sanchez', 'Nowakowski', 'Anne', 'Breaux', 'Van Gogh', 'Schwarzenegger', + 'Campbell', 'MacGregor', 'Tchaikovsky', 'Dvorak', 'Niece', 'Resnais', + 'Arnow', 'Filipowicz', 'Rogier', 'Hochmeier', 'Vasquez', 'Yankelovich' +]) AS w; + w | dm | alt +----------------+------+------ + Smith | SM0 | XMT + Schmidt | XMT | SMT + Wagner | AKNR | FKNR + Kowalski | KLSK | KLSK + Czerny | SRN | XRN + McCarthy | MKR0 | MKRT + Caesar | SSR | SSR + Chianti | KNT | KNT + Michael | MKL | MXL + Chorus | KRS | KRS + Architect | ARKT | ARKT + Orchestra | ARKS | ARKS + Bacher | PKR | PKR + Wachter | AKTR | FKTR + Ghislaine | JLN | JLN + Hugh | H | H + Laugh | LF | LF + Gnome | NM | NM + Judge | JJ | AJ + Sugar | XKR | SKR + Island | ALNT | ALNT + Jose | HS | HS + Jackson | JKSN | AKSN + Villa | FL | F + Pfeiffer | PFFR | PFFR + Thomas | TMS | TMS + Xavier | SF | SFR + Zhao | J | J + Bordeaux | PRT | PRT + Dumb | TM | TM + Knight | NT | NT + Wright | RT | RT + Gonzalez | KNSL | KNSL + Sanchez | SNXS | SNKS + Nowakowski | NKSK | NKFS + Anne | AN | AN + Breaux | PR | PR + Van Gogh | FNKK | FNKK + Schwarzenegger | XRSN | XFRT + Campbell | KMPL | KMPL + MacGregor | MKRK | MKRK + Tchaikovsky | XKFS | XKFS + Dvorak | TFRK | TFRK + Niece | NS | NS + Resnais | RSN | RSNS + Arnow | ARN | ARNF + Filipowicz | FLPT | FLPF + Rogier | RJ | RJR + Hochmeier | HKMR | HKMR + Vasquez | FSKS | FSKS + Yankelovich | ANKL | ANKL +(51 rows) + +-- Rarer branches of the encoder. Most of these are letter combinations that +-- only show up in names of a particular origin. +SELECT w, dmetaphone(w) AS dm, dmetaphone_alt(w) AS alt FROM unnest(ARRAY[ + 'McHugh', -- CH preceded by MC + 'Chopin', -- CH treated as K rather than X + 'Fuccia', -- CC followed by IA + 'Accede', -- CC followed by E + 'Bellocchio', -- CC followed by H + 'Ecclesiastes', -- CC followed by a consonant, and SIA + 'Wojciech', -- CI in a Slavic name, and a trailing CH + 'Judgment', -- DG followed by a consonant + 'Burgh', -- GH preceded by a consonant + 'Ghana', -- GH at the start of a word + 'Agnes', -- GN after an initial vowel + 'Lasagna', -- GN inside a word + 'Gigli', -- GLI in an Italian name + 'Berger', -- GER at the end of a word + 'Gergen', -- G before E, not at the start + 'Schlegel', -- G before E in a word starting with SCH + 'Gersheim', -- G before E, and SH followed by HEIM + 'Josephine', -- initial JOSE not followed by a space, and PH + 'Ajax', -- J inside a word, and X + 'Raj', -- J at the end of a word + 'Shipwright', -- SH inside a word, and WR not at the start + 'Stensholm', -- SH followed by HOLM + 'Kasian', -- SIAN in a Slavic name + 'Schenker', -- SCH followed by a consonant + 'Schooner', -- SCH followed by OO + 'Brescia', -- SC followed by I + 'Scot', -- SC followed by a consonant + 'Nation', -- TION + 'Wheeler' -- W followed by H +]) AS w; + w | dm | alt +--------------+------+------ + McHugh | MK | MK + Chopin | XPN | XPN + Fuccia | FX | FX + Accede | AKST | AKST + Bellocchio | PLX | PLX + Ecclesiastes | AKLS | AKLX + Wojciech | AJSK | FJXK + Judgment | JTKM | ATKM + Burgh | PRK | PRK + Ghana | KN | KN + Agnes | AKNS | ANS + Lasagna | LSN | LSKN + Gigli | JKL | KL + Berger | PRKR | PRJR + Gergen | KRJN | JRKN + Schlegel | XLKL | SLKL + Gersheim | KRSM | JRSM + Josephine | JSFN | HSFN + Ajax | AJKS | AHKS + Raj | RJ | R + Shipwright | XPRT | XPRT + Stensholm | STNS | STNS + Kasian | KSN | KSN + Schenker | XNKR | SKNK + Schooner | SKNR | SKNR + Brescia | PRS | PRS + Scot | SKT | SKT + Nation | NXN | NXN + Wheeler | ALR | ALR +(29 rows) + +-- A long input +SELECT dmetaphone(repeat('schwarzenegger', 20)); + dmetaphone +------------ + XRSN +(1 row) + +-- Repeated GLI adds two characters to the primary code but only one to the +-- secondary one, which is what makes the code outgrow its initial buffer +SELECT dmetaphone('agliagliaglia'); + dmetaphone +------------ + AKLK +(1 row) + -- Wovels SELECT daitch_mokotoff('Augsburg'); daitch_mokotoff @@ -213,6 +504,26 @@ SELECT daitch_mokotoff('O''Brien'); {079600} (1 row) +-- Input with no encodable characters +SELECT daitch_mokotoff(''); + daitch_mokotoff +----------------- + +(1 row) + +SELECT daitch_mokotoff('123'); + daitch_mokotoff +----------------- + +(1 row) + +-- ASCII characters reserved for internal conversions +SELECT daitch_mokotoff('a[b'); + daitch_mokotoff +----------------- + {070000} +(1 row) + -- "Difficult" cases, likely to cause trouble for other implementations. SELECT daitch_mokotoff('CJC'); daitch_mokotoff diff --git a/contrib/fuzzystrmatch/sql/fuzzystrmatch.sql b/contrib/fuzzystrmatch/sql/fuzzystrmatch.sql index c1d55f823d7..a630638ebc0 100644 --- a/contrib/fuzzystrmatch/sql/fuzzystrmatch.sql +++ b/contrib/fuzzystrmatch/sql/fuzzystrmatch.sql @@ -8,6 +8,11 @@ SELECT soundex('Anne'), soundex('Andrew'), difference('Anne', 'Andrew'); SELECT soundex('Anne'), soundex('Margaret'), difference('Anne', 'Margaret'); SELECT soundex(''), difference('', ''); +-- Leading non-alphabetic characters are skipped +SELECT soundex('-hello'); +SELECT soundex('3M'); +SELECT soundex('!@#'); + SELECT levenshtein('GUMBO', 'GAMBOL'); SELECT levenshtein('GUMBO', 'GAMBOL', 2, 1, 1); @@ -20,10 +25,121 @@ SELECT levenshtein_less_equal('aaa', 'aaaaa', 1073741824, 0, 1073741824, 10); SELECT metaphone('GUMBO', 4); +-- Edge cases and errors +SELECT metaphone('', 4); +SELECT metaphone('-', 10); +SELECT metaphone(repeat('a', 256), 4); +SELECT metaphone('gumbo', 256); +SELECT metaphone('gumbo', 0); +SELECT metaphone('gumbo', -1); + +-- The first letter of the word is handled specially +SELECT w, metaphone(w, 10) FROM unnest(ARRAY[ + 'aero', -- AE becomes E + 'anne', -- other leading vowels are preserved + 'gnome', -- GN becomes N + 'knight', -- KN becomes N + 'pneumatic', -- PN becomes N + 'white', -- WH becomes H + 'wrong', -- WR becomes R + 'water', -- W before a vowel is kept + 'wto', -- W not before a vowel is dropped + 'xray', -- X becomes S + 'eat', 'igloo', 'open', 'under' +]) AS w; + +-- The rest of the word +SELECT w, metaphone(w, 10) FROM unnest(ARRAY[ + 'bob', -- B + 'dumb', -- B in MB is dropped + 'special', -- CIA becomes X + 'science', -- C in SCI is dropped, C before E becomes S + 'christ', -- CH before R becomes K + 'school', -- CH after S becomes K + 'anchor', -- CH elsewhere in the word becomes X + 'chair', -- CH otherwise becomes X + 'judge', -- DG before E becomes J + 'daughter', -- GH after D is silent + 'knight', -- GH otherwise becomes F + 'alright', -- GH far enough into the word to look four letters back + 'sign', -- GN at the end of a word is dropped + 'signed', -- GNED is dropped + 'magnet', -- GN otherwise becomes K + 'george', -- G before E becomes J + 'behave', -- H before a vowel is kept, V becomes F + 'phone', -- PH becomes F, H after P is dropped + 'black', -- K after C is dropped + 'kite', -- K otherwise is kept + 'queen', -- Q becomes K + 'vision', -- SIO becomes X + 'shoe', -- SH becomes X + 'schwa', -- SCHW becomes X + 'nation', -- TIO becomes X + 'thing', -- TH becomes 0 + 'boxer', -- X becomes KS + 'yellow', -- Y before a vowel is kept + 'zebra', -- Z becomes S + 'o''brien' -- non-alphabetic characters are ignored +]) AS w; + SELECT dmetaphone('gumbo'); SELECT dmetaphone_alt('gumbo'); +SELECT dmetaphone(''), dmetaphone_alt(''); + +SELECT w, dmetaphone(w) AS dm, dmetaphone_alt(w) AS alt FROM unnest(ARRAY[ + 'Smith', 'Schmidt', 'Wagner', 'Kowalski', 'Czerny', 'McCarthy', + 'Caesar', 'Chianti', 'Michael', 'Chorus', 'Architect', 'Orchestra', + 'Bacher', 'Wachter', 'Ghislaine', 'Hugh', 'Laugh', 'Gnome', 'Judge', + 'Sugar', 'Island', 'Jose', 'Jackson', 'Villa', 'Pfeiffer', 'Thomas', + 'Xavier', 'Zhao', 'Bordeaux', 'Dumb', 'Knight', 'Wright', 'Gonzalez', + 'Sanchez', 'Nowakowski', 'Anne', 'Breaux', 'Van Gogh', 'Schwarzenegger', + 'Campbell', 'MacGregor', 'Tchaikovsky', 'Dvorak', 'Niece', 'Resnais', + 'Arnow', 'Filipowicz', 'Rogier', 'Hochmeier', 'Vasquez', 'Yankelovich' +]) AS w; + +-- Rarer branches of the encoder. Most of these are letter combinations that +-- only show up in names of a particular origin. +SELECT w, dmetaphone(w) AS dm, dmetaphone_alt(w) AS alt FROM unnest(ARRAY[ + 'McHugh', -- CH preceded by MC + 'Chopin', -- CH treated as K rather than X + 'Fuccia', -- CC followed by IA + 'Accede', -- CC followed by E + 'Bellocchio', -- CC followed by H + 'Ecclesiastes', -- CC followed by a consonant, and SIA + 'Wojciech', -- CI in a Slavic name, and a trailing CH + 'Judgment', -- DG followed by a consonant + 'Burgh', -- GH preceded by a consonant + 'Ghana', -- GH at the start of a word + 'Agnes', -- GN after an initial vowel + 'Lasagna', -- GN inside a word + 'Gigli', -- GLI in an Italian name + 'Berger', -- GER at the end of a word + 'Gergen', -- G before E, not at the start + 'Schlegel', -- G before E in a word starting with SCH + 'Gersheim', -- G before E, and SH followed by HEIM + 'Josephine', -- initial JOSE not followed by a space, and PH + 'Ajax', -- J inside a word, and X + 'Raj', -- J at the end of a word + 'Shipwright', -- SH inside a word, and WR not at the start + 'Stensholm', -- SH followed by HOLM + 'Kasian', -- SIAN in a Slavic name + 'Schenker', -- SCH followed by a consonant + 'Schooner', -- SCH followed by OO + 'Brescia', -- SC followed by I + 'Scot', -- SC followed by a consonant + 'Nation', -- TION + 'Wheeler' -- W followed by H +]) AS w; + +-- A long input +SELECT dmetaphone(repeat('schwarzenegger', 20)); + +-- Repeated GLI adds two characters to the primary code but only one to the +-- secondary one, which is what makes the code outgrow its initial buffer +SELECT dmetaphone('agliagliaglia'); + -- Wovels SELECT daitch_mokotoff('Augsburg'); SELECT daitch_mokotoff('Breuer'); @@ -60,6 +176,13 @@ SELECT daitch_mokotoff('Rosokhovatsets'); SELECT daitch_mokotoff('''OBrien'); SELECT daitch_mokotoff('O''Brien'); +-- Input with no encodable characters +SELECT daitch_mokotoff(''); +SELECT daitch_mokotoff('123'); + +-- ASCII characters reserved for internal conversions +SELECT daitch_mokotoff('a[b'); + -- "Difficult" cases, likely to cause trouble for other implementations. SELECT daitch_mokotoff('CJC'); SELECT daitch_mokotoff('BESST'); -- 2.43.0
