@@ -150,5 +150,117 @@ class DuplicateRowCountTest extends AnyWordSpec with Matchers with SparkContextS
150150 val result = DuplicateRowCount (Seq (" col1" , " col2" )).calculate(df)
151151 result.value shouldBe scala.util.Success (0.0 )
152152 }
153+
154+ " correctly merge state across partitions" in withSparkSession { session =>
155+ import session .implicits ._
156+ // Partition A: ("a",1) is unique
157+ val dfA = Seq ((" a" , 1 ), (" b" , 2 )).toDF(" col1" , " col2" )
158+ // Partition B: ("a",1) appears again - now duplicate across A+B
159+ val dfB = Seq ((" a" , 1 ), (" c" , 3 )).toDF(" col1" , " col2" )
160+
161+ val analyzer = DuplicateRowCount (Seq (" col1" , " col2" ))
162+ val stateA = analyzer.computeStateFrom(dfA).get
163+ val stateB = analyzer.computeStateFrom(dfB).get
164+ val merged = stateA.sum(stateB)
165+
166+ val metric = analyzer.computeMetricFrom(Some (merged))
167+ // ("a",1) has count 2 after merge -> 2 duplicate rows
168+ metric.value shouldBe Success (2.0 )
169+ }
170+
171+ " correctly merge state with overlapping groups" in withSparkSession { session =>
172+ import session .implicits ._
173+ // Partition A: ("a",1) appears twice
174+ val dfA = Seq ((" a" , 1 ), (" a" , 1 ), (" b" , 2 )).toDF(" col1" , " col2" )
175+ // Partition B: ("a",1) appears once more
176+ val dfB = Seq ((" a" , 1 ), (" c" , 3 )).toDF(" col1" , " col2" )
177+
178+ val analyzer = DuplicateRowCount (Seq (" col1" , " col2" ))
179+ val stateA = analyzer.computeStateFrom(dfA).get
180+ val stateB = analyzer.computeStateFrom(dfB).get
181+ val merged = stateA.sum(stateB)
182+
183+ val metric = analyzer.computeMetricFrom(Some (merged))
184+ // ("a",1) has count 3 after merge -> 3 duplicate rows
185+ metric.value shouldBe Success (3.0 )
186+ }
187+
188+ " produce correct row-level results" in withSparkSession { session =>
189+ import session .implicits ._
190+ import com .amazon .deequ .{VerificationSuite , VerificationResult }
191+ import com .amazon .deequ .checks .{Check , CheckLevel , CheckStatus }
192+
193+ val df = Seq ((" a" , 1 ), (" b" , 2 ), (" a" , 1 ), (" c" , 3 )).toDF(" col1" , " col2" )
194+
195+ val result = VerificationSuite ()
196+ .onData(df)
197+ .addCheck(Check (CheckLevel .Error , " dup-check" )
198+ .hasDuplicateRowCount(Seq (" col1" , " col2" ), _ == 2 ))
199+ .run()
200+
201+ // Verify the check passes
202+ result.status shouldBe CheckStatus .Success
203+
204+ // Verify row-level results: true = duplicate, false = unique
205+ val rowLevelDf = VerificationResult .rowLevelResultsAsDataFrame(session, result, df)
206+ val flags = rowLevelDf.select(" `dup-check`" ).collect().map(_.getBoolean(0 ))
207+ // 2 rows are duplicates (true), 2 rows are unique (false)
208+ flags.count(_ == true ) shouldBe 2
209+ flags.count(_ == false ) shouldBe 2
210+ }
211+
212+ " work with empty columns through VerificationSuite" in withSparkSession { session =>
213+ import session .implicits ._
214+ import com .amazon .deequ .{VerificationSuite , VerificationResult }
215+ import com .amazon .deequ .checks .{Check , CheckLevel , CheckStatus }
216+
217+ val df = Seq ((" a" , 1 ), (" b" , 2 ), (" a" , 1 ), (" c" , 3 )).toDF(" col1" , " col2" )
218+
219+ val result = VerificationSuite ()
220+ .onData(df)
221+ .addCheck(Check (CheckLevel .Error , " dup-empty-cols" )
222+ .hasDuplicateRowCount(Seq .empty, _ == 2 ))
223+ .run()
224+
225+ // Empty columns resolves to all columns at runtime
226+ result.status shouldBe CheckStatus .Success
227+ }
228+
229+ " not crash with empty columns through constraint path" in withSparkSession { session =>
230+ import session .implicits ._
231+ import com .amazon .deequ .constraints .Constraint
232+
233+ val df = Seq ((" a" , 1 ), (" b" , 2 ), (" a" , 1 )).toDF(" col1" , " col2" )
234+ // Should not throw NoSuchElementException (NamedConstraint fallback for empty columns)
235+ val constraint = Constraint .duplicateRowCountConstraint(Seq .empty, _ == 2 )
236+ constraint should not be null
237+ }
238+
239+ " produce row-level results for empty columns through DeequRulesExecutor" in withSparkSession { session =>
240+ import session .implicits ._
241+ import com .amazon .deequ .{VerificationSuite , VerificationResult }
242+ import com .amazon .deequ .checks .{Check , CheckLevel , CheckStatus }
243+
244+ val df = Seq ((" a" , 1 ), (" b" , 2 ), (" a" , 1 ), (" c" , 3 )).toDF(" col1" , " col2" )
245+
246+ // Simulate what DeequRulesExecutor does: resolve empty columns then run
247+ val allColumns = df.columns.toSeq
248+ val result = VerificationSuite ()
249+ .onData(df)
250+ .addCheck(Check (CheckLevel .Error , " dup-resolved" )
251+ .hasDuplicateRowCount(allColumns, _ == 2 ))
252+ .run()
253+
254+ result.status shouldBe CheckStatus .Success
255+
256+ // With resolved columns, RowLevelGroupedConstraint is used -> row-level results exist
257+ val rowLevelDf = VerificationResult .rowLevelResultsAsDataFrame(session, result, df)
258+ rowLevelDf.columns should contain (" dup-resolved" )
259+
260+ // Verify flags: 2 duplicates (true), 2 unique (false)
261+ val flags = rowLevelDf.select(" `dup-resolved`" ).collect().map(_.getBoolean(0 ))
262+ flags.count(_ == true ) shouldBe 2
263+ flags.count(_ == false ) shouldBe 2
264+ }
153265 }
154266}
0 commit comments