From e8afb7bae87cdedfff13e4f2cc3153c540a5eddc Mon Sep 17 00:00:00 2001
From: Aleksander Alekseev <aleksander@tigerdata.com>
Date: Fri, 30 Oct 2026 15:53:10 +0300
Subject: [PATCH v1] contrib/fuzzystrmatch: imporve test coverage

Author: Aleksander Alekseev <aleksander@tigerdata.com>
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

