Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handling Cluster Extension loading when custom XML is reloaded #1471

Merged
merged 3 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3673,6 +3673,7 @@ This module provides queries for ZCL loading
* [~commandMap(clusterId, packageId, commands)](#module_DB API_ zcl loading queries..commandMap) ⇒
* [~fieldMap(eventId, packageId, fields)](#module_DB API_ zcl loading queries..fieldMap) ⇒
* [~argMap(cmdId, packageId, args)](#module_DB API_ zcl loading queries..argMap) ⇒
* [~filterDuplicates(db, packageId, data, keys, elementName)](#module_DB API_ zcl loading queries..filterDuplicates) ⇒ <code>Array</code>
* [~insertAttributeAccessData(db, packageId, accessData)](#module_DB API_ zcl loading queries..insertAttributeAccessData) ⇒
* [~insertCommandAccessData(db, packageId, accessData)](#module_DB API_ zcl loading queries..insertCommandAccessData) ⇒
* [~insertEventAccessData(db, packageId, accessData)](#module_DB API_ zcl loading queries..insertEventAccessData) ⇒
Expand Down Expand Up @@ -3785,6 +3786,24 @@ Transforms the array of command args in a certain format and returns it.
| packageId | <code>\*</code> |
| args | <code>\*</code> |

<a name="module_DB API_ zcl loading queries..filterDuplicates"></a>

### DB API: zcl loading queries~filterDuplicates(db, packageId, data, keys, elementName) ⇒ <code>Array</code>
Filters out duplicates in an array of objects based on specified keys and logs a warning for each duplicate found.
This function is used to filter out duplicates in command, attribute, and event data before inserting into the database.
Treats `null` and `0` as equivalent.

**Kind**: inner method of [<code>DB API: zcl loading queries</code>](#module_DB API_ zcl loading queries)
**Returns**: <code>Array</code> - - Array of unique objects (duplicates removed).

| Param | Type | Description |
| --- | --- | --- |
| db | <code>\*</code> | |
| packageId | <code>\*</code> | |
| data | <code>Array</code> | Array of objects. |
| keys | <code>Array</code> | Array of keys to compare for duplicates (e.g., ['code', 'manufacturerCode']). |
| elementName | <code>\*</code> | |

<a name="module_DB API_ zcl loading queries..insertAttributeAccessData"></a>

### DB API: zcl loading queries~insertAttributeAccessData(db, packageId, accessData) ⇒
Expand Down
100 changes: 97 additions & 3 deletions src-electron/db/query-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ INSERT INTO COMMAND_ARG (
// Attribute table needs to be unique based on:
// UNIQUE("CLUSTER_REF", "PACKAGE_REF", "CODE", "MANUFACTURER_CODE")
const INSERT_ATTRIBUTE_QUERY = `
INSERT OR REPLACE INTO ATTRIBUTE (
INSERT INTO ATTRIBUTE (
CLUSTER_REF,
PACKAGE_REF,
CODE,
Expand Down Expand Up @@ -360,6 +360,50 @@ function argMap(cmdId, packageId, args) {
packageId
])
}
/**
* Filters out duplicates in an array of objects based on specified keys and logs a warning for each duplicate found.
* This function is used to filter out duplicates in command, attribute, and event data before inserting into the database.
* Treats `null` and `0` as equivalent.
*
* @param {*} db
* @param {*} packageId
* @param {Array} data - Array of objects.
* @param {Array} keys - Array of keys to compare for duplicates (e.g., ['code', 'manufacturerCode']).
* @param {*} elementName
* @returns {Array} - Array of unique objects (duplicates removed).
*/
function filterDuplicates(db, packageId, data, keys, elementName) {
let seen = new Map()
let uniqueItems = []

data.forEach((item, index) => {
let anyKeysPresent = keys.some((key) => key in item)

if (!anyKeysPresent) {
// If all keys are missing, treat this item as unique
uniqueItems.push(item)
} else {
let uniqueKey = keys
.map((key) => (item[key] === null || item[key] === 0 ? 0 : item[key]))
.join('|')

if (seen.has(uniqueKey)) {
// Log a warning with the duplicate information
queryNotification.setNotification(
db,
'ERROR',
`Duplicate ${elementName} found: ${JSON.stringify(item)}`,
packageId
)
} else {
seen.set(uniqueKey, true)
uniqueItems.push(item)
}
}
})

return uniqueItems
}

/**
* access data is array of objects, containing id/op/role/modifier.
Expand Down Expand Up @@ -655,7 +699,7 @@ async function insertClusterExtensions(db, packageId, knownPackages, data) {
)}) AND CODE = ?`,
data.map((cluster) => [cluster.code])
)
.then((rows) => {
.then(async (rows) => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never been a fan of this. Just break the code into separate statements using await

let commands = {
data: [],
args: [],
Expand All @@ -678,17 +722,38 @@ async function insertClusterExtensions(db, packageId, knownPackages, data) {
// NOTE: This code must stay in sync with insertClusters
if ('commands' in data[i]) {
let cmds = data[i].commands
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let cmds = filterDuplicates(
db,
packageId,
data[i].commands,
['code', 'manufacturerCode', 'source'],
'command'
)

cmds = filterDuplicates(
db,
packageId,
cmds,
['code', 'manufacturerCode', 'source'],
'command'
) // Removes any duplicates based of db unique constraint and logs package notification (avoids SQL error)
commands.data.push(...commandMap(lastId, packageId, cmds))
commands.args.push(...cmds.map((command) => command.args))
commands.access.push(...cmds.map((command) => command.access))
}
if ('attributes' in data[i]) {
let atts = data[i].attributes
atts = filterDuplicates(
db,
packageId,
atts,
['code', 'manufacturerCode', 'side'],
'attribute'
) // Removes any duplicates based of db unique constraint and logs package notification (avoids SQL error)
attributes.data.push(...attributeMap(lastId, packageId, atts))
attributes.access.push(...atts.map((at) => at.access))
}
if ('events' in data[i]) {
let evs = data[i].events
evs = filterDuplicates(
db,
packageId,
evs,
['code', 'manufacturerCode'],
'event'
) // Removes any duplicates based of db unique constraint and logs package notification (avoids SQL error)
events.data.push(...eventMap(lastId, packageId, evs))
events.fields.push(...evs.map((event) => event.fields))
events.access.push(...evs.map((event) => event.access))
Expand Down Expand Up @@ -720,7 +785,15 @@ async function insertClusterExtensions(db, packageId, knownPackages, data) {
let pCommand = insertCommands(db, packageId, commands)
let pAttribute = insertAttributes(db, packageId, attributes)
let pEvent = insertEvents(db, packageId, events)
return Promise.all([pCommand, pAttribute, pEvent])
return Promise.all([pCommand, pAttribute, pEvent]).catch((err) => {
if (err.includes('SQLITE_CONSTRAINT') && err.includes('UNIQUE')) {
env.logDebug(
`CRC match for file with package id ${packageId}, skipping parsing.`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This message does not seem right. Also use env.logWarning or env.logError

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not an error or a warning. We get to this point if we try to reload a cluster extension for a top-level package that already has it.

)
} else {
throw err
}
})
})
}

Expand Down Expand Up @@ -783,17 +856,38 @@ async function insertClusters(db, packageId, data) {
// NOTE: This code must stay in sync with insertClusterExtensions
if ('commands' in data[i]) {
let cmds = data[i].commands
cmds = filterDuplicates(
db,
packageId,
cmds,
['code', 'manufacturerCode', 'source'],
'command'
) // Removes any duplicates based of db unique constraint and logs package notification (avoids SQL error)
commands.data.push(...commandMap(lastId, packageId, cmds))
commands.args.push(...cmds.map((command) => command.args))
commands.access.push(...cmds.map((command) => command.access))
}
if ('attributes' in data[i]) {
let atts = data[i].attributes
atts = filterDuplicates(
db,
packageId,
atts,
['code', 'manufacturerCode', 'side'],
'attribute'
) // Removes any duplicates based of db unique constraint and logs package notification (avoids SQL error)
attributes.data.push(...attributeMap(lastId, packageId, atts))
attributes.access.push(...atts.map((at) => at.access))
}
if ('events' in data[i]) {
let evs = data[i].events
evs = filterDuplicates(
db,
packageId,
evs,
['code', 'manufacturerCode'],
'event'
) // Removes any duplicates based of db unique constraint and logs package notification (avoids SQL error)
events.data.push(...eventMap(lastId, packageId, evs))
events.fields.push(...evs.map((event) => event.fields))
events.access.push(...evs.map((event) => event.access))
Expand Down
12 changes: 8 additions & 4 deletions src-electron/db/zap-schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,11 @@ CREATE TABLE IF NOT EXISTS "CLUSTER" (
"INTRODUCED_IN_REF" integer,
"REMOVED_IN_REF" integer,
"API_MATURITY" text,
"MANUFACTURER_CODE_DERIVED" AS (COALESCE(MANUFACTURER_CODE, 0)),
foreign key (INTRODUCED_IN_REF) references SPEC(SPEC_ID) ON DELETE CASCADE ON UPDATE CASCADE,
foreign key (REMOVED_IN_REF) references SPEC(SPEC_ID) ON DELETE CASCADE ON UPDATE CASCADE,
foreign key (PACKAGE_REF) references PACKAGE(PACKAGE_ID) ON DELETE CASCADE ON UPDATE CASCADE
UNIQUE(PACKAGE_REF, CODE, MANUFACTURER_CODE)
UNIQUE(PACKAGE_REF, CODE, MANUFACTURER_CODE_DERIVED)
);
/*
COMMAND table contains commands contained inside a cluster.
Expand All @@ -183,12 +184,13 @@ CREATE TABLE IF NOT EXISTS "COMMAND" (
"RESPONSE_REF" integer,
"IS_DEFAULT_RESPONSE_ENABLED" integer,
"IS_LARGE_MESSAGE" integer,
"MANUFACTURER_CODE_DERIVED" AS (COALESCE(MANUFACTURER_CODE, 0)),
foreign key (INTRODUCED_IN_REF) references SPEC(SPEC_ID) ON DELETE CASCADE ON UPDATE CASCADE,
foreign key (REMOVED_IN_REF) references SPEC(SPEC_ID) ON DELETE CASCADE ON UPDATE CASCADE,
foreign key (CLUSTER_REF) references CLUSTER(CLUSTER_ID) ON DELETE CASCADE ON UPDATE CASCADE,
foreign key (PACKAGE_REF) references PACKAGE(PACKAGE_ID) ON DELETE CASCADE ON UPDATE CASCADE,
foreign key (RESPONSE_REF) references COMMAND(COMMAND_ID) ON DELETE CASCADE ON UPDATE CASCADE
UNIQUE(CLUSTER_REF, PACKAGE_REF, CODE, MANUFACTURER_CODE, SOURCE)
UNIQUE(CLUSTER_REF, PACKAGE_REF, CODE, MANUFACTURER_CODE_DERIVED, SOURCE)
);
/*
COMMAND_ARG table contains arguments for a command.
Expand Down Expand Up @@ -233,11 +235,12 @@ CREATE TABLE IF NOT EXISTS "EVENT" (
"PRIORITY" text,
"INTRODUCED_IN_REF" integer,
"REMOVED_IN_REF" integer,
"MANUFACTURER_CODE_DERIVED" AS (COALESCE(MANUFACTURER_CODE, 0)),
foreign key (CLUSTER_REF) references CLUSTER(CLUSTER_ID) ON DELETE CASCADE ON UPDATE CASCADE,
foreign key (PACKAGE_REF) references PACKAGE(PACKAGE_ID) ON DELETE CASCADE ON UPDATE CASCADE,
foreign key (INTRODUCED_IN_REF) references SPEC(SPEC_ID) ON DELETE CASCADE ON UPDATE CASCADE,
foreign key (REMOVED_IN_REF) references SPEC(SPEC_ID) ON DELETE CASCADE ON UPDATE CASCADE
UNIQUE(CLUSTER_REF, PACKAGE_REF, CODE, MANUFACTURER_CODE)
UNIQUE(CLUSTER_REF, PACKAGE_REF, CODE, MANUFACTURER_CODE_DERIVED)
);
/*
EVENT_FIELD table contains events for a given cluster.
Expand Down Expand Up @@ -294,11 +297,12 @@ CREATE TABLE IF NOT EXISTS "ATTRIBUTE" (
"API_MATURITY" text,
"IS_CHANGE_OMITTED" integer,
"PERSISTENCE" text,
"MANUFACTURER_CODE_DERIVED" AS (COALESCE(MANUFACTURER_CODE, 0)),
foreign key (INTRODUCED_IN_REF) references SPEC(SPEC_ID) ON DELETE CASCADE ON UPDATE CASCADE,
foreign key (REMOVED_IN_REF) references SPEC(SPEC_ID) ON DELETE CASCADE ON UPDATE CASCADE,
foreign key (CLUSTER_REF) references CLUSTER(CLUSTER_ID) ON DELETE CASCADE ON UPDATE CASCADE,
foreign key (PACKAGE_REF) references PACKAGE(PACKAGE_ID) ON DELETE CASCADE ON UPDATE CASCADE
UNIQUE("CLUSTER_REF", "PACKAGE_REF", "CODE", "MANUFACTURER_CODE")
UNIQUE("CLUSTER_REF", "PACKAGE_REF", "CODE", "MANUFACTURER_CODE_DERIVED", "SIDE")
);

/*
Expand Down
8 changes: 7 additions & 1 deletion test/custom-matter-xml.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,13 @@ test(
)
expect(
packageNotif.some((notif) => notif.message.includes('type contradiction'))
).toBeTruthy() // checks if the correct warning is thrown
).toBeTruthy() // checks if the correct type contradiction warning is thrown

expect(
packageNotif.some((notif) =>
notif.message.includes('Duplicate attribute found')
)
).toBeTruthy() // checks if the correct duplicate attribute error is thrown

let sessionNotif = await querySessionNotification.getNotification(db, sid)
expect(
Expand Down
2 changes: 2 additions & 0 deletions test/resource/custom-cluster/matter-bad-custom.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<!-- Warning: This custom xml has multiple intentional errors for testing. -->
<configurator>
<domain name="CHIP"/>

Expand All @@ -11,6 +12,7 @@
<clusterExtension code="0x0006">
// intentional undefined type errors
<attribute side="server" code="0xFFF10000" define="SAMPLE_MFG_SPECIFIC_TRANSITION_TIME_6" type="INT9" min="0x0000" max="0xFFFF" writable="true" default="0x0000" optional="true">Sample Mfg Specific Attribute 6</attribute>
<attribute side="server" code="0xFFF10000" define="SAMPLE_MFG_SPECIFIC_TRANSITION_TIME_6_DUPLICATE" type="INT8" min="0x0000" max="0xFFFF" writable="true" default="0x0000" optional="true">Sample Mfg Specific Attribute 6 Duplicate</attribute>
<attribute side="server" code="0xFFF20001" define="SAMPLE_MFG_SPECIFIC_TRANSITION_TIME_8" type="INT16U" min="0x0000" max="0xFFFF" writable="true" default="0x0000" optional="true">Sample Mfg Specific Attribute 8</attribute>
dhchandw marked this conversation as resolved.
Show resolved Hide resolved
<command source="client" code="0xFFF20001" name="SampleMfgSpecificOnWithTransition2" optional="true">
<description>Client command that turns the device on with a transition given
Expand Down
6 changes: 3 additions & 3 deletions test/test-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,12 @@ exports.testMatterCustomZap =

exports.totalClusterCount = 111
exports.totalDomainCount = 20
exports.totalCommandArgsCount = 1786
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am always a little cautious about tests changing...could you explain to me why you did this please?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the unique constraints in the schema were not being hit because for standard clusters, attributes, etc we had manufacturer code set to NULL and NULL does not equal to NULL in SQL. When I added a derived column to circumvent this, I found a duplicate command in zcl-builtin/silabs/demo.xml. With my changes this duplicate command wouldn't be loaded in and hence the command count decreased by one.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool, approved

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

assuming CI passes of course

exports.totalCommandCount = 632
exports.totalCommandArgsCount = 1785
exports.totalCommandCount = 631
exports.totalEventFieldCount = 3
exports.totalEventCount = 1
exports.totalAttributeCount = 3438
exports.totalClusterCommandCount = 609
exports.totalClusterCommandCount = 608
exports.totalServerAttributeCount = 2962
exports.totalSpecCount = 24
exports.totalEnumCount = 211
Expand Down
4 changes: 0 additions & 4 deletions zcl-builtin/silabs/demo.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@
<description>Send a hello command to the server</description>
<arg name="numberOfTimes" type="INT8U"/>
</command>
<command source="client" code="0x00" name="SayHelloFromClient" optional="false">
<description>Send a hello command to the client</description>
<arg name="numberOfTimes" type="INT8U"/>
</command>
<event code="0x0001" name="HelloEvent" priority="info" side="server">
<description>Example test event</description>
<field id="1" name="arg1" type="INT8U"/>
Expand Down
Loading