Skip to content
15 changes: 10 additions & 5 deletions src/plexosdb/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ def add_object(
*,
description: str | None = None,
category: str | None = None,
collection_enum: CollectionEnum | None = None,
collection_enum: CollectionEnum | None | Literal[False] = None,
) -> int:
"""Add an object to the database and append a system membership.

Expand All @@ -627,7 +627,7 @@ def add_object(
Category of the object, by default "-"
description : str | None, optional
Description of the object, by default None
collection_enum : CollectionEnum | None, optional
collection_enum : CollectionEnum | None | Literal[False] = None, optional
Collection for the system membership. If None, a default collection is determined
based on the class, by default None

Expand Down Expand Up @@ -679,6 +679,10 @@ def add_object(
assert query_result
object_id = self._db.last_insert_rowid()

# Skip system membership for System class itself, or if explicitly set to False
if collection_enum is False:
return object_id

if not collection_enum:
collection_enum = get_default_collection(class_enum)
_ = self.add_membership(ClassEnum.System, class_enum, "System", name, collection_enum)
Expand Down Expand Up @@ -739,7 +743,6 @@ def add_objects(
query_result = self._db.executemany(query, params)
assert query_result

# Add system memberships in bulk
collection_enum = get_default_collection(class_enum)
object_ids = self.get_objects_id(names, class_enum=class_enum)
parent_class_id = self.get_class_id(ClassEnum.System)
Expand Down Expand Up @@ -852,10 +855,12 @@ def add_properties_from_records(
logger.warning("No records provided for bulk property and text insertion")
return

params, _ = prepare_properties_params(self, records, object_class, collection, parent_class)
params, _, metadata_map = prepare_properties_params(
self, records, object_class, collection, parent_class
)

with self._db.transaction():
data_id_map = insert_property_data(self, params)
data_id_map = insert_property_data(self, params, metadata_map)
insert_scenario_tags(self, scenario, params, chunksize)

if any("datafile_text" in rec for rec in records):
Expand Down
73 changes: 65 additions & 8 deletions src/plexosdb/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,22 @@ def prepare_properties_params(
object_class: ClassEnum,
collection: CollectionEnum,
parent_class: ClassEnum,
) -> tuple[list[tuple[int, int, Any]], list[tuple[str, int]]]:
) -> tuple[list[tuple[int, int, Any]], list[tuple[str, int]], dict[tuple[int, int, Any], dict[str, Any]]]:
"""Prepare SQL parameters for property insertion.

Parameters
----------
db : PlexosDB
Database instance
records : list[dict]
List of property records
List of property records with property-specific format:
{
"name": "obj1",
"properties": {
"Property1": {"value": value1, "band": 1, "date_from": date1},
"Property2": {"value": value2, "band": 2, "date_from": date2}
}
}
object_class : ClassEnum
Class enumeration of the objects
collection : CollectionEnum
Expand All @@ -211,8 +218,8 @@ def prepare_properties_params(

Returns
-------
tuple[list[tuple], list]
Tuple of (params, collection_properties)
tuple[list[tuple], list, dict]
Tuple of (params, collection_properties, metadata_map)
"""
collection_id = db.get_collection_id(
collection, parent_class_enum=parent_class, child_class_enum=object_class
Expand All @@ -229,12 +236,45 @@ def prepare_properties_params(
"Make sure you use `add_object` before adding properties."
)

params = prepare_sql_data_params(records, memberships=memberships, property_mapping=collection_properties)
return params, collection_properties
property_id_map = {prop: pid for prop, pid in collection_properties}
name_to_membership = {membership["name"]: membership["membership_id"] for membership in memberships}

params = []
metadata_map = {}

for record in records:
membership_id = name_to_membership.get(record["name"])
if not membership_id:
continue

properties = record.get("properties", {})

for prop_name, prop_data in properties.items():
property_id = property_id_map.get(prop_name)
if not property_id:
continue

# Extract value and metadata - handle both dict and simple value
value = prop_data.get("value") if isinstance(prop_data, dict) else prop_data
band = prop_data.get("band") or prop_data.get("Band") if isinstance(prop_data, dict) else None
date_from = prop_data.get("date_from") if isinstance(prop_data, dict) else None
date_to = prop_data.get("date_to") if isinstance(prop_data, dict) else None

param_key = (membership_id, property_id, value)
params.append(param_key)
metadata_map[param_key] = {
"band": band,
"date_from": date_from,
"date_to": date_to,
}

return params, collection_properties, metadata_map


def insert_property_data(
db: PlexosDB, params: list[tuple[int, int, Any]]
db: PlexosDB,
params: list[tuple[int, int, Any]],
metadata_map: dict[tuple[int, int, Any], dict[str, Any]] | None = None,
) -> dict[tuple[int, int, Any], tuple[int, str]]:
"""Insert property data and return mapping of data IDs to object names.

Expand All @@ -244,6 +284,8 @@ def insert_property_data(
Database instance
params : list[tuple]
List of (membership_id, property_id, value) tuples
metadata_map : dict | None, optional
Mapping of params to metadata (band, date_from, date_to), by default None

Returns
-------
Expand All @@ -268,7 +310,22 @@ def insert_property_data(
for membership_id, property_id, value in params:
result = db._db.fetchone(data_ids_query, (membership_id, property_id, value))
if result:
data_id_map[(membership_id, property_id, value)] = (result[0], result[1])
data_id = result[0]
obj_name = result[1]
data_id_map[(membership_id, property_id, value)] = (data_id, obj_name)

if metadata_map and (membership_id, property_id, value) in metadata_map:
metadata = metadata_map[(membership_id, property_id, value)]
band = metadata.get("band")
date_from = metadata.get("date_from")
date_to = metadata.get("date_to")

if band is not None:
db.add_band(data_id, band)

if date_from is not None or date_to is not None:
db._handle_dates(data_id, date_from, date_to)

return data_id_map


Expand Down
29 changes: 25 additions & 4 deletions tests/test_plexosdb_from_records.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,37 @@
def test_bulk_insert_properties_from_records(db_base: PlexosDB):
from plexosdb import ClassEnum, CollectionEnum

db: PlexosDB() = db_base
db: PlexosDB = db_base

db.add_object(ClassEnum.Generator, "Generator1")
db.add_object(ClassEnum.Generator, "Generator2")
db.add_object(ClassEnum.Generator, "Generator3")

records = [
{"name": "Generator1", "Max Capacity": 100.0, "Min Stable Level": 20.0, "Heat Rate": 10.5},
{"name": "Generator2", "Max Capacity": 150.0, "Min Stable Level": 30.0, "Heat Rate": 9.8},
{"name": "Generator3", "Max Capacity": 200.0, "Min Stable Level": 40.0, "Heat Rate": 8.7},
{
"name": "Generator1",
"properties": {
"Max Capacity": {"value": 100.0},
"Min Stable Level": {"value": 20.0},
"Heat Rate": {"value": 10.5},
},
},
{
"name": "Generator2",
"properties": {
"Max Capacity": {"value": 150.0},
"Min Stable Level": {"value": 30.0},
"Heat Rate": {"value": 9.8},
},
},
{
"name": "Generator3",
"properties": {
"Max Capacity": {"value": 200.0},
"Min Stable Level": {"value": 40.0},
"Heat Rate": {"value": 8.7},
},
},
]

db.add_properties_from_records(
Expand Down
55 changes: 30 additions & 25 deletions tests/test_utils_build_data_id_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ def test_build_data_id_map_single_record(db_with_topology: PlexosDB) -> None:

db_with_topology.add_object(ClassEnum.Generator, "gen-01")

records = [{"name": "gen-01", "Max Capacity": 100.0}]
records = [{"name": "gen-01", "properties": {"Max Capacity": {"value": 100.0}}}]

params, _ = prepare_properties_params(
params, _, metadata_map = prepare_properties_params(
db_with_topology,
records,
ClassEnum.Generator,
Expand All @@ -26,7 +26,7 @@ def test_build_data_id_map_single_record(db_with_topology: PlexosDB) -> None:
)

with db_with_topology._db.transaction():
insert_property_data(db_with_topology, params)
insert_property_data(db_with_topology, params, metadata_map)
data_id_map = build_data_id_map(db_with_topology._db, params)

assert len(data_id_map) == 1
Expand All @@ -40,9 +40,9 @@ def test_build_data_id_map_returns_correct_structure(db_with_topology: PlexosDB)

db_with_topology.add_object(ClassEnum.Generator, "gen-01")

records = [{"name": "gen-01", "Max Capacity": 100.0}]
records = [{"name": "gen-01", "properties": {"Max Capacity": {"value": 100.0}}}]

params, _ = prepare_properties_params(
params, _, metadata_map = prepare_properties_params(
db_with_topology,
records,
ClassEnum.Generator,
Expand All @@ -51,7 +51,7 @@ def test_build_data_id_map_returns_correct_structure(db_with_topology: PlexosDB)
)

with db_with_topology._db.transaction():
insert_property_data(db_with_topology, params)
insert_property_data(db_with_topology, params, metadata_map)
data_id_map = build_data_id_map(db_with_topology._db, params)

for key, value in data_id_map.items():
Expand All @@ -75,11 +75,11 @@ def test_build_data_id_map_multiple_records(db_with_topology: PlexosDB) -> None:
db_with_topology.add_object(ClassEnum.Generator, "gen-02")

records = [
{"name": "gen-01", "Max Capacity": 100.0},
{"name": "gen-02", "Max Capacity": 200.0},
{"name": "gen-01", "properties": {"Max Capacity": {"value": 100.0}}},
{"name": "gen-02", "properties": {"Max Capacity": {"value": 200.0}}},
]

params, _ = prepare_properties_params(
params, _, metadata_map = prepare_properties_params(
db_with_topology,
records,
ClassEnum.Generator,
Expand All @@ -88,7 +88,7 @@ def test_build_data_id_map_multiple_records(db_with_topology: PlexosDB) -> None:
)

with db_with_topology._db.transaction():
insert_property_data(db_with_topology, params)
insert_property_data(db_with_topology, params, metadata_map)
data_id_map = build_data_id_map(db_with_topology._db, params)

assert len(data_id_map) == 2
Expand All @@ -101,9 +101,14 @@ def test_build_data_id_map_multiple_properties(db_with_topology: PlexosDB) -> No

db_with_topology.add_object(ClassEnum.Generator, "gen-01")

records = [{"name": "gen-01", "Max Capacity": 100.0, "Fuel Price": 5.0}]
records = [
{
"name": "gen-01",
"properties": {"Max Capacity": {"value": 100.0}, "Fuel Price": {"value": 5.0}},
}
]

params, _ = prepare_properties_params(
params, _, metadata_map = prepare_properties_params(
db_with_topology,
records,
ClassEnum.Generator,
Expand All @@ -112,7 +117,7 @@ def test_build_data_id_map_multiple_properties(db_with_topology: PlexosDB) -> No
)

with db_with_topology._db.transaction():
insert_property_data(db_with_topology, params)
insert_property_data(db_with_topology, params, metadata_map)
data_id_map = build_data_id_map(db_with_topology._db, params)

assert len(data_id_map) == 2
Expand All @@ -137,11 +142,11 @@ def test_build_data_id_map_preserves_mapping_accuracy(db_with_topology: PlexosDB
db_with_topology.add_object(ClassEnum.Generator, "gen-02")

records = [
{"name": "gen-01", "Max Capacity": 100.0},
{"name": "gen-02", "Max Capacity": 200.0},
{"name": "gen-01", "properties": {"Max Capacity": {"value": 100.0}}},
{"name": "gen-02", "properties": {"Max Capacity": {"value": 200.0}}},
]

params, _ = prepare_properties_params(
params, _, metadata_map = prepare_properties_params(
db_with_topology,
records,
ClassEnum.Generator,
Expand All @@ -150,7 +155,7 @@ def test_build_data_id_map_preserves_mapping_accuracy(db_with_topology: PlexosDB
)

with db_with_topology._db.transaction():
insert_property_data(db_with_topology, params)
insert_property_data(db_with_topology, params, metadata_map)
data_id_map = build_data_id_map(db_with_topology._db, params)

# Verify all params are in the mapping
Expand All @@ -170,12 +175,12 @@ def test_build_data_id_map_edge_case_values(db_with_topology: PlexosDB) -> None:
db_with_topology.add_object(ClassEnum.Generator, "gen-03")

records = [
{"name": "gen-01", "Max Capacity": 0.0},
{"name": "gen-02", "Max Capacity": -100.0},
{"name": "gen-03", "Max Capacity": 1e15},
{"name": "gen-01", "properties": {"Max Capacity": {"value": 0.0}}},
{"name": "gen-02", "properties": {"Max Capacity": {"value": -100.0}}},
{"name": "gen-03", "properties": {"Max Capacity": {"value": 1e15}}},
]

params, _ = prepare_properties_params(
params, _, metadata_map = prepare_properties_params(
db_with_topology,
records,
ClassEnum.Generator,
Expand All @@ -184,7 +189,7 @@ def test_build_data_id_map_edge_case_values(db_with_topology: PlexosDB) -> None:
)

with db_with_topology._db.transaction():
insert_property_data(db_with_topology, params)
insert_property_data(db_with_topology, params, metadata_map)
data_id_map = build_data_id_map(db_with_topology._db, params)

assert len(data_id_map) == 3
Expand All @@ -203,9 +208,9 @@ def test_build_data_id_map_data_ids_and_names_valid(db_with_topology: PlexosDB)

db_with_topology.add_object(ClassEnum.Generator, "test-generator")

records = [{"name": "test-generator", "Max Capacity": 100.0}]
records = [{"name": "test-generator", "properties": {"Max Capacity": {"value": 100.0}}}]

params, _ = prepare_properties_params(
params, _, metadata_map = prepare_properties_params(
db_with_topology,
records,
ClassEnum.Generator,
Expand All @@ -214,7 +219,7 @@ def test_build_data_id_map_data_ids_and_names_valid(db_with_topology: PlexosDB)
)

with db_with_topology._db.transaction():
insert_property_data(db_with_topology, params)
insert_property_data(db_with_topology, params, metadata_map)
data_id_map = build_data_id_map(db_with_topology._db, params)

for data_id, obj_name in data_id_map.values():
Expand Down
Loading