diff --git a/CHANGELOG.md b/CHANGELOG.md
index 21858277a..f118c1d79 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Added
+- Added 12 database functions: DCOUNT, DSUM, DAVERAGE, DMAX, DMIN, DGET, DPRODUCT, DCOUNTA, DSTDEV, DSTDEVP, DVAR, DVARP. [#1652](https://github.com/handsontable/hyperformula/pull/1652)
- Added new functions: PERCENTILE, PERCENTILE.INC, PERCENTILE.EXC, QUARTILE, QUARTILE.INC, QUARTILE.EXC. [#1650](https://github.com/handsontable/hyperformula/pull/1650)
- Added `maxPendingLazyTransformations` configuration option to control memory usage by limiting accumulated transformations before cleanup. [#1629](https://github.com/handsontable/hyperformula/issues/1629)
- Added a new function: TEXTJOIN. [#1640](https://github.com/handsontable/hyperformula/pull/1640)
diff --git a/docs/guide/built-in-functions.md b/docs/guide/built-in-functions.md
index e1ad66821..b1c998e60 100644
--- a/docs/guide/built-in-functions.md
+++ b/docs/guide/built-in-functions.md
@@ -28,6 +28,7 @@ The latest version of HyperFormula has an extensive collection of
**{{ $page.functionsCount }}** functions grouped into categories:
- [Array manipulation](#array-manipulation)
+- [Database](#database)
- [Date and time](#date-and-time)
- [Engineering](#engineering)
- [Information](#information)
@@ -40,8 +41,7 @@ The latest version of HyperFormula has an extensive collection of
- [Statistical](#statistical)
- [Text](#text)
-_Some categories such as compatibility, cube, and database are yet to be
-supported._
+_Some categories such as compatibility and cube are yet to be supported._
::: tip
You can modify the built-in functions or create your own, by adding a [custom function](custom-functions).
@@ -91,6 +91,23 @@ Total number of functions: **{{ $page.functionsCount }}**
| YEAR | Returns the year as a number according to the internal calculation rules. | YEAR(Number) |
| YEARFRAC | Computes the difference between two date values, in fraction of years. | YEARFRAC(Date2, Date1[, Format]) |
+### Database
+
+| Function ID | Description | Syntax |
+|:------------|:----------------------------------------------------------------------------------------------------------------|:----------------------------------|
+| DAVERAGE | Returns the average of all values in a database field that match the given criteria. | DAVERAGE(Database, Field, Criteria) |
+| DCOUNT | Counts the cells containing numbers in a database field that match the given criteria. | DCOUNT(Database, Field, Criteria) |
+| DCOUNTA | Counts the non-empty cells in a database field that match the given criteria. | DCOUNTA(Database, Field, Criteria) |
+| DGET | Returns the single value from a database field that matches the given criteria. Returns #VALUE! if no records match, and #NUM! if more than one record matches. | DGET(Database, Field, Criteria) |
+| DMAX | Returns the maximum value in a database field that matches the given criteria. | DMAX(Database, Field, Criteria) |
+| DMIN | Returns the minimum value in a database field that matches the given criteria. | DMIN(Database, Field, Criteria) |
+| DPRODUCT | Returns the product of all values in a database field that match the given criteria. | DPRODUCT(Database, Field, Criteria) |
+| DSTDEV | Returns the sample standard deviation of all values in a database field that match the given criteria. | DSTDEV(Database, Field, Criteria) |
+| DSTDEVP | Returns the population standard deviation of all values in a database field that match the given criteria. | DSTDEVP(Database, Field, Criteria) |
+| DSUM | Returns the sum of all values in a database field that match the given criteria. | DSUM(Database, Field, Criteria) |
+| DVAR | Returns the sample variance of all values in a database field that match the given criteria. | DVAR(Database, Field, Criteria) |
+| DVARP | Returns the population variance of all values in a database field that match the given criteria. | DVARP(Database, Field, Criteria) |
+
### Engineering
| Function ID | Description | Syntax |
diff --git a/docs/guide/integration-with-angular.md b/docs/guide/integration-with-angular.md
index 8f78e2097..1e6b57998 100644
--- a/docs/guide/integration-with-angular.md
+++ b/docs/guide/integration-with-angular.md
@@ -4,6 +4,70 @@ Installing HyperFormula in an Angular application works the same as with vanilla
For more details, see the [client-side installation](client-side-installation.md) section.
+## Basic usage
+
+### Step 1. Create a service
+
+Wrap HyperFormula in an Angular service. Create the instance in the constructor and expose methods for reading data.
+
+```typescript
+import { Injectable, OnDestroy } from '@angular/core';
+import { HyperFormula } from 'hyperformula';
+
+@Injectable({ providedIn: 'root' })
+export class SpreadsheetService implements OnDestroy {
+ private hf: HyperFormula;
+ private sheetId = 0;
+
+ constructor() {
+ // Create a HyperFormula instance with initial data.
+ this.hf = HyperFormula.buildFromArray(
+ [
+ [10, 20, '=SUM(A1:B1)'],
+ [30, 40, '=SUM(A2:B2)'],
+ ],
+ { licenseKey: 'gpl-v3' }
+ );
+ }
+
+ /** Return calculated values for the entire sheet. */
+ getSheetValues(): (number | string | null)[][] {
+ return this.hf.getSheetValues(this.sheetId);
+ }
+
+ ngOnDestroy(): void {
+ this.hf.destroy();
+ }
+}
+```
+
+### Step 2. Use the service in a component
+
+Inject the service and display the calculated values.
+
+```typescript
+import { Component } from '@angular/core';
+import { SpreadsheetService } from './spreadsheet.service';
+
+@Component({
+ selector: 'app-spreadsheet',
+ template: `
+
+
+
{{ cell }}
+
+
+ `,
+})
+export class SpreadsheetComponent {
+ data: (number | string | null)[][];
+
+ constructor(private spreadsheet: SpreadsheetService) {
+ this.data = this.spreadsheet.getSheetValues();
+ }
+}
+```
+
## Demo
Explore the full working example on [Stackblitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/angular-demo?v=${$page.buildDateURIEncoded}).
diff --git a/docs/guide/integration-with-react.md b/docs/guide/integration-with-react.md
index d4bc7fe75..1591aec85 100644
--- a/docs/guide/integration-with-react.md
+++ b/docs/guide/integration-with-react.md
@@ -4,6 +4,62 @@ Installing HyperFormula in a React application works the same as with vanilla Ja
For more details, see the [client-side installation](client-side-installation.md) section.
+## Basic usage
+
+### Step 1. Initialize HyperFormula
+
+Use `useRef` to hold the HyperFormula instance so it persists across re-renders. Initialize it inside a `useEffect` hook.
+
+```javascript
+import { useRef, useEffect, useState } from 'react';
+import { HyperFormula } from 'hyperformula';
+
+function SpreadsheetComponent() {
+ const hfRef = useRef(null);
+ const [sheetData, setSheetData] = useState([]);
+
+ useEffect(() => {
+ // Create a HyperFormula instance with initial data.
+ hfRef.current = HyperFormula.buildFromArray(
+ [
+ [10, 20, '=SUM(A1:B1)'],
+ [30, 40, '=SUM(A2:B2)'],
+ ],
+ { licenseKey: 'gpl-v3' }
+ );
+
+ // Read calculated values from the sheet.
+ const sheetId = 0;
+ setSheetData(hfRef.current.getSheetValues(sheetId));
+
+ return () => {
+ // Clean up the instance when the component unmounts.
+ hfRef.current?.destroy();
+ };
+ }, []);
+```
+
+### Step 2. Render the results
+
+Display the calculated values in a table.
+
+```javascript
+ return (
+
+
+ {sheetData.map((row, rowIdx) => (
+
+ {row.map((cell, colIdx) => (
+
{cell}
+ ))}
+
+ ))}
+
+
+ );
+}
+```
+
## Demo
Explore the full working example on [Stackblitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/react-demo?v=${$page.buildDateURIEncoded}).
diff --git a/docs/guide/integration-with-svelte.md b/docs/guide/integration-with-svelte.md
index 8b3a5f4b6..bd8aff85d 100644
--- a/docs/guide/integration-with-svelte.md
+++ b/docs/guide/integration-with-svelte.md
@@ -4,6 +4,52 @@ Installing HyperFormula in a Svelte application works the same as with vanilla J
For more details, see the [client-side installation](client-side-installation.md) section.
+## Basic usage
+
+### Step 1. Initialize HyperFormula
+
+Create the HyperFormula instance directly in the component's `
+```
+
+### Step 2. Render the results
+
+Display the data in a table and trigger calculation with a button.
+
+```html
+
+
+
+ {#each data as row, rowIdx}
+
+ {#each row as cell, colIdx}
+
{cell}
+ {/each}
+
+ {/each}
+
+```
+
## Demo
Explore the full working example on [Stackblitz](https://stackblitz.com/github/handsontable/hyperformula-demos/tree/3.2.x/svelte-demo?v=${$page.buildDateURIEncoded}).
diff --git a/docs/guide/integration-with-vue.md b/docs/guide/integration-with-vue.md
index eaa104e0e..68afa5a8e 100644
--- a/docs/guide/integration-with-vue.md
+++ b/docs/guide/integration-with-vue.md
@@ -4,6 +4,48 @@ Installing HyperFormula in a Vue application works the same as with vanilla Java
For more details, see the [client-side installation](client-side-installation.md) section.
+## Basic usage
+
+### Step 1. Initialize HyperFormula
+
+Wrap the HyperFormula instance in `markRaw` to prevent Vue's reactivity system from converting it into a proxy (see [Troubleshooting](#vue-reactivity-issues) below). Use `ref` for the data you want to display.
+
+```javascript
+
+```
+
+### Step 2. Render the results
+
+Display the calculated values in a template.
+
+```html
+
+
+
+
{{ cell }}
+
+
+
+```
+
## Troubleshooting
### Vue reactivity issues
diff --git a/docs/guide/known-limitations.md b/docs/guide/known-limitations.md
index 72753b9b2..6b628db94 100644
--- a/docs/guide/known-limitations.md
+++ b/docs/guide/known-limitations.md
@@ -38,3 +38,5 @@ you can't compare the arguments in a formula like this:
* The INDEX function doesn't support returning whole rows or columns of the source range – it always returns the contents of a single cell.
* The FILTER function accepts either single rows of equal width or single columns of equal height. In other words, all arrays passed to the FILTER function must have equal dimensions, and at least one of those dimensions must be 1.
* Array-producing functions (e.g., SEQUENCE, FILTER) require their output dimensions to be determinable at parse time. Passing cell references or formulas as dimension arguments (e.g., `=SEQUENCE(A1)`) results in a `#VALUE!` error, because the output size cannot be resolved before evaluation.
+* DGET returns `#VALUE!` when no records match the criteria, and `#NUM!` when more than one record matches.
+* DMAX, DMIN, and DPRODUCT return `0` when no records match the criteria.
diff --git a/src/i18n/languages/csCZ.ts b/src/i18n/languages/csCZ.ts
index 463bb1d2a..71ce56b14 100644
--- a/src/i18n/languages/csCZ.ts
+++ b/src/i18n/languages/csCZ.ts
@@ -75,6 +75,18 @@ const dictionary: RawTranslationPackage = {
DAYS360: 'ROK360',
DAYS: 'DAYS',
DB: 'ODPIS.ZRYCH',
+ DAVERAGE: 'DPRŮMĚR',
+ DCOUNT: 'DPOČET',
+ DCOUNTA: 'DPOČET2',
+ DGET: 'DZÍSKAT',
+ DMAX: 'DMAX',
+ DMIN: 'DMIN',
+ DPRODUCT: 'DSOUČIN',
+ DSTDEV: 'DSMODCH.VÝBĚR',
+ DSTDEVP: 'DSMODCH',
+ DSUM: 'DSUMA',
+ DVAR: 'DVAR.VÝBĚR',
+ DVARP: 'DVAR',
DDB: 'ODPIS.ZRYCH2',
DEC2BIN: 'DEC2BIN',
DEC2HEX: 'DEC2HEX',
diff --git a/src/i18n/languages/daDK.ts b/src/i18n/languages/daDK.ts
index 6525c442a..ea5a1e4ce 100644
--- a/src/i18n/languages/daDK.ts
+++ b/src/i18n/languages/daDK.ts
@@ -75,6 +75,18 @@ const dictionary: RawTranslationPackage = {
DAYS360: 'DAGE360',
DAYS: 'DAGE',
DB: 'DB',
+ DAVERAGE: 'DMIDDEL',
+ DCOUNT: 'DTÆL',
+ DCOUNTA: 'DTÆLV',
+ DGET: 'DHENT',
+ DMAX: 'DMAKS',
+ DMIN: 'DMIN',
+ DPRODUCT: 'DPRODUKT',
+ DSTDEV: 'DSTDAFV',
+ DSTDEVP: 'DSTDAFVP',
+ DSUM: 'DSUM',
+ DVAR: 'DVARIANS',
+ DVARP: 'DVARIANSP',
DDB: 'DSA',
DEC2BIN: 'DEC.TIL.BIN',
DEC2HEX: 'DEC.TIL.HEX',
diff --git a/src/i18n/languages/deDE.ts b/src/i18n/languages/deDE.ts
index d99d6cc1c..24a8ec03b 100644
--- a/src/i18n/languages/deDE.ts
+++ b/src/i18n/languages/deDE.ts
@@ -75,6 +75,18 @@ const dictionary: RawTranslationPackage = {
DAYS360: 'TAGE360',
DAYS: 'TAGE',
DB: 'GDA2',
+ DAVERAGE: 'DBMITTELWERT',
+ DCOUNT: 'DBANZAHL',
+ DCOUNTA: 'DBANZAHL2',
+ DGET: 'DBAUSZUG',
+ DMAX: 'DBMAX',
+ DMIN: 'DBMIN',
+ DPRODUCT: 'DBPRODUKT',
+ DSTDEV: 'DBSTDABW',
+ DSTDEVP: 'DBSTDABWN',
+ DSUM: 'DBSUMME',
+ DVAR: 'DBVARIANZ',
+ DVARP: 'DBVARIANZEN',
DDB: 'GDA',
DEC2BIN: 'DEZINBIN',
DEC2HEX: 'DEZINHEX',
diff --git a/src/i18n/languages/enGB.ts b/src/i18n/languages/enGB.ts
index 840e14a90..e878f897a 100644
--- a/src/i18n/languages/enGB.ts
+++ b/src/i18n/languages/enGB.ts
@@ -76,6 +76,18 @@ const dictionary: RawTranslationPackage = {
DAYS360: 'DAYS360',
DAYS: 'DAYS',
DB: 'DB',
+ DAVERAGE: 'DAVERAGE',
+ DCOUNT: 'DCOUNT',
+ DCOUNTA: 'DCOUNTA',
+ DGET: 'DGET',
+ DMAX: 'DMAX',
+ DMIN: 'DMIN',
+ DPRODUCT: 'DPRODUCT',
+ DSTDEV: 'DSTDEV',
+ DSTDEVP: 'DSTDEVP',
+ DSUM: 'DSUM',
+ DVAR: 'DVAR',
+ DVARP: 'DVARP',
DDB: 'DDB',
DEC2BIN: 'DEC2BIN',
DEC2HEX: 'DEC2HEX',
diff --git a/src/i18n/languages/esES.ts b/src/i18n/languages/esES.ts
index 1d8278ca5..d36d14fd0 100644
--- a/src/i18n/languages/esES.ts
+++ b/src/i18n/languages/esES.ts
@@ -75,6 +75,18 @@ export const dictionary: RawTranslationPackage = {
DAYS360: 'DIAS360',
DAYS: 'DÍAS',
DB: 'DB',
+ DAVERAGE: 'BDPROMEDIO',
+ DCOUNT: 'BDCONTAR',
+ DCOUNTA: 'BDCONTARA',
+ DGET: 'BDEXTRAER',
+ DMAX: 'BDMAX',
+ DMIN: 'BDMIN',
+ DPRODUCT: 'BDPRODUCTO',
+ DSTDEV: 'BDDESVEST',
+ DSTDEVP: 'BDDESVESTP',
+ DSUM: 'BDSUMA',
+ DVAR: 'BDVAR',
+ DVARP: 'BDVARP',
DDB: 'DDB',
DEC2BIN: 'DEC.A.BIN',
DEC2HEX: 'DEC.A.HEX',
diff --git a/src/i18n/languages/fiFI.ts b/src/i18n/languages/fiFI.ts
index cd11bc7ec..5735278d8 100644
--- a/src/i18n/languages/fiFI.ts
+++ b/src/i18n/languages/fiFI.ts
@@ -75,6 +75,18 @@ const dictionary: RawTranslationPackage = {
DAYS360: 'PÄIVÄT360',
DAYS: 'PV',
DB: 'DB',
+ DAVERAGE: 'TKESKIARVO',
+ DCOUNT: 'TLASKE',
+ DCOUNTA: 'TLASKE.A',
+ DGET: 'TNOUDA',
+ DMAX: 'TMAKS',
+ DMIN: 'TMIN',
+ DPRODUCT: 'TTULO',
+ DSTDEV: 'TKESKIHAJONTA',
+ DSTDEVP: 'TKESKIHAJONTAP',
+ DSUM: 'TSUMMA',
+ DVAR: 'TVARIANSSI',
+ DVARP: 'TVARIANSSIP',
DDB: 'DDB',
DEC2BIN: 'DESBIN',
DEC2HEX: 'DESHEKSA',
diff --git a/src/i18n/languages/frFR.ts b/src/i18n/languages/frFR.ts
index ae1dd4389..044c70715 100644
--- a/src/i18n/languages/frFR.ts
+++ b/src/i18n/languages/frFR.ts
@@ -75,6 +75,18 @@ const dictionary: RawTranslationPackage = {
DAYS360: 'JOURS360',
DAYS: 'JOURS',
DB: 'DB',
+ DAVERAGE: 'BDMOYENNE',
+ DCOUNT: 'BDNB',
+ DCOUNTA: 'BDNBVAL',
+ DGET: 'BDLIRE',
+ DMAX: 'BDMAX',
+ DMIN: 'BDMIN',
+ DPRODUCT: 'BDPRODUIT',
+ DSTDEV: 'BDECARTYPE',
+ DSTDEVP: 'BDECARTYPEP',
+ DSUM: 'BDSOMME',
+ DVAR: 'BDVAR',
+ DVARP: 'BDVARP',
DDB: 'DDB',
DEC2BIN: 'DECBIN',
DEC2HEX: 'DECHEX',
diff --git a/src/i18n/languages/huHU.ts b/src/i18n/languages/huHU.ts
index 223f12811..3a5119222 100644
--- a/src/i18n/languages/huHU.ts
+++ b/src/i18n/languages/huHU.ts
@@ -75,6 +75,18 @@ const dictionary: RawTranslationPackage = {
DAYS360: 'DAYS360',
DAYS: 'NAPOK',
DB: 'DB',
+ DAVERAGE: 'AB.ÁTLAG',
+ DCOUNT: 'AB.DARAB',
+ DCOUNTA: 'AB.DARAB2',
+ DGET: 'AB.MEZŐ',
+ DMAX: 'AB.MAX',
+ DMIN: 'AB.MIN',
+ DPRODUCT: 'AB.SZORZAT',
+ DSTDEV: 'AB.SZÓRÁS',
+ DSTDEVP: 'AB.SZÓRÁS2',
+ DSUM: 'AB.SZUM',
+ DVAR: 'AB.VAR',
+ DVARP: 'AB.VAR2',
DDB: 'KCSA',
DEC2BIN: 'DEC.BIN',
DEC2HEX: 'DEC.HEX',
diff --git a/src/i18n/languages/itIT.ts b/src/i18n/languages/itIT.ts
index 1ebbaa255..2716d3471 100644
--- a/src/i18n/languages/itIT.ts
+++ b/src/i18n/languages/itIT.ts
@@ -75,6 +75,18 @@ const dictionary: RawTranslationPackage = {
DAYS360: 'GIORNO360',
DAYS: 'GIORNI',
DB: 'AMMORT.FISSO',
+ DAVERAGE: 'DB.MEDIA',
+ DCOUNT: 'DB.CONTA.NUMERI',
+ DCOUNTA: 'DB.CONTA.VALORI',
+ DGET: 'DB.VALORI',
+ DMAX: 'DB.MAX',
+ DMIN: 'DB.MIN',
+ DPRODUCT: 'DB.PRODOTTO',
+ DSTDEV: 'DB.DEV.ST',
+ DSTDEVP: 'DB.DEV.ST.POP',
+ DSUM: 'DB.SOMMA',
+ DVAR: 'DB.VAR',
+ DVARP: 'DB.VAR.POP',
DDB: 'AMMORT',
DEC2BIN: 'DECIMALE.BINARIO',
DEC2HEX: 'DECIMALE.HEX',
diff --git a/src/i18n/languages/nbNO.ts b/src/i18n/languages/nbNO.ts
index 9ec608493..80d9948f6 100644
--- a/src/i18n/languages/nbNO.ts
+++ b/src/i18n/languages/nbNO.ts
@@ -75,6 +75,18 @@ const dictionary: RawTranslationPackage = {
DAYS360: 'DAGER360',
DAYS: 'DAGER',
DB: 'DAVSKR',
+ DAVERAGE: 'DGJENNOMSNITT',
+ DCOUNT: 'DANTALL',
+ DCOUNTA: 'DANTALLA',
+ DGET: 'DHENT',
+ DMAX: 'DMAKS',
+ DMIN: 'DMIN',
+ DPRODUCT: 'DPRODUKT',
+ DSTDEV: 'DSTDAV',
+ DSTDEVP: 'DSTDAVP',
+ DSUM: 'DSUMMER',
+ DVAR: 'DVARIANS',
+ DVARP: 'DVARIANSP',
DDB: 'DEGRAVS',
DEC2BIN: 'DESTILBIN',
DEC2HEX: 'DESTILHEKS',
diff --git a/src/i18n/languages/nlNL.ts b/src/i18n/languages/nlNL.ts
index 201318236..57d6fddb9 100644
--- a/src/i18n/languages/nlNL.ts
+++ b/src/i18n/languages/nlNL.ts
@@ -75,6 +75,18 @@ const dictionary: RawTranslationPackage = {
DAYS360: 'DAGEN360',
DAYS: 'DAGEN',
DB: 'DB',
+ DAVERAGE: 'DBGEMIDDELDE',
+ DCOUNT: 'DBAANTAL',
+ DCOUNTA: 'DBAANTALC',
+ DGET: 'DBLEZEN',
+ DMAX: 'DBMAX',
+ DMIN: 'DBMIN',
+ DPRODUCT: 'DBPRODUKT',
+ DSTDEV: 'DBSTDAFWIJKING',
+ DSTDEVP: 'DBSTDAFWIJKINGP',
+ DSUM: 'DBSOM',
+ DVAR: 'DBVAR',
+ DVARP: 'DBVARP',
DDB: 'DDB',
DEC2BIN: 'DEC.N.BIN',
DEC2HEX: 'DEC.N.HEX',
diff --git a/src/i18n/languages/plPL.ts b/src/i18n/languages/plPL.ts
index acce07114..251a35bba 100644
--- a/src/i18n/languages/plPL.ts
+++ b/src/i18n/languages/plPL.ts
@@ -75,6 +75,18 @@ const dictionary: RawTranslationPackage = {
DAYS360: 'DNI.360',
DAYS: 'DNI',
DB: 'DB',
+ DAVERAGE: 'BD.ŚREDNIA',
+ DCOUNT: 'BD.ILE.REKORDÓW',
+ DCOUNTA: 'BD.ILE.REKORDÓW.A',
+ DGET: 'BD.POLE',
+ DMAX: 'BD.MAX',
+ DMIN: 'BD.MIN',
+ DPRODUCT: 'BD.ILOCZYN',
+ DSTDEV: 'BD.ODCH.STANDARD',
+ DSTDEVP: 'BD.ODCH.STANDARD.POPUL',
+ DSUM: 'BD.SUMA',
+ DVAR: 'BD.WARIANCJA',
+ DVARP: 'BD.WARIANCJA.POPUL',
DDB: 'DDB',
DEC2BIN: 'DZIES.NA.DWÓJK',
DEC2HEX: 'DZIES.NA.SZESN',
diff --git a/src/i18n/languages/ptPT.ts b/src/i18n/languages/ptPT.ts
index a161a5497..3195612f0 100644
--- a/src/i18n/languages/ptPT.ts
+++ b/src/i18n/languages/ptPT.ts
@@ -75,6 +75,18 @@ const dictionary: RawTranslationPackage = {
DAYS360: 'DIAS360',
DAYS: 'DIAS',
DB: 'BD',
+ DAVERAGE: 'BDMÉDIA',
+ DCOUNT: 'BDCONTAR',
+ DCOUNTA: 'BDCONTAR.VAL',
+ DGET: 'BDOBTER',
+ DMAX: 'BDMÁX',
+ DMIN: 'BDMÍN',
+ DPRODUCT: 'BDMULTIPL',
+ DSTDEV: 'BDDESVPAD',
+ DSTDEVP: 'BDDESVPADP',
+ DSUM: 'BDSOMA',
+ DVAR: 'BDVAR',
+ DVARP: 'BDVARP',
DDB: 'BDD',
DEC2BIN: 'DECABIN',
DEC2HEX: 'DECAHEX',
diff --git a/src/i18n/languages/ruRU.ts b/src/i18n/languages/ruRU.ts
index 8d07da729..654b28a50 100644
--- a/src/i18n/languages/ruRU.ts
+++ b/src/i18n/languages/ruRU.ts
@@ -75,6 +75,18 @@ const dictionary: RawTranslationPackage = {
DAYS360: 'ДНЕЙ360',
DAYS: 'ДНИ',
DB: 'ФУО',
+ DAVERAGE: 'БДСРЕДНЕЕ',
+ DCOUNT: 'БСЧЁТ',
+ DCOUNTA: 'БСЧЕТА',
+ DGET: 'БИЗВЛЕЧЬ',
+ DMAX: 'БДМАКС',
+ DMIN: 'БДМИН',
+ DPRODUCT: 'БДПРОИЗВЕД',
+ DSTDEV: 'БДСТАНДОТКЛ',
+ DSTDEVP: 'БДСТАНДОТКЛП',
+ DSUM: 'БДСУММ',
+ DVAR: 'БДДИСП',
+ DVARP: 'БДДИСПП',
DDB: 'ДДОБ',
DEC2BIN: 'ДЕС.В.ДВ',
DEC2HEX: 'ДЕС.В.ШЕСТН',
diff --git a/src/i18n/languages/svSE.ts b/src/i18n/languages/svSE.ts
index c92269c33..bce2c4cf8 100644
--- a/src/i18n/languages/svSE.ts
+++ b/src/i18n/languages/svSE.ts
@@ -75,6 +75,18 @@ const dictionary: RawTranslationPackage = {
DAYS360: 'DAGAR360',
DAYS: 'DAYS',
DB: 'DB',
+ DAVERAGE: 'DMEDEL',
+ DCOUNT: 'DANTAL',
+ DCOUNTA: 'DANTALV',
+ DGET: 'DHÄMTA',
+ DMAX: 'DMAX',
+ DMIN: 'DMIN',
+ DPRODUCT: 'DPRODUKT',
+ DSTDEV: 'DSTDAV',
+ DSTDEVP: 'DSTDAVP',
+ DSUM: 'DSUMMA',
+ DVAR: 'DVARIANS',
+ DVARP: 'DVARIANSP',
DDB: 'DEGAVSKR',
DEC2BIN: 'DEC.TILL.BIN',
DEC2HEX: 'DEC.TILL.HEX',
diff --git a/src/i18n/languages/trTR.ts b/src/i18n/languages/trTR.ts
index 417b96bb9..af2c25390 100644
--- a/src/i18n/languages/trTR.ts
+++ b/src/i18n/languages/trTR.ts
@@ -75,6 +75,18 @@ const dictionary: RawTranslationPackage = {
DAYS360: 'GÜN360',
DAYS: 'GÜNSAY',
DB: 'AZALANBAKİYE',
+ DAVERAGE: 'VSEÇORT',
+ DCOUNT: 'VSEÇSAY',
+ DCOUNTA: 'VSEÇSAYDOLU',
+ DGET: 'VAL',
+ DMAX: 'VSEÇMAK',
+ DMIN: 'VSEÇMİN',
+ DPRODUCT: 'VSEÇÇARP',
+ DSTDEV: 'VSEÇSTDSAPMA',
+ DSTDEVP: 'VSEÇSTDSAPMAS',
+ DSUM: 'VSEÇTOPLA',
+ DVAR: 'VSEÇVAR',
+ DVARP: 'VSEÇVARS',
DDB: 'ÇİFTAZALANBAKİYE',
DEC2BIN: 'DEC2BIN',
DEC2HEX: 'DEC2HEX',
diff --git a/src/interpreter/plugin/DatabasePlugin.ts b/src/interpreter/plugin/DatabasePlugin.ts
new file mode 100644
index 000000000..91d3c8270
--- /dev/null
+++ b/src/interpreter/plugin/DatabasePlugin.ts
@@ -0,0 +1,587 @@
+/**
+ * @license
+ * Copyright (c) 2025 Handsoncode. All rights reserved.
+ */
+
+import {CellError, ErrorType} from '../../Cell'
+import {ErrorMessage} from '../../error-message'
+import {ProcedureAst} from '../../parser'
+import {InterpreterState} from '../InterpreterState'
+import {EmptyValue, getRawValue, InternalScalarValue, InterpreterValue, isExtendedNumber, RawScalarValue} from '../InterpreterValue'
+import {SimpleRangeValue} from '../../SimpleRangeValue'
+import {FunctionArgumentType, FunctionPlugin, FunctionPluginTypecheck, ImplementedFunctions} from './FunctionPlugin'
+import {CriterionLambda} from '../Criterion'
+
+/**
+ * Parsed criterion for a single cell in the criteria range.
+ * Maps a database column index to a matching lambda.
+ */
+interface DatabaseCriterionEntry {
+ /** 0-based column index within the database range. */
+ columnIndex: number,
+ /** Lambda that tests whether a raw cell value satisfies the criterion. */
+ lambda: CriterionLambda,
+}
+
+/**
+ * A single criteria row is a list of AND-ed criterion entries.
+ * Multiple criteria rows are OR-ed together.
+ */
+type DatabaseCriteriaRow = DatabaseCriterionEntry[]
+
+/** Shared parameter signature for all database functions: (database RANGE, field SCALAR, criteria RANGE). */
+const databaseFunctionParameters = [
+ {argumentType: FunctionArgumentType.RANGE},
+ {argumentType: FunctionArgumentType.SCALAR},
+ {argumentType: FunctionArgumentType.RANGE},
+]
+
+/**
+ * Interpreter plugin implementing Excel database functions.
+ *
+ * Implements: DAVERAGE, DCOUNT, DCOUNTA, DGET, DMAX, DMIN, DPRODUCT, DSTDEV, DSTDEVP, DSUM, DVAR, DVARP.
+ */
+export class DatabasePlugin extends FunctionPlugin implements FunctionPluginTypecheck {
+
+ public static implementedFunctions: ImplementedFunctions = {
+ 'DCOUNT': {method: 'dcount', parameters: databaseFunctionParameters},
+ 'DCOUNTA': {method: 'dcounta', parameters: databaseFunctionParameters},
+ 'DPRODUCT': {method: 'dproduct', parameters: databaseFunctionParameters},
+ 'DSTDEV': {method: 'dstdev', parameters: databaseFunctionParameters},
+ 'DSTDEVP': {method: 'dstdevp', parameters: databaseFunctionParameters},
+ 'DSUM': {method: 'dsum', parameters: databaseFunctionParameters},
+ 'DVAR': {method: 'dvar', parameters: databaseFunctionParameters},
+ 'DVARP': {method: 'dvarp', parameters: databaseFunctionParameters},
+ 'DAVERAGE': {method: 'daverage', parameters: databaseFunctionParameters},
+ 'DGET': {method: 'dget', parameters: databaseFunctionParameters},
+ 'DMAX': {method: 'dmax', parameters: databaseFunctionParameters},
+ 'DMIN': {method: 'dmin', parameters: databaseFunctionParameters},
+ }
+
+ /**
+ * Resolves field index and criteria, then delegates to the callback with the parsed database arguments.
+ * Shared boilerplate for all 12 database functions.
+ */
+ private withDatabaseArgs(
+ ast: ProcedureAst,
+ state: InterpreterState,
+ functionName: string,
+ callback: (dbData: InternalScalarValue[][], fieldIndex: number, criteriaRows: DatabaseCriteriaRow[]) => InternalScalarValue
+ ): InterpreterValue {
+ return this.runFunction(ast.args, state, this.metadata(functionName),
+ (database: SimpleRangeValue, field: RawScalarValue, criteria: SimpleRangeValue) => {
+ const fieldIndex = this.resolveFieldIndex(database, field)
+ if (fieldIndex instanceof CellError) {
+ return fieldIndex
+ }
+
+ const criteriaRows = this.buildDatabaseCriteria(database, criteria)
+ if (criteriaRows instanceof CellError) {
+ return criteriaRows
+ }
+
+ return callback(database.data, fieldIndex, criteriaRows)
+ })
+ }
+
+ /**
+ * Counts cells containing numbers in the specified field of a database range,
+ * for rows that match all criteria.
+ *
+ * DCOUNT(database, field, criteria)
+ */
+ public dcount(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
+ return this.withDatabaseArgs(ast, state, 'DCOUNT', (dbData, fieldIndex, criteriaRows) => {
+ let count = 0
+
+ for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) {
+ if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) {
+ const cellValue = dbData[rowIdx][fieldIndex]
+ if (cellValue instanceof CellError) {
+ return cellValue
+ }
+ if (isExtendedNumber(cellValue)) {
+ count++
+ }
+ }
+ }
+
+ return count
+ })
+ }
+
+ /**
+ * Counts all non-blank cells in the specified field of a database range,
+ * for rows that match all criteria.
+ *
+ * DCOUNTA(database, field, criteria)
+ */
+ public dcounta(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
+ return this.withDatabaseArgs(ast, state, 'DCOUNTA', (dbData, fieldIndex, criteriaRows) => {
+ let count = 0
+
+ for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) {
+ if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) {
+ const cellValue = dbData[rowIdx][fieldIndex]
+ if (cellValue instanceof CellError) {
+ return cellValue
+ }
+ if (cellValue !== EmptyValue && cellValue !== undefined && cellValue !== null) {
+ count++
+ }
+ }
+ }
+
+ return count
+ })
+ }
+
+ /**
+ * Returns the product of numeric values in the specified field of a database range,
+ * for rows that match all criteria.
+ *
+ * DPRODUCT(database, field, criteria)
+ */
+ public dproduct(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
+ return this.withDatabaseArgs(ast, state, 'DPRODUCT', (dbData, fieldIndex, criteriaRows) => {
+ let product = 1
+ let hasNumeric = false
+
+ for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) {
+ if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) {
+ const cellValue = dbData[rowIdx][fieldIndex]
+ if (cellValue instanceof CellError) {
+ return cellValue
+ }
+ if (isExtendedNumber(cellValue)) {
+ product *= getRawValue(cellValue)
+ hasNumeric = true
+ }
+ }
+ }
+
+ return hasNumeric ? product : 0
+ })
+ }
+
+ /**
+ * Returns the sum of numeric values in the specified field of a database range,
+ * for rows that match all criteria.
+ *
+ * DSUM(database, field, criteria)
+ */
+ public dsum(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
+ return this.withDatabaseArgs(ast, state, 'DSUM', (dbData, fieldIndex, criteriaRows) => {
+ let sum = 0
+
+ for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) {
+ if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) {
+ const cellValue = dbData[rowIdx][fieldIndex]
+ if (cellValue instanceof CellError) {
+ return cellValue
+ }
+ if (isExtendedNumber(cellValue)) {
+ sum += getRawValue(cellValue)
+ }
+ }
+ }
+
+ return sum
+ })
+ }
+
+ /**
+ * Returns the average of numeric values in the specified field of a database range,
+ * for rows that match all criteria.
+ * Returns #DIV/0! when no numeric values are found.
+ *
+ * DAVERAGE(database, field, criteria)
+ */
+ public daverage(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
+ return this.withDatabaseArgs(ast, state, 'DAVERAGE', (dbData, fieldIndex, criteriaRows) => {
+ let sum = 0
+ let count = 0
+
+ for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) {
+ if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) {
+ const cellValue = dbData[rowIdx][fieldIndex]
+ if (cellValue instanceof CellError) {
+ return cellValue
+ }
+ if (isExtendedNumber(cellValue)) {
+ sum += getRawValue(cellValue)
+ count++
+ }
+ }
+ }
+
+ if (count === 0) {
+ return new CellError(ErrorType.DIV_BY_ZERO)
+ }
+
+ return sum / count
+ })
+ }
+
+ /**
+ * Returns a single value from the specified field of a database range,
+ * for the row that matches all criteria.
+ * Returns #VALUE! if no rows match, #NUM! if more than one row matches.
+ *
+ * DGET(database, field, criteria)
+ */
+ public dget(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
+ return this.withDatabaseArgs(ast, state, 'DGET', (dbData, fieldIndex, criteriaRows) => {
+ let matchedValue: InternalScalarValue | undefined
+ let matchCount = 0
+
+ for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) {
+ if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) {
+ matchCount++
+ if (matchCount > 1) {
+ return new CellError(ErrorType.NUM, ErrorMessage.ValueLarge)
+ }
+ matchedValue = dbData[rowIdx][fieldIndex]
+ }
+ }
+
+ if (matchCount === 0) {
+ return new CellError(ErrorType.VALUE, ErrorMessage.WrongType)
+ }
+
+ if (matchedValue instanceof CellError) {
+ return matchedValue
+ }
+
+ return matchedValue === EmptyValue || matchedValue === undefined || matchedValue === null
+ ? 0
+ : matchedValue
+ })
+ }
+
+ /**
+ * Returns the maximum numeric value in the specified field of a database range,
+ * for rows that match all criteria.
+ * Returns 0 when no numeric values are found (Excel behavior).
+ *
+ * DMAX(database, field, criteria)
+ */
+ public dmax(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
+ return this.withDatabaseArgs(ast, state, 'DMAX', (dbData, fieldIndex, criteriaRows) => {
+ let max = -Infinity
+ let hasNumeric = false
+
+ for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) {
+ if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) {
+ const cellValue = dbData[rowIdx][fieldIndex]
+ if (cellValue instanceof CellError) {
+ return cellValue
+ }
+ if (isExtendedNumber(cellValue)) {
+ const numValue = getRawValue(cellValue)
+ if (numValue > max) {
+ max = numValue
+ }
+ hasNumeric = true
+ }
+ }
+ }
+
+ return hasNumeric ? max : 0
+ })
+ }
+
+ /**
+ * Returns the minimum numeric value in the specified field of a database range,
+ * for rows that match all criteria.
+ * Returns 0 when no numeric values are found (Excel behavior).
+ *
+ * DMIN(database, field, criteria)
+ */
+ public dmin(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
+ return this.withDatabaseArgs(ast, state, 'DMIN', (dbData, fieldIndex, criteriaRows) => {
+ let min = Infinity
+ let hasNumeric = false
+
+ for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) {
+ if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) {
+ const cellValue = dbData[rowIdx][fieldIndex]
+ if (cellValue instanceof CellError) {
+ return cellValue
+ }
+ if (isExtendedNumber(cellValue)) {
+ const numValue = getRawValue(cellValue)
+ if (numValue < min) {
+ min = numValue
+ }
+ hasNumeric = true
+ }
+ }
+ }
+
+ return hasNumeric ? min : 0
+ })
+ }
+
+ /**
+ * Returns the sample standard deviation of numeric values in the specified field
+ * of a database range, for rows that match all criteria.
+ * Returns #DIV/0! when fewer than 2 numeric values are found.
+ *
+ * DSTDEV(database, field, criteria)
+ */
+ public dstdev(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
+ return this.withDatabaseArgs(ast, state, 'DSTDEV', (dbData, fieldIndex, criteriaRows) => {
+ const values = this.collectNumericValues(dbData, fieldIndex, criteriaRows)
+ if (values instanceof CellError) {
+ return values
+ }
+
+ if (values.length <= 1) {
+ return new CellError(ErrorType.DIV_BY_ZERO)
+ }
+
+ const mean = values.reduce((a, b) => a + b, 0) / values.length
+ const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / (values.length - 1)
+ return Math.sqrt(variance)
+ })
+ }
+
+ /**
+ * Returns the population standard deviation of numeric values in the specified field
+ * of a database range, for rows that match all criteria.
+ * Returns #DIV/0! when no numeric values are found.
+ * Returns 0 when exactly one numeric value is found.
+ *
+ * DSTDEVP(database, field, criteria)
+ */
+ public dstdevp(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
+ return this.withDatabaseArgs(ast, state, 'DSTDEVP', (dbData, fieldIndex, criteriaRows) => {
+ const values = this.collectNumericValues(dbData, fieldIndex, criteriaRows)
+ if (values instanceof CellError) {
+ return values
+ }
+
+ if (values.length === 0) {
+ return new CellError(ErrorType.DIV_BY_ZERO)
+ }
+
+ const mean = values.reduce((a, b) => a + b, 0) / values.length
+ const variance = values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length
+ return Math.sqrt(variance)
+ })
+ }
+
+ /**
+ * Returns the sample variance of numeric values in the specified field
+ * of a database range, for rows that match all criteria.
+ * Returns #DIV/0! when fewer than 2 numeric values are found.
+ *
+ * DVAR(database, field, criteria)
+ */
+ public dvar(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
+ return this.withDatabaseArgs(ast, state, 'DVAR', (dbData, fieldIndex, criteriaRows) => {
+ const values = this.collectNumericValues(dbData, fieldIndex, criteriaRows)
+ if (values instanceof CellError) {
+ return values
+ }
+
+ if (values.length <= 1) {
+ return new CellError(ErrorType.DIV_BY_ZERO)
+ }
+
+ const mean = values.reduce((a, b) => a + b, 0) / values.length
+ return values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / (values.length - 1)
+ })
+ }
+
+ /**
+ * Returns the population variance of numeric values in the specified field
+ * of a database range, for rows that match all criteria.
+ * Returns #DIV/0! when no numeric values are found.
+ * Returns 0 when exactly one numeric value is found.
+ *
+ * DVARP(database, field, criteria)
+ */
+ public dvarp(ast: ProcedureAst, state: InterpreterState): InterpreterValue {
+ return this.withDatabaseArgs(ast, state, 'DVARP', (dbData, fieldIndex, criteriaRows) => {
+ const values = this.collectNumericValues(dbData, fieldIndex, criteriaRows)
+ if (values instanceof CellError) {
+ return values
+ }
+
+ if (values.length === 0) {
+ return new CellError(ErrorType.DIV_BY_ZERO)
+ }
+
+ const mean = values.reduce((a, b) => a + b, 0) / values.length
+ return values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length
+ })
+ }
+
+ /**
+ * Collects numeric values from the specified field column of matching database rows.
+ *
+ * @param dbData - Full database data including header row.
+ * @param fieldIndex - 0-based column index of the target field.
+ * @param criteriaRows - Parsed criteria rows.
+ * @returns Array of numeric values from matching rows, or the first CellError
+ * encountered in a matching field cell (propagated per Excel semantics).
+ */
+ private collectNumericValues(
+ dbData: InternalScalarValue[][],
+ fieldIndex: number,
+ criteriaRows: DatabaseCriteriaRow[]
+ ): number[] | CellError {
+ const values: number[] = []
+
+ for (let rowIdx = 1; rowIdx < dbData.length; rowIdx++) {
+ if (this.rowMatchesCriteria(dbData[rowIdx], criteriaRows)) {
+ const cellValue = dbData[rowIdx][fieldIndex]
+ if (cellValue instanceof CellError) {
+ return cellValue
+ }
+ if (isExtendedNumber(cellValue)) {
+ values.push(getRawValue(cellValue))
+ }
+ }
+ }
+
+ return values
+ }
+
+ /**
+ * Resolves the field argument to a 0-based column index within the database range.
+ *
+ * @param database - The database range (first row = headers).
+ * @param field - A string (header name, case-insensitive) or number (1-based column index).
+ * Booleans are coerced to numbers (TRUE → 1, FALSE → 0) per Excel convention.
+ * @returns 0-based column index, or CellError if field is invalid.
+ */
+ private resolveFieldIndex(database: SimpleRangeValue, field: RawScalarValue): number | CellError {
+ if (field instanceof CellError) {
+ return field
+ }
+
+ const headers = database.data[0]
+
+ if (typeof field === 'string') {
+ const lowerField = field.toLowerCase()
+ for (let i = 0; i < headers.length; i++) {
+ const header = headers[i]
+ if (typeof header === 'string' && header.toLowerCase() === lowerField) {
+ return i
+ }
+ }
+ return new CellError(ErrorType.VALUE, ErrorMessage.WrongType)
+ }
+
+ const numericField = typeof field === 'boolean' ? Number(field) : field
+
+ if (isExtendedNumber(numericField)) {
+ const index = Math.trunc(getRawValue(numericField))
+ if (!Number.isFinite(index) || index < 1 || index > headers.length) {
+ return new CellError(ErrorType.VALUE, ErrorMessage.WrongType)
+ }
+ return index - 1
+ }
+
+ return new CellError(ErrorType.VALUE, ErrorMessage.WrongType)
+ }
+
+ /**
+ * Parses the criteria range into an array of criteria rows.
+ * Each row is a list of AND-ed conditions. Rows are OR-ed together.
+ *
+ * @param database - The database range (first row = headers).
+ * @param criteria - The criteria range (first row = header labels, subsequent rows = conditions).
+ * @returns Array of criteria rows, or CellError if a criterion cannot be parsed.
+ */
+ private buildDatabaseCriteria(database: SimpleRangeValue, criteria: SimpleRangeValue): DatabaseCriteriaRow[] | CellError {
+ const dbHeaders = database.data[0]
+ const criteriaData = criteria.data
+ const criteriaHeaders = criteriaData[0]
+
+ // Map each criteria column to a database column index (or -1 if no match)
+ const criteriaColumnMapping: number[] = criteriaHeaders.map(criteriaHeader => {
+ if (typeof criteriaHeader !== 'string') {
+ return -1
+ }
+ const lowerHeader = criteriaHeader.toLowerCase()
+ return dbHeaders.findIndex(
+ dbHeader => typeof dbHeader === 'string' && dbHeader.toLowerCase() === lowerHeader
+ )
+ })
+
+ const rows: DatabaseCriteriaRow[] = []
+
+ for (let rowIdx = 1; rowIdx < criteriaData.length; rowIdx++) {
+ const row: DatabaseCriteriaRow = []
+
+ for (let colIdx = 0; colIdx < criteriaHeaders.length; colIdx++) {
+ const dbColIndex = criteriaColumnMapping[colIdx]
+ if (dbColIndex === -1) {
+ continue // Unknown criteria header — ignore
+ }
+
+ const criterionValue = criteriaData[rowIdx]?.[colIdx]
+
+ // Empty/blank criteria cell = match-all for that column — skip
+ if (criterionValue === EmptyValue || criterionValue === undefined || criterionValue === null) {
+ continue
+ }
+
+ // Propagate errors from the criteria cells instead of masking them as BadCriterion
+ if (criterionValue instanceof CellError) {
+ return criterionValue
+ }
+
+ const rawCriterionValue = isExtendedNumber(criterionValue) ? getRawValue(criterionValue) : criterionValue
+
+ const criterionPackage = this.interpreter.criterionBuilder.fromCellValue(
+ rawCriterionValue as RawScalarValue,
+ this.arithmeticHelper
+ )
+
+ if (criterionPackage === undefined) {
+ return new CellError(ErrorType.VALUE, ErrorMessage.BadCriterion)
+ }
+
+ row.push({
+ columnIndex: dbColIndex,
+ lambda: criterionPackage.lambda,
+ })
+ }
+
+ rows.push(row)
+ }
+
+ return rows
+ }
+
+ /**
+ * Tests whether a database data row matches any of the criteria rows (OR logic).
+ * Within each criteria row, all conditions must match (AND logic).
+ *
+ * @param dataRow - A single row of data from the database (excluding the header row).
+ * @param criteriaRows - Parsed criteria rows.
+ * @returns true if the row qualifies, false otherwise.
+ */
+ private rowMatchesCriteria(dataRow: InternalScalarValue[], criteriaRows: DatabaseCriteriaRow[]): boolean {
+ if (criteriaRows.length === 0) {
+ return false
+ }
+
+ return criteriaRows.some(criteriaRow => {
+ if (criteriaRow.length === 0) {
+ return true // Empty criteria row = match all
+ }
+
+ return criteriaRow.every(entry => {
+ const cellValue = dataRow[entry.columnIndex]
+ const rawValue = isExtendedNumber(cellValue) ? getRawValue(cellValue) : cellValue
+ return entry.lambda(rawValue)
+ })
+ })
+ }
+}
diff --git a/src/interpreter/plugin/index.ts b/src/interpreter/plugin/index.ts
index 2b1191c63..e86ba4f2a 100644
--- a/src/interpreter/plugin/index.ts
+++ b/src/interpreter/plugin/index.ts
@@ -13,6 +13,7 @@ export {CharPlugin} from './CharPlugin'
export {CodePlugin} from './CodePlugin'
export {CountBlankPlugin} from './CountBlankPlugin'
export {CountUniquePlugin} from './CountUniquePlugin'
+export {DatabasePlugin} from './DatabasePlugin'
export {DateTimePlugin} from './DateTimePlugin'
export {DegreesPlugin} from './DegreesPlugin'
export {DeltaPlugin} from './DeltaPlugin'