Skip to content

Commit 1365bf9

Browse files
feat: Add json_path_expr() method to database adapters (Phase 6 Part 1)
Add json_path_expr() method to support backend-agnostic JSON path extraction: - Add abstract method to DatabaseAdapter base class - Implement for MySQL: json_value(`col`, _utf8mb4'$.path' returning type) - Implement for PostgreSQL: jsonb_extract_path_text("col", 'path_part1', 'path_part2') - Add comprehensive unit tests for both backends This is Part 1 of Phase 6. Parts 2-3 will update condition.py and expression.py to use adapter methods for WHERE clauses and query expression SQL. All tests pass. Fully backward compatible. Part of multi-backend PostgreSQL support implementation. Related: #1338
1 parent 8692c99 commit 1365bf9

File tree

4 files changed

+107
-0
lines changed

4 files changed

+107
-0
lines changed

src/datajoint/adapters/base.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,32 @@ def interval_expr(self, value: int, unit: str) -> str:
683683
"""
684684
...
685685

686+
@abstractmethod
687+
def json_path_expr(self, column: str, path: str, return_type: str | None = None) -> str:
688+
"""
689+
Generate JSON path extraction expression.
690+
691+
Parameters
692+
----------
693+
column : str
694+
Column name containing JSON data.
695+
path : str
696+
JSON path (e.g., 'field' or 'nested.field').
697+
return_type : str, optional
698+
Return type specification (MySQL-specific).
699+
700+
Returns
701+
-------
702+
str
703+
Database-specific JSON extraction SQL expression.
704+
705+
Examples
706+
--------
707+
MySQL: json_value(`column`, _utf8mb4'$.path' returning type)
708+
PostgreSQL: jsonb_extract_path_text("column", 'path_part1', 'path_part2')
709+
"""
710+
...
711+
686712
# =========================================================================
687713
# Error Translation
688714
# =========================================================================

src/datajoint/adapters/mysql.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,35 @@ def interval_expr(self, value: int, unit: str) -> str:
666666
# MySQL uses singular unit names
667667
return f"INTERVAL {value} {unit.upper()}"
668668

669+
def json_path_expr(self, column: str, path: str, return_type: str | None = None) -> str:
670+
"""
671+
Generate MySQL json_value() expression.
672+
673+
Parameters
674+
----------
675+
column : str
676+
Column name containing JSON data.
677+
path : str
678+
JSON path (e.g., 'field' or 'nested.field').
679+
return_type : str, optional
680+
Return type specification (e.g., 'decimal(10,2)').
681+
682+
Returns
683+
-------
684+
str
685+
MySQL json_value() expression.
686+
687+
Examples
688+
--------
689+
>>> adapter.json_path_expr('data', 'field')
690+
"json_value(`data`, _utf8mb4'$.field')"
691+
>>> adapter.json_path_expr('data', 'value', 'decimal(10,2)')
692+
"json_value(`data`, _utf8mb4'$.value' returning decimal(10,2))"
693+
"""
694+
quoted_col = self.quote_identifier(column)
695+
return_clause = f" returning {return_type}" if return_type else ""
696+
return f"json_value({quoted_col}, _utf8mb4'$.{path}'{return_clause})"
697+
669698
# =========================================================================
670699
# Error Translation
671700
# =========================================================================

src/datajoint/adapters/postgres.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,38 @@ def interval_expr(self, value: int, unit: str) -> str:
727727
unit_plural = unit.lower() + "s" if not unit.endswith("s") else unit.lower()
728728
return f"INTERVAL '{value} {unit_plural}'"
729729

730+
def json_path_expr(self, column: str, path: str, return_type: str | None = None) -> str:
731+
"""
732+
Generate PostgreSQL jsonb_extract_path_text() expression.
733+
734+
Parameters
735+
----------
736+
column : str
737+
Column name containing JSON data.
738+
path : str
739+
JSON path (e.g., 'field' or 'nested.field').
740+
return_type : str, optional
741+
Return type specification (not used in PostgreSQL jsonb_extract_path_text).
742+
743+
Returns
744+
-------
745+
str
746+
PostgreSQL jsonb_extract_path_text() expression.
747+
748+
Examples
749+
--------
750+
>>> adapter.json_path_expr('data', 'field')
751+
'jsonb_extract_path_text("data", \\'field\\')'
752+
>>> adapter.json_path_expr('data', 'nested.field')
753+
'jsonb_extract_path_text("data", \\'nested\\', \\'field\\')'
754+
"""
755+
quoted_col = self.quote_identifier(column)
756+
# Split path by '.' for nested access
757+
path_parts = path.split(".")
758+
path_args = ", ".join(f"'{part}'" for part in path_parts)
759+
# Note: PostgreSQL jsonb_extract_path_text doesn't use return type parameter
760+
return f"jsonb_extract_path_text({quoted_col}, {path_args})"
761+
730762
# =========================================================================
731763
# Error Translation
732764
# =========================================================================

tests/unit/test_adapters.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,16 @@ def test_interval_expr(self, adapter):
171171
assert adapter.interval_expr(5, "second") == "INTERVAL 5 SECOND"
172172
assert adapter.interval_expr(10, "minute") == "INTERVAL 10 MINUTE"
173173

174+
def test_json_path_expr(self, adapter):
175+
"""Test JSON path extraction."""
176+
assert adapter.json_path_expr("data", "field") == "json_value(`data`, _utf8mb4'$.field')"
177+
assert adapter.json_path_expr("record", "nested") == "json_value(`record`, _utf8mb4'$.nested')"
178+
179+
def test_json_path_expr_with_return_type(self, adapter):
180+
"""Test JSON path extraction with return type."""
181+
result = adapter.json_path_expr("data", "value", "decimal(10,2)")
182+
assert result == "json_value(`data`, _utf8mb4'$.value' returning decimal(10,2))"
183+
174184
def test_transaction_sql(self, adapter):
175185
"""Test transaction statements."""
176186
assert "START TRANSACTION" in adapter.start_transaction_sql()
@@ -306,6 +316,16 @@ def test_interval_expr(self, adapter):
306316
assert adapter.interval_expr(5, "second") == "INTERVAL '5 seconds'"
307317
assert adapter.interval_expr(10, "minute") == "INTERVAL '10 minutes'"
308318

319+
def test_json_path_expr(self, adapter):
320+
"""Test JSON path extraction for PostgreSQL."""
321+
assert adapter.json_path_expr("data", "field") == "jsonb_extract_path_text(\"data\", 'field')"
322+
assert adapter.json_path_expr("record", "name") == "jsonb_extract_path_text(\"record\", 'name')"
323+
324+
def test_json_path_expr_nested(self, adapter):
325+
"""Test JSON path extraction with nested paths."""
326+
result = adapter.json_path_expr("data", "nested.field")
327+
assert result == "jsonb_extract_path_text(\"data\", 'nested', 'field')"
328+
309329
def test_transaction_sql(self, adapter):
310330
"""Test transaction statements."""
311331
assert adapter.start_transaction_sql() == "BEGIN"

0 commit comments

Comments
 (0)