Skip to content

Commit 72192d8

Browse files
authored
Handling Cluster Extension loading when custom XML is reloaded (#1471)
* Handling Cluster Extension loading when custom XML is reloaded: * Making sure cluster extension is not duplicated when custom XML is reloaded * Added a MANUFACTURER_CODE_DERIVED column since NULL!=NULL in SQL * Added SIDE to the UNIQUE constraint in attributes * Added logic to catch duplicates during loading before insertion * Added relevant tests JIRA ZAPP-1486
1 parent 57ffa8e commit 72192d8

File tree

7 files changed

+218
-83
lines changed

7 files changed

+218
-83
lines changed

docs/api.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3673,6 +3673,7 @@ This module provides queries for ZCL loading
36733673
* [~commandMap(clusterId, packageId, commands)](#module_DB API_ zcl loading queries..commandMap) ⇒
36743674
* [~fieldMap(eventId, packageId, fields)](#module_DB API_ zcl loading queries..fieldMap) ⇒
36753675
* [~argMap(cmdId, packageId, args)](#module_DB API_ zcl loading queries..argMap) ⇒
3676+
* [~filterDuplicates(db, packageId, data, keys, elementName)](#module_DB API_ zcl loading queries..filterDuplicates) ⇒ <code>Array</code>
36763677
* [~insertAttributeAccessData(db, packageId, accessData)](#module_DB API_ zcl loading queries..insertAttributeAccessData) ⇒
36773678
* [~insertCommandAccessData(db, packageId, accessData)](#module_DB API_ zcl loading queries..insertCommandAccessData) ⇒
36783679
* [~insertEventAccessData(db, packageId, accessData)](#module_DB API_ zcl loading queries..insertEventAccessData) ⇒
@@ -3785,6 +3786,24 @@ Transforms the array of command args in a certain format and returns it.
37853786
| packageId | <code>\*</code> |
37863787
| args | <code>\*</code> |
37873788

3789+
<a name="module_DB API_ zcl loading queries..filterDuplicates"></a>
3790+
3791+
### DB API: zcl loading queries~filterDuplicates(db, packageId, data, keys, elementName) ⇒ <code>Array</code>
3792+
Filters out duplicates in an array of objects based on specified keys and logs a warning for each duplicate found.
3793+
This function is used to filter out duplicates in command, attribute, and event data before inserting into the database.
3794+
Treats `null` and `0` as equivalent.
3795+
3796+
**Kind**: inner method of [<code>DB API: zcl loading queries</code>](#module_DB API_ zcl loading queries)
3797+
**Returns**: <code>Array</code> - - Array of unique objects (duplicates removed).
3798+
3799+
| Param | Type | Description |
3800+
| --- | --- | --- |
3801+
| db | <code>\*</code> | |
3802+
| packageId | <code>\*</code> | |
3803+
| data | <code>Array</code> | Array of objects. |
3804+
| keys | <code>Array</code> | Array of keys to compare for duplicates (e.g., ['code', 'manufacturerCode']). |
3805+
| elementName | <code>\*</code> | |
3806+
37883807
<a name="module_DB API_ zcl loading queries..insertAttributeAccessData"></a>
37893808

37903809
### DB API: zcl loading queries~insertAttributeAccessData(db, packageId, accessData) ⇒

src-electron/db/query-loader.js

Lines changed: 158 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ INSERT INTO COMMAND_ARG (
138138
// Attribute table needs to be unique based on:
139139
// UNIQUE("CLUSTER_REF", "PACKAGE_REF", "CODE", "MANUFACTURER_CODE")
140140
const INSERT_ATTRIBUTE_QUERY = `
141-
INSERT OR REPLACE INTO ATTRIBUTE (
141+
INSERT INTO ATTRIBUTE (
142142
CLUSTER_REF,
143143
PACKAGE_REF,
144144
CODE,
@@ -360,6 +360,50 @@ function argMap(cmdId, packageId, args) {
360360
packageId
361361
])
362362
}
363+
/**
364+
* Filters out duplicates in an array of objects based on specified keys and logs a warning for each duplicate found.
365+
* This function is used to filter out duplicates in command, attribute, and event data before inserting into the database.
366+
* Treats `null` and `0` as equivalent.
367+
*
368+
* @param {*} db
369+
* @param {*} packageId
370+
* @param {Array} data - Array of objects.
371+
* @param {Array} keys - Array of keys to compare for duplicates (e.g., ['code', 'manufacturerCode']).
372+
* @param {*} elementName
373+
* @returns {Array} - Array of unique objects (duplicates removed).
374+
*/
375+
function filterDuplicates(db, packageId, data, keys, elementName) {
376+
let seen = new Map()
377+
let uniqueItems = []
378+
379+
data.forEach((item, index) => {
380+
let anyKeysPresent = keys.some((key) => key in item)
381+
382+
if (!anyKeysPresent) {
383+
// If all keys are missing, treat this item as unique
384+
uniqueItems.push(item)
385+
} else {
386+
let uniqueKey = keys
387+
.map((key) => (item[key] === null || item[key] === 0 ? 0 : item[key]))
388+
.join('|')
389+
390+
if (seen.has(uniqueKey)) {
391+
// Log a warning with the duplicate information
392+
queryNotification.setNotification(
393+
db,
394+
'ERROR',
395+
`Duplicate ${elementName} found: ${JSON.stringify(item)}`,
396+
packageId
397+
)
398+
} else {
399+
seen.set(uniqueKey, true)
400+
uniqueItems.push(item)
401+
}
402+
}
403+
})
404+
405+
return uniqueItems
406+
}
363407

364408
/**
365409
* access data is array of objects, containing id/op/role/modifier.
@@ -647,81 +691,101 @@ async function insertGlobals(db, packageId, data) {
647691
* @returns Promise of cluster extension insertion.
648692
*/
649693
async function insertClusterExtensions(db, packageId, knownPackages, data) {
650-
return dbApi
651-
.dbMultiSelect(
652-
db,
653-
`SELECT CLUSTER_ID FROM CLUSTER WHERE PACKAGE_REF IN (${dbApi.toInClause(
654-
knownPackages
655-
)}) AND CODE = ?`,
656-
data.map((cluster) => [cluster.code])
657-
)
658-
.then((rows) => {
659-
let commands = {
660-
data: [],
661-
args: [],
662-
access: []
663-
}
664-
let events = {
665-
data: [],
666-
fields: [],
667-
access: []
694+
let rows = await dbApi.dbMultiSelect(
695+
db,
696+
`SELECT CLUSTER_ID FROM CLUSTER WHERE PACKAGE_REF IN (${dbApi.toInClause(
697+
knownPackages
698+
)}) AND CODE = ?`,
699+
data.map((cluster) => [cluster.code])
700+
)
701+
702+
let commands = {
703+
data: [],
704+
args: [],
705+
access: []
706+
}
707+
let events = {
708+
data: [],
709+
fields: [],
710+
access: []
711+
}
712+
let attributes = {
713+
data: [],
714+
access: []
715+
}
716+
717+
let i, lastId
718+
for (i = 0; i < rows.length; i++) {
719+
let row = rows[i]
720+
if (row != null) {
721+
lastId = row.CLUSTER_ID
722+
// NOTE: This code must stay in sync with insertClusters
723+
if ('commands' in data[i]) {
724+
let cmds = filterDuplicates(
725+
db,
726+
packageId,
727+
data[i].commands,
728+
['code', 'manufacturerCode', 'source'],
729+
'command'
730+
) // Removes any duplicates based of db unique constraint and logs package notification (avoids SQL error)
731+
commands.data.push(...commandMap(lastId, packageId, cmds))
732+
commands.args.push(...cmds.map((command) => command.args))
733+
commands.access.push(...cmds.map((command) => command.access))
668734
}
669-
let attributes = {
670-
data: [],
671-
access: []
735+
if ('attributes' in data[i]) {
736+
let atts = filterDuplicates(
737+
db,
738+
packageId,
739+
data[i].attributes,
740+
['code', 'manufacturerCode', 'side'],
741+
'attribute'
742+
) // Removes any duplicates based of db unique constraint and logs package notification (avoids SQL error)
743+
attributes.data.push(...attributeMap(lastId, packageId, atts))
744+
attributes.access.push(...atts.map((at) => at.access))
672745
}
673-
let i, lastId
674-
for (i = 0; i < rows.length; i++) {
675-
let row = rows[i]
676-
if (row != null) {
677-
lastId = row.CLUSTER_ID
678-
// NOTE: This code must stay in sync with insertClusters
679-
if ('commands' in data[i]) {
680-
let cmds = data[i].commands
681-
commands.data.push(...commandMap(lastId, packageId, cmds))
682-
commands.args.push(...cmds.map((command) => command.args))
683-
commands.access.push(...cmds.map((command) => command.access))
684-
}
685-
if ('attributes' in data[i]) {
686-
let atts = data[i].attributes
687-
attributes.data.push(...attributeMap(lastId, packageId, atts))
688-
attributes.access.push(...atts.map((at) => at.access))
689-
}
690-
if ('events' in data[i]) {
691-
let evs = data[i].events
692-
events.data.push(...eventMap(lastId, packageId, evs))
693-
events.fields.push(...evs.map((event) => event.fields))
694-
events.access.push(...evs.map((event) => event.access))
695-
}
696-
} else {
697-
// DANGER: We got here because we are adding a cluster extension for a
698-
// cluster which is not defined. For eg:
699-
// <clusterExtension code="0x0000">
700-
// <attribute side="server" code="0x4000" define="SW_BUILD_ID"
701-
// type="CHAR_STRING" length="16" writable="false"
702-
// default="" optional="true"
703-
// introducedIn="zll-1.0-11-0037-10">sw build id</attribute>
704-
// </clusterExtension>
705-
// If a cluster with code 0x0000 does not exist then we run into this
706-
// issue.
707-
let message = `Attempting to insert cluster extension for a cluster which does not
708-
exist. Check clusterExtension meta data in xml file.
709-
Cluster Code: ${data[i].code}`
710-
env.logWarning(message)
711-
queryNotification.setNotification(
712-
db,
713-
'WARNING',
714-
message,
715-
packageId,
716-
2
717-
)
718-
}
746+
if ('events' in data[i]) {
747+
let evs = filterDuplicates(
748+
db,
749+
packageId,
750+
data[i].events,
751+
['code', 'manufacturerCode'],
752+
'event'
753+
) // Removes any duplicates based of db unique constraint and logs package notification (avoids SQL error)
754+
events.data.push(...eventMap(lastId, packageId, evs))
755+
events.fields.push(...evs.map((event) => event.fields))
756+
events.access.push(...evs.map((event) => event.access))
719757
}
720-
let pCommand = insertCommands(db, packageId, commands)
721-
let pAttribute = insertAttributes(db, packageId, attributes)
722-
let pEvent = insertEvents(db, packageId, events)
723-
return Promise.all([pCommand, pAttribute, pEvent])
724-
})
758+
} else {
759+
// DANGER: We got here because we are adding a cluster extension for a
760+
// cluster which is not defined. For eg:
761+
// <clusterExtension code="0x0000">
762+
// <attribute side="server" code="0x4000" define="SW_BUILD_ID"
763+
// type="CHAR_STRING" length="16" writable="false"
764+
// default="" optional="true"
765+
// introducedIn="zll-1.0-11-0037-10">sw build id</attribute>
766+
// </clusterExtension>
767+
// If a cluster with code 0x0000 does not exist then we run into this
768+
// issue.
769+
let message = `Attempting to insert cluster extension for a cluster which does not
770+
exist. Check clusterExtension meta data in xml file.
771+
Cluster Code: ${data[i].code}`
772+
env.logWarning(message)
773+
queryNotification.setNotification(db, 'WARNING', message, packageId, 2)
774+
}
775+
}
776+
777+
let pCommand = insertCommands(db, packageId, commands)
778+
let pAttribute = insertAttributes(db, packageId, attributes)
779+
let pEvent = insertEvents(db, packageId, events)
780+
return Promise.all([pCommand, pAttribute, pEvent]).catch((err) => {
781+
if (err.includes('SQLITE_CONSTRAINT') && err.includes('UNIQUE')) {
782+
env.logDebug(
783+
`CRC match for file with package id ${packageId}, skipping parsing.`
784+
)
785+
} else {
786+
throw err
787+
}
788+
})
725789
}
726790

727791
/**
@@ -783,17 +847,38 @@ async function insertClusters(db, packageId, data) {
783847
// NOTE: This code must stay in sync with insertClusterExtensions
784848
if ('commands' in data[i]) {
785849
let cmds = data[i].commands
850+
cmds = filterDuplicates(
851+
db,
852+
packageId,
853+
cmds,
854+
['code', 'manufacturerCode', 'source'],
855+
'command'
856+
) // Removes any duplicates based of db unique constraint and logs package notification (avoids SQL error)
786857
commands.data.push(...commandMap(lastId, packageId, cmds))
787858
commands.args.push(...cmds.map((command) => command.args))
788859
commands.access.push(...cmds.map((command) => command.access))
789860
}
790861
if ('attributes' in data[i]) {
791862
let atts = data[i].attributes
863+
atts = filterDuplicates(
864+
db,
865+
packageId,
866+
atts,
867+
['code', 'manufacturerCode', 'side'],
868+
'attribute'
869+
) // Removes any duplicates based of db unique constraint and logs package notification (avoids SQL error)
792870
attributes.data.push(...attributeMap(lastId, packageId, atts))
793871
attributes.access.push(...atts.map((at) => at.access))
794872
}
795873
if ('events' in data[i]) {
796874
let evs = data[i].events
875+
evs = filterDuplicates(
876+
db,
877+
packageId,
878+
evs,
879+
['code', 'manufacturerCode'],
880+
'event'
881+
) // Removes any duplicates based of db unique constraint and logs package notification (avoids SQL error)
797882
events.data.push(...eventMap(lastId, packageId, evs))
798883
events.fields.push(...evs.map((event) => event.fields))
799884
events.access.push(...evs.map((event) => event.access))

src-electron/db/zap-schema.sql

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,10 +156,11 @@ CREATE TABLE IF NOT EXISTS "CLUSTER" (
156156
"INTRODUCED_IN_REF" integer,
157157
"REMOVED_IN_REF" integer,
158158
"API_MATURITY" text,
159+
"MANUFACTURER_CODE_DERIVED" AS (COALESCE(MANUFACTURER_CODE, 0)),
159160
foreign key (INTRODUCED_IN_REF) references SPEC(SPEC_ID) ON DELETE CASCADE ON UPDATE CASCADE,
160161
foreign key (REMOVED_IN_REF) references SPEC(SPEC_ID) ON DELETE CASCADE ON UPDATE CASCADE,
161162
foreign key (PACKAGE_REF) references PACKAGE(PACKAGE_ID) ON DELETE CASCADE ON UPDATE CASCADE
162-
UNIQUE(PACKAGE_REF, CODE, MANUFACTURER_CODE)
163+
UNIQUE(PACKAGE_REF, CODE, MANUFACTURER_CODE_DERIVED)
163164
);
164165
/*
165166
COMMAND table contains commands contained inside a cluster.
@@ -183,12 +184,13 @@ CREATE TABLE IF NOT EXISTS "COMMAND" (
183184
"RESPONSE_REF" integer,
184185
"IS_DEFAULT_RESPONSE_ENABLED" integer,
185186
"IS_LARGE_MESSAGE" integer,
187+
"MANUFACTURER_CODE_DERIVED" AS (COALESCE(MANUFACTURER_CODE, 0)),
186188
foreign key (INTRODUCED_IN_REF) references SPEC(SPEC_ID) ON DELETE CASCADE ON UPDATE CASCADE,
187189
foreign key (REMOVED_IN_REF) references SPEC(SPEC_ID) ON DELETE CASCADE ON UPDATE CASCADE,
188190
foreign key (CLUSTER_REF) references CLUSTER(CLUSTER_ID) ON DELETE CASCADE ON UPDATE CASCADE,
189191
foreign key (PACKAGE_REF) references PACKAGE(PACKAGE_ID) ON DELETE CASCADE ON UPDATE CASCADE,
190192
foreign key (RESPONSE_REF) references COMMAND(COMMAND_ID) ON DELETE CASCADE ON UPDATE CASCADE
191-
UNIQUE(CLUSTER_REF, PACKAGE_REF, CODE, MANUFACTURER_CODE, SOURCE)
193+
UNIQUE(CLUSTER_REF, PACKAGE_REF, CODE, MANUFACTURER_CODE_DERIVED, SOURCE)
192194
);
193195
/*
194196
COMMAND_ARG table contains arguments for a command.
@@ -233,11 +235,12 @@ CREATE TABLE IF NOT EXISTS "EVENT" (
233235
"PRIORITY" text,
234236
"INTRODUCED_IN_REF" integer,
235237
"REMOVED_IN_REF" integer,
238+
"MANUFACTURER_CODE_DERIVED" AS (COALESCE(MANUFACTURER_CODE, 0)),
236239
foreign key (CLUSTER_REF) references CLUSTER(CLUSTER_ID) ON DELETE CASCADE ON UPDATE CASCADE,
237240
foreign key (PACKAGE_REF) references PACKAGE(PACKAGE_ID) ON DELETE CASCADE ON UPDATE CASCADE,
238241
foreign key (INTRODUCED_IN_REF) references SPEC(SPEC_ID) ON DELETE CASCADE ON UPDATE CASCADE,
239242
foreign key (REMOVED_IN_REF) references SPEC(SPEC_ID) ON DELETE CASCADE ON UPDATE CASCADE
240-
UNIQUE(CLUSTER_REF, PACKAGE_REF, CODE, MANUFACTURER_CODE)
243+
UNIQUE(CLUSTER_REF, PACKAGE_REF, CODE, MANUFACTURER_CODE_DERIVED)
241244
);
242245
/*
243246
EVENT_FIELD table contains events for a given cluster.
@@ -294,11 +297,12 @@ CREATE TABLE IF NOT EXISTS "ATTRIBUTE" (
294297
"API_MATURITY" text,
295298
"IS_CHANGE_OMITTED" integer,
296299
"PERSISTENCE" text,
300+
"MANUFACTURER_CODE_DERIVED" AS (COALESCE(MANUFACTURER_CODE, 0)),
297301
foreign key (INTRODUCED_IN_REF) references SPEC(SPEC_ID) ON DELETE CASCADE ON UPDATE CASCADE,
298302
foreign key (REMOVED_IN_REF) references SPEC(SPEC_ID) ON DELETE CASCADE ON UPDATE CASCADE,
299303
foreign key (CLUSTER_REF) references CLUSTER(CLUSTER_ID) ON DELETE CASCADE ON UPDATE CASCADE,
300304
foreign key (PACKAGE_REF) references PACKAGE(PACKAGE_ID) ON DELETE CASCADE ON UPDATE CASCADE
301-
UNIQUE("CLUSTER_REF", "PACKAGE_REF", "CODE", "MANUFACTURER_CODE")
305+
UNIQUE("CLUSTER_REF", "PACKAGE_REF", "CODE", "MANUFACTURER_CODE_DERIVED", "SIDE")
302306
);
303307

304308
/*

test/custom-matter-xml.test.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,19 @@ test(
448448
)
449449
expect(
450450
packageNotif.some((notif) => notif.message.includes('type contradiction'))
451-
).toBeTruthy() // checks if the correct warning is thrown
451+
).toBeTruthy() // checks if the correct type contradiction warning is thrown
452+
453+
expect(
454+
packageNotif.some((notif) =>
455+
notif.message.includes('Duplicate attribute found')
456+
)
457+
).toBeTruthy() // checks if the correct duplicate attribute error is thrown
458+
459+
expect(
460+
packageNotif.some((notif) =>
461+
notif.message.includes('Duplicate command found')
462+
)
463+
).toBeTruthy() // checks if the correct duplicate command error is thrown
452464

453465
let sessionNotif = await querySessionNotification.getNotification(db, sid)
454466
expect(

0 commit comments

Comments
 (0)