Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Support for Oracle Database (>= 19C)
- export: JSON Schema exporter now exports validation constraints from ODCS logicalTypeOptions (minLength, maxLength, pattern, format, minimum, maximum, multipleOf, minItems, maxItems, uniqueItems, minProperties, maxProperties)

### Fixed

Expand All @@ -65,13 +66,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- import: Support for nested arrays in odcs v3 importer
- import: Support for ODCS logicalTypeOptions in odcs v3 importer
- lint: ODCS schema is now checked before converting
- --debug flag for all commands

### Fixed

- export: Excel exporter now exports critical data element

- test: Fixed DuckDB type conversion for number/decimal/numeric types - now uses DECIMAL instead of VARCHAR

## [0.10.36] - 2025-10-17

Expand Down
3 changes: 1 addition & 2 deletions datacontract/export/duckdb_type_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ def convert_to_duckdb_csv_type(field) -> None | str:
if datacontract_type.lower() in ["time"]:
return "TIME"
if datacontract_type.lower() in ["number", "decimal", "numeric"]:
# precision and scale not supported by data contract
return "VARCHAR"
return "DECIMAL"
if datacontract_type.lower() in ["float", "double"]:
return "DOUBLE"
if datacontract_type.lower() in ["integer", "int", "long", "bigint"]:
Expand Down
17 changes: 16 additions & 1 deletion datacontract/export/jsonschema_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,22 @@ def to_property(field: Field) -> dict:
if field.classification is not None:
property["classification"] = field.classification

# TODO: all constraints
# Export constraints from config (for fields not directly available on Field model)
if field.config:
if "multipleOf" in field.config and field.config["multipleOf"] is not None:
property["multipleOf"] = field.config["multipleOf"]
if "minItems" in field.config and field.config["minItems"] is not None:
property["minItems"] = field.config["minItems"]
if "maxItems" in field.config and field.config["maxItems"] is not None:
property["maxItems"] = field.config["maxItems"]
# Only export uniqueItems when it's true (default is false, so don't export false)
if "uniqueItems" in field.config and field.config["uniqueItems"] is True:
property["uniqueItems"] = field.config["uniqueItems"]
if "minProperties" in field.config and field.config["minProperties"] is not None:
property["minProperties"] = field.config["minProperties"]
if "maxProperties" in field.config and field.config["maxProperties"] is not None:
property["maxProperties"] = field.config["maxProperties"]

return property


Expand Down
30 changes: 29 additions & 1 deletion datacontract/imports/odcs_v3_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,21 @@ def import_field_config(odcs_property: SchemaProperty, server_type=None) -> dict
for item in odcs_property.customProperties:
config[item.property] = item.value

# Extract logicalTypeOptions that don't have direct Field properties
logical_type_options = odcs_property.logicalTypeOptions if odcs_property.logicalTypeOptions is not None else {}
if logical_type_options.get("minItems") is not None:
config["minItems"] = logical_type_options.get("minItems")
if logical_type_options.get("maxItems") is not None:
config["maxItems"] = logical_type_options.get("maxItems")
if logical_type_options.get("uniqueItems") is not None:
config["uniqueItems"] = logical_type_options.get("uniqueItems")
if logical_type_options.get("multipleOf") is not None:
config["multipleOf"] = logical_type_options.get("multipleOf")
if logical_type_options.get("minProperties") is not None:
config["minProperties"] = logical_type_options.get("minProperties")
if logical_type_options.get("maxProperties") is not None:
config["maxProperties"] = logical_type_options.get("maxProperties")

physical_type = odcs_property.physicalType
if physical_type is not None:
if server_type == "postgres" or server_type == "postgresql":
Expand Down Expand Up @@ -399,6 +414,10 @@ def import_field(
return None

description = odcs_property.description if odcs_property.description is not None else None

# Extract logicalTypeOptions
logical_type_options = odcs_property.logicalTypeOptions if odcs_property.logicalTypeOptions is not None else {}

field = Field(
description=" ".join(description.splitlines()) if description is not None else None,
type=mapped_type,
Expand All @@ -414,7 +433,16 @@ def import_field(
if odcs_property.properties is not None
else {},
config=import_field_config(odcs_property, server_type),
format=getattr(odcs_property, "format", None),
format=logical_type_options.get("format") or getattr(odcs_property, "format", None),
# String constraints
minLength=logical_type_options.get("minLength"),
maxLength=logical_type_options.get("maxLength"),
pattern=logical_type_options.get("pattern"),
# Number constraints
minimum=logical_type_options.get("minimum"),
maximum=logical_type_options.get("maximum"),
exclusiveMinimum=logical_type_options.get("exclusiveMinimum"),
exclusiveMaximum=logical_type_options.get("exclusiveMaximum"),
)

# mapped_type is array
Expand Down
Loading
Loading