Skip to content

Commit 0d7862d

Browse files
authored
Merge pull request #622 from chisholm/relational-data-sink-dicts-of-lists
Add support for lists as dict values
2 parents 9653bd7 + 002c9e7 commit 0d7862d

File tree

1 file changed

+73
-11
lines changed

1 file changed

+73
-11
lines changed

stix2/datastore/relational_db/query.py

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import collections
12
import inspect
23

34
import sqlalchemy as sa
@@ -280,7 +281,15 @@ def _read_kill_chain_phases(stix_id, type_table, metadata, conn):
280281
return kill_chain_phases
281282

282283

283-
def _read_dictionary_property(stix_id, type_table, prop_name, prop_instance, metadata, conn):
284+
def _read_dictionary_property(
285+
stix_id,
286+
type_table,
287+
prop_name,
288+
prop_instance,
289+
metadata,
290+
conn,
291+
db_backend
292+
):
284293
"""
285294
Read a dictionary from a table.
286295
@@ -292,23 +301,57 @@ def _read_dictionary_property(stix_id, type_table, prop_name, prop_instance, met
292301
:param metadata: SQLAlchemy Metadata object containing all the table
293302
information
294303
:param conn: An SQLAlchemy DB connection
304+
:param db_backend: A backend object with information about how data is
305+
stored in the database
295306
:return: The dictionary, or None if no dictionary entries were found
296307
"""
297308
dict_table_name = f"{type_table.fullname}_{prop_name}"
298309
dict_table = metadata.tables[dict_table_name]
299310

300311
if len(prop_instance.valid_types) == 1:
301-
stmt = sa.select(
302-
dict_table.c.name, dict_table.c.value,
303-
).where(
304-
dict_table.c.id == stix_id,
305-
)
312+
valid_type = prop_instance.valid_types[0]
313+
314+
if isinstance(valid_type, stix2.properties.ListProperty):
315+
if db_backend.array_allowed():
316+
stmt = sa.select(
317+
dict_table.c.name, dict_table.c["values"],
318+
).where(
319+
dict_table.c.id == stix_id,
320+
)
306321

307-
results = conn.execute(stmt)
308-
dict_value = dict(results.all())
322+
results = conn.execute(stmt)
323+
dict_value = dict(results.all())
324+
325+
else:
326+
# Dict contains a list, but array columns are not supported.
327+
# So query from an auxiliary table.
328+
list_table_name = f"{dict_table_name}_values"
329+
list_table = metadata.tables[list_table_name]
330+
stmt = sa.select(
331+
dict_table.c.name, list_table.c.value
332+
).select_from(dict_table).join(
333+
list_table, list_table.c.id == dict_table.c.values
334+
).where(
335+
dict_table.c.id == stix_id
336+
)
337+
338+
results = conn.execute(stmt)
339+
dict_value = collections.defaultdict(list)
340+
for key, value in results:
341+
dict_value[key].append(value)
342+
343+
else:
344+
stmt = sa.select(
345+
dict_table.c.name, dict_table.c.value,
346+
).where(
347+
dict_table.c.id == stix_id,
348+
)
349+
350+
results = conn.execute(stmt)
351+
dict_value = dict(results.all())
309352

310353
else:
311-
# In this case, we get one column per valid type
354+
# In this case, we get one column per valid type (assume no lists here)
312355
type_cols = (col for col in dict_table.c if col.key not in ("id", "name"))
313356
stmt = sa.select(dict_table.c.name, *type_cols).where(dict_table.c.id == stix_id)
314357
results = conn.execute(stmt)
@@ -437,7 +480,15 @@ def _read_embedded_object_list(fk_id, join_table, embedded_type, metadata, conn)
437480
return obj_list
438481

439482

440-
def _read_complex_property_value(obj_id, prop_name, prop_instance, obj_table, metadata, conn):
483+
def _read_complex_property_value(
484+
obj_id,
485+
prop_name,
486+
prop_instance,
487+
obj_table,
488+
metadata,
489+
conn,
490+
db_backend
491+
):
441492
"""
442493
Read property values which require auxiliary tables to store. These are
443494
idiosyncratic and just require a lot of special cases. This function has
@@ -456,6 +507,8 @@ def _read_complex_property_value(obj_id, prop_name, prop_instance, obj_table, me
456507
:param metadata: SQLAlchemy Metadata object containing all the table
457508
information
458509
:param conn: An SQLAlchemy DB connection
510+
:param db_backend: A backend object with information about how data is
511+
stored in the database
459512
:return: The property value
460513
"""
461514

@@ -523,7 +576,15 @@ def _read_complex_property_value(obj_id, prop_name, prop_instance, obj_table, me
523576
elif isinstance(prop_instance, stix2.properties.DictionaryProperty):
524577
# ExtensionsProperty/HashesProperty subclasses DictionaryProperty, so
525578
# this must come after those
526-
prop_value = _read_dictionary_property(obj_id, obj_table, prop_name, prop_instance, metadata, conn)
579+
prop_value = _read_dictionary_property(
580+
obj_id,
581+
obj_table,
582+
prop_name,
583+
prop_instance,
584+
metadata,
585+
conn,
586+
db_backend
587+
)
527588

528589
elif isinstance(prop_instance, stix2.properties.EmbeddedObjectProperty):
529590
prop_value = _read_embedded_object(
@@ -611,6 +672,7 @@ def _read_complex_top_level_property_value(
611672
type_table,
612673
metadata,
613674
conn,
675+
db_backend
614676
)
615677

616678
return prop_value

0 commit comments

Comments
 (0)