agentic-data-contracts 0.2.3__tar.gz → 0.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/CHANGELOG.md +17 -0
  2. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/PKG-INFO +21 -2
  3. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/README.md +20 -1
  4. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/pyproject.toml +1 -1
  5. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/adapters/base.py +1 -0
  6. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/adapters/duckdb.py +12 -0
  7. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/core/contract.py +29 -0
  8. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/semantic/base.py +8 -0
  9. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/semantic/cube.py +8 -1
  10. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/semantic/dbt.py +8 -1
  11. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/semantic/yaml_source.py +16 -1
  12. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/tools/factory.py +9 -0
  13. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/fixtures/semantic_source.yml +21 -0
  14. agentic_data_contracts-0.2.5/tests/test_core/test_wildcard_tables.py +106 -0
  15. agentic_data_contracts-0.2.5/tests/test_semantic/test_relationships.py +83 -0
  16. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_semantic/test_yaml_source.py +1 -1
  17. agentic_data_contracts-0.2.5/tests/test_tools/test_wildcard_tools.py +76 -0
  18. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/uv.lock +1 -1
  19. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/.github/dependabot.yml +0 -0
  20. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/.github/workflows/ci.yml +0 -0
  21. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/.gitignore +0 -0
  22. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/.pre-commit-config.yaml +0 -0
  23. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/.python-version +0 -0
  24. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/CLAUDE.md +0 -0
  25. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/LICENSE +0 -0
  26. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/docs/architecture.md +0 -0
  27. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/examples/revenue_agent/agent.py +0 -0
  28. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/examples/revenue_agent/contract.yml +0 -0
  29. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/examples/revenue_agent/semantic.yml +0 -0
  30. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/examples/revenue_agent/setup_db.py +0 -0
  31. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/__init__.py +0 -0
  32. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/adapters/__init__.py +0 -0
  33. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/bridge/__init__.py +0 -0
  34. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/bridge/compiler.py +0 -0
  35. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/core/__init__.py +0 -0
  36. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/core/schema.py +0 -0
  37. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/core/session.py +0 -0
  38. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/py.typed +0 -0
  39. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/semantic/__init__.py +0 -0
  40. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/tools/__init__.py +0 -0
  41. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/tools/middleware.py +0 -0
  42. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/tools/sdk.py +0 -0
  43. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/validation/__init__.py +0 -0
  44. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/validation/checkers.py +0 -0
  45. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/validation/explain.py +0 -0
  46. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/src/agentic_data_contracts/validation/validator.py +0 -0
  47. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/__init__.py +0 -0
  48. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/conftest.py +0 -0
  49. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/fixtures/minimal_contract.yml +0 -0
  50. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/fixtures/sample_cube_schema.yml +0 -0
  51. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/fixtures/sample_dbt_manifest.json +0 -0
  52. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/fixtures/valid_contract.yml +0 -0
  53. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_adapters/__init__.py +0 -0
  54. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_adapters/test_duckdb.py +0 -0
  55. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_bridge/__init__.py +0 -0
  56. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_bridge/test_compiler.py +0 -0
  57. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_core/__init__.py +0 -0
  58. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_core/test_contract.py +0 -0
  59. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_core/test_load_semantic_source.py +0 -0
  60. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_core/test_schema.py +0 -0
  61. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_core/test_sdk_config.py +0 -0
  62. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_core/test_session.py +0 -0
  63. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_core/test_system_prompt_metrics.py +0 -0
  64. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_public_api.py +0 -0
  65. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_semantic/__init__.py +0 -0
  66. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_semantic/test_cube.py +0 -0
  67. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_semantic/test_dbt.py +0 -0
  68. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_semantic/test_search.py +0 -0
  69. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_tools/__init__.py +0 -0
  70. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_tools/test_auto_load.py +0 -0
  71. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_tools/test_factory.py +0 -0
  72. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_tools/test_middleware.py +0 -0
  73. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_tools/test_sdk.py +0 -0
  74. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_tools/test_semantic_tools.py +0 -0
  75. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_validation/__init__.py +0 -0
  76. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_validation/test_checkers.py +0 -0
  77. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_validation/test_explain.py +0 -0
  78. {agentic_data_contracts-0.2.3 → agentic_data_contracts-0.2.5}/tests/test_validation/test_validator.py +0 -0
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.2.5] - 2026-03-29
6
+
7
+ ### Added
8
+
9
+ - **Table relationship metadata**: `Relationship` dataclass and `get_relationships()` on `SemanticSource` protocol for declaring join paths between tables (from/to column + relationship type)
10
+ - **Relationships in system prompt**: `to_system_prompt()` includes join paths so the agent knows how to combine tables correctly
11
+ - **YamlSource relationships**: Parsed from `relationships` section in semantic YAML files
12
+ - DbtSource and CubeSource return empty relationships (ready for future parsing of native join metadata)
13
+
14
+ ## [0.2.4] - 2026-03-29
15
+
16
+ ### Added
17
+
18
+ - **Wildcard table support**: Use `tables: ["*"]` in `allowed_tables` to allow all tables in a schema, discovered from the database at runtime via `adapter.list_tables()`
19
+ - **`DataContract.resolve_tables(adapter)`**: Expands wildcard entries using the database adapter; called automatically by `create_tools()` when an adapter is provided
20
+ - **`DatabaseAdapter.list_tables(schema)`**: New protocol method for listing tables in a schema; implemented in `DuckDBAdapter` via `information_schema.tables`
21
+
5
22
  ## [0.2.3] - 2026-03-29
6
23
 
7
24
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentic-data-contracts
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: YAML-first data contract governance for AI agents
5
5
  Project-URL: Homepage, https://github.com/flyersworder/agentic-data-contracts
6
6
  Project-URL: Repository, https://github.com/flyersworder/agentic-data-contracts
@@ -117,7 +117,9 @@ semantic:
117
117
  path: "./semantic.yml"
118
118
  allowed_tables:
119
119
  - schema: analytics
120
- tables: [orders, customers, subscriptions]
120
+ tables: ["*"] # all tables in schema (discovered from database)
121
+ - schema: marketing
122
+ tables: [campaigns] # or list specific tables
121
123
  forbidden_operations: [DELETE, DROP, TRUNCATE, UPDATE, INSERT]
122
124
  rules:
123
125
  - name: tenant_isolation
@@ -275,6 +277,23 @@ semantic:
275
277
  path: "./cube/schema.yml"
276
278
  ```
277
279
 
280
+ ## Table Relationships
281
+
282
+ Define join paths so the agent knows how to combine tables correctly:
283
+
284
+ ```yaml
285
+ # semantic.yml
286
+ relationships:
287
+ - from: analytics.orders.customer_id
288
+ to: analytics.customers.id
289
+ type: many_to_one
290
+ - from: analytics.orders.product_id
291
+ to: analytics.products.id
292
+ type: many_to_one
293
+ ```
294
+
295
+ The agent sees these in its system prompt and uses them to write correct JOINs instead of guessing from column names.
296
+
278
297
  ## Scalable Metric Discovery
279
298
 
280
299
  For large data lakes with hundreds of KPIs, group metrics by domain and let the agent discover them efficiently:
@@ -64,7 +64,9 @@ semantic:
64
64
  path: "./semantic.yml"
65
65
  allowed_tables:
66
66
  - schema: analytics
67
- tables: [orders, customers, subscriptions]
67
+ tables: ["*"] # all tables in schema (discovered from database)
68
+ - schema: marketing
69
+ tables: [campaigns] # or list specific tables
68
70
  forbidden_operations: [DELETE, DROP, TRUNCATE, UPDATE, INSERT]
69
71
  rules:
70
72
  - name: tenant_isolation
@@ -222,6 +224,23 @@ semantic:
222
224
  path: "./cube/schema.yml"
223
225
  ```
224
226
 
227
+ ## Table Relationships
228
+
229
+ Define join paths so the agent knows how to combine tables correctly:
230
+
231
+ ```yaml
232
+ # semantic.yml
233
+ relationships:
234
+ - from: analytics.orders.customer_id
235
+ to: analytics.customers.id
236
+ type: many_to_one
237
+ - from: analytics.orders.product_id
238
+ to: analytics.products.id
239
+ type: many_to_one
240
+ ```
241
+
242
+ The agent sees these in its system prompt and uses them to write correct JOINs instead of guessing from column names.
243
+
225
244
  ## Scalable Metric Discovery
226
245
 
227
246
  For large data lakes with hundreds of KPIs, group metrics by domain and let the agent discover them efficiently:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agentic-data-contracts"
3
- version = "0.2.3"
3
+ version = "0.2.5"
4
4
  description = "YAML-first data contract governance for AI agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -37,6 +37,7 @@ class DatabaseAdapter(Protocol):
37
37
  def execute(self, sql: str) -> QueryResult: ...
38
38
  def explain(self, sql: str) -> ExplainResult: ...
39
39
  def describe_table(self, schema: str, table: str) -> TableSchema: ...
40
+ def list_tables(self, schema: str) -> list[str]: ...
40
41
 
41
42
  @property
42
43
  def dialect(self) -> str: ...
@@ -59,6 +59,18 @@ class DuckDBAdapter:
59
59
  last_estimate = int(match.group(1))
60
60
  return last_estimate
61
61
 
62
+ def list_tables(self, schema: str) -> list[str]:
63
+ rows = self.connection.execute(
64
+ """
65
+ SELECT table_name
66
+ FROM information_schema.tables
67
+ WHERE table_schema = ?
68
+ ORDER BY table_name
69
+ """,
70
+ [schema],
71
+ ).fetchall()
72
+ return [row[0] for row in rows]
73
+
62
74
  def describe_table(self, schema: str, table: str) -> TableSchema:
63
75
  rows = self.connection.execute(
64
76
  """
@@ -14,6 +14,7 @@ from agentic_data_contracts.core.schema import (
14
14
  )
15
15
 
16
16
  if TYPE_CHECKING:
17
+ from agentic_data_contracts.adapters.base import DatabaseAdapter
17
18
  from agentic_data_contracts.semantic.base import SemanticSource
18
19
 
19
20
 
@@ -38,10 +39,27 @@ class DataContract:
38
39
  schema = DataContractSchema.model_validate(raw)
39
40
  return cls(schema=schema)
40
41
 
42
+ def has_wildcard_tables(self) -> bool:
43
+ """Check if any schema uses wildcard ('*') for tables."""
44
+ return any("*" in entry.tables for entry in self.schema.semantic.allowed_tables)
45
+
46
+ def resolve_tables(self, adapter: DatabaseAdapter) -> None:
47
+ """Expand wildcard tables using the database adapter.
48
+
49
+ Replaces ["*"] entries with actual table names from the database.
50
+ Call this once after creating the adapter. Results are cached
51
+ on the schema object.
52
+ """
53
+ for entry in self.schema.semantic.allowed_tables:
54
+ if "*" in entry.tables:
55
+ entry.tables = adapter.list_tables(entry.schema_)
56
+
41
57
  def allowed_table_names(self) -> list[str]:
42
58
  names: list[str] = []
43
59
  for entry in self.schema.semantic.allowed_tables:
44
60
  for table in entry.tables:
61
+ if table == "*":
62
+ continue # unresolved wildcard — skip
45
63
  names.append(f"{entry.schema_}.{table}")
46
64
  return names
47
65
 
@@ -153,6 +171,17 @@ class DataContract:
153
171
  )
154
172
  sections.append(line)
155
173
 
174
+ # Table relationships
175
+ if semantic_source is not None:
176
+ rels = semantic_source.get_relationships()
177
+ if rels:
178
+ sections.append(
179
+ "\n### Table Relationships\n"
180
+ "Use these join paths when combining tables:"
181
+ )
182
+ for r in rels:
183
+ sections.append(f"- {r.from_} \u2192 {r.to} ({r.type})")
184
+
156
185
  # Resource limits
157
186
  res = self.schema.resources
158
187
  if res:
@@ -20,12 +20,20 @@ class MetricDefinition:
20
20
  filters: list[str] = field(default_factory=list)
21
21
 
22
22
 
23
+ @dataclass
24
+ class Relationship:
25
+ from_: str # "schema.table.column"
26
+ to: str # "schema.table.column"
27
+ type: str = "many_to_one" # many_to_one | one_to_one | many_to_many
28
+
29
+
23
30
  @runtime_checkable
24
31
  class SemanticSource(Protocol):
25
32
  def get_metrics(self) -> list[MetricDefinition]: ...
26
33
  def get_metric(self, name: str) -> MetricDefinition | None: ...
27
34
  def get_table_schema(self, schema: str, table: str) -> TableSchema | None: ...
28
35
  def search_metrics(self, query: str) -> list[MetricDefinition]: ...
36
+ def get_relationships(self) -> list[Relationship]: ...
29
37
 
30
38
 
31
39
  def fuzzy_search_metrics(
@@ -7,7 +7,11 @@ from pathlib import Path
7
7
  import yaml
8
8
 
9
9
  from agentic_data_contracts.adapters.base import Column, TableSchema
10
- from agentic_data_contracts.semantic.base import MetricDefinition, fuzzy_search_metrics
10
+ from agentic_data_contracts.semantic.base import (
11
+ MetricDefinition,
12
+ Relationship,
13
+ fuzzy_search_metrics,
14
+ )
11
15
 
12
16
 
13
17
  class CubeSource:
@@ -54,5 +58,8 @@ class CubeSource:
54
58
  def search_metrics(self, query: str) -> list[MetricDefinition]:
55
59
  return fuzzy_search_metrics(self._metrics, self.get_metric, query)
56
60
 
61
+ def get_relationships(self) -> list[Relationship]:
62
+ return [] # TODO: parse from Cube joins config
63
+
57
64
  def get_table_schema(self, schema: str, table: str) -> TableSchema | None:
58
65
  return self._tables.get(f"{schema}.{table}")
@@ -7,7 +7,11 @@ from pathlib import Path
7
7
  from typing import Any
8
8
 
9
9
  from agentic_data_contracts.adapters.base import Column, TableSchema
10
- from agentic_data_contracts.semantic.base import MetricDefinition, fuzzy_search_metrics
10
+ from agentic_data_contracts.semantic.base import (
11
+ MetricDefinition,
12
+ Relationship,
13
+ fuzzy_search_metrics,
14
+ )
11
15
 
12
16
 
13
17
  class DbtSource:
@@ -77,5 +81,8 @@ class DbtSource:
77
81
  def search_metrics(self, query: str) -> list[MetricDefinition]:
78
82
  return fuzzy_search_metrics(self._metrics, self.get_metric, query)
79
83
 
84
+ def get_relationships(self) -> list[Relationship]:
85
+ return [] # TODO: parse from dbt manifest relationships/refs
86
+
80
87
  def get_table_schema(self, schema: str, table: str) -> TableSchema | None:
81
88
  return self._tables.get(f"{schema}.{table}")
@@ -7,7 +7,11 @@ from pathlib import Path
7
7
  import yaml
8
8
 
9
9
  from agentic_data_contracts.adapters.base import Column, TableSchema
10
- from agentic_data_contracts.semantic.base import MetricDefinition, fuzzy_search_metrics
10
+ from agentic_data_contracts.semantic.base import (
11
+ MetricDefinition,
12
+ Relationship,
13
+ fuzzy_search_metrics,
14
+ )
11
15
 
12
16
 
13
17
  class YamlSource:
@@ -38,6 +42,14 @@ class YamlSource:
38
42
  for c in t.get("columns", [])
39
43
  ]
40
44
  )
45
+ self._relationships = [
46
+ Relationship(
47
+ from_=r["from"],
48
+ to=r["to"],
49
+ type=r.get("type", "many_to_one"),
50
+ )
51
+ for r in raw.get("relationships", [])
52
+ ]
41
53
 
42
54
  def get_metrics(self) -> list[MetricDefinition]:
43
55
  return list(self._metrics)
@@ -51,5 +63,8 @@ class YamlSource:
51
63
  def search_metrics(self, query: str) -> list[MetricDefinition]:
52
64
  return fuzzy_search_metrics(self._metrics, self.get_metric, query)
53
65
 
66
+ def get_relationships(self) -> list[Relationship]:
67
+ return list(self._relationships)
68
+
54
69
  def get_table_schema(self, schema: str, table: str) -> TableSchema | None:
55
70
  return self._tables.get(f"{schema}.{table}")
@@ -41,6 +41,10 @@ def create_tools(
41
41
  if semantic_source is None:
42
42
  semantic_source = contract.load_semantic_source()
43
43
 
44
+ # Resolve wildcard tables if adapter is available
45
+ if adapter is not None and contract.has_wildcard_tables():
46
+ contract.resolve_tables(adapter)
47
+
44
48
  dialect = adapter.dialect if adapter else None
45
49
  validator = Validator(contract, dialect=dialect, explain_adapter=adapter)
46
50
 
@@ -60,6 +64,11 @@ def create_tools(
60
64
  for entry in contract.schema.semantic.allowed_tables:
61
65
  if schema_filter and entry.schema_ != schema_filter:
62
66
  continue
67
+ if "*" in entry.tables:
68
+ return _text_response(
69
+ f"Schema '{entry.schema_}' uses wildcard tables"
70
+ " but no database adapter is available to resolve them."
71
+ )
63
72
  for table in entry.tables:
64
73
  info: dict[str, Any] = {
65
74
  "schema": entry.schema_,
@@ -29,3 +29,24 @@ tables:
29
29
  - name: status
30
30
  type: VARCHAR
31
31
  description: "Order status: pending, completed, cancelled"
32
+ - name: customer_id
33
+ type: INTEGER
34
+ description: "FK to customers"
35
+
36
+ - schema: analytics
37
+ table: customers
38
+ columns:
39
+ - name: id
40
+ type: INTEGER
41
+ description: "Primary key"
42
+ - name: name
43
+ type: VARCHAR
44
+ description: "Customer name"
45
+ - name: region
46
+ type: VARCHAR
47
+ description: "Geographic region"
48
+
49
+ relationships:
50
+ - from: analytics.orders.customer_id
51
+ to: analytics.customers.id
52
+ type: many_to_one
@@ -0,0 +1,106 @@
1
+ """Tests for wildcard table support in allowed_tables."""
2
+
3
+ from agentic_data_contracts.adapters.duckdb import DuckDBAdapter
4
+ from agentic_data_contracts.core.contract import DataContract
5
+ from agentic_data_contracts.core.schema import (
6
+ AllowedTable,
7
+ DataContractSchema,
8
+ SemanticConfig,
9
+ )
10
+
11
+
12
+ def _make_contract(tables_config: list[dict]) -> DataContract:
13
+ allowed = [AllowedTable.model_validate(t) for t in tables_config]
14
+ schema = DataContractSchema(
15
+ name="test",
16
+ semantic=SemanticConfig(allowed_tables=allowed),
17
+ )
18
+ return DataContract(schema)
19
+
20
+
21
+ def _make_adapter() -> DuckDBAdapter:
22
+ db = DuckDBAdapter(":memory:")
23
+ db.connection.execute("""
24
+ CREATE SCHEMA IF NOT EXISTS analytics;
25
+ CREATE TABLE analytics.orders (id INTEGER);
26
+ CREATE TABLE analytics.customers (id INTEGER);
27
+ CREATE TABLE analytics.products (id INTEGER);
28
+ CREATE SCHEMA IF NOT EXISTS raw;
29
+ CREATE TABLE raw.events (id INTEGER);
30
+ """)
31
+ return db
32
+
33
+
34
+ def test_has_wildcard_tables_true() -> None:
35
+ dc = _make_contract([{"schema": "analytics", "tables": ["*"]}])
36
+ assert dc.has_wildcard_tables()
37
+
38
+
39
+ def test_has_wildcard_tables_false() -> None:
40
+ dc = _make_contract(
41
+ [
42
+ {"schema": "analytics", "tables": ["orders"]},
43
+ ]
44
+ )
45
+ assert not dc.has_wildcard_tables()
46
+
47
+
48
+ def test_resolve_tables_expands_wildcard() -> None:
49
+ dc = _make_contract([{"schema": "analytics", "tables": ["*"]}])
50
+ adapter = _make_adapter()
51
+ dc.resolve_tables(adapter)
52
+
53
+ names = dc.allowed_table_names()
54
+ assert "analytics.orders" in names
55
+ assert "analytics.customers" in names
56
+ assert "analytics.products" in names
57
+ assert not any(n.startswith("raw.") for n in names)
58
+
59
+
60
+ def test_resolve_tables_mixed() -> None:
61
+ dc = _make_contract(
62
+ [
63
+ {"schema": "analytics", "tables": ["*"]},
64
+ {"schema": "raw", "tables": []},
65
+ ]
66
+ )
67
+ adapter = _make_adapter()
68
+ dc.resolve_tables(adapter)
69
+
70
+ names = dc.allowed_table_names()
71
+ assert "analytics.orders" in names
72
+ assert not any(n.startswith("raw.") for n in names)
73
+
74
+
75
+ def test_resolve_tables_preserves_explicit() -> None:
76
+ dc = _make_contract(
77
+ [
78
+ {"schema": "analytics", "tables": ["orders"]},
79
+ ]
80
+ )
81
+ adapter = _make_adapter()
82
+ dc.resolve_tables(adapter)
83
+
84
+ names = dc.allowed_table_names()
85
+ assert names == ["analytics.orders"]
86
+
87
+
88
+ def test_unresolved_wildcard_skipped() -> None:
89
+ dc = _make_contract([{"schema": "analytics", "tables": ["*"]}])
90
+ # Without calling resolve_tables, wildcard is skipped
91
+ names = dc.allowed_table_names()
92
+ assert names == []
93
+
94
+
95
+ def test_adapter_list_tables() -> None:
96
+ adapter = _make_adapter()
97
+ tables = adapter.list_tables("analytics")
98
+ assert "orders" in tables
99
+ assert "customers" in tables
100
+ assert "products" in tables
101
+
102
+
103
+ def test_adapter_list_tables_empty_schema() -> None:
104
+ adapter = _make_adapter()
105
+ tables = adapter.list_tables("nonexistent")
106
+ assert tables == []
@@ -0,0 +1,83 @@
1
+ """Tests for table relationship metadata."""
2
+
3
+ from pathlib import Path
4
+
5
+ from agentic_data_contracts.core.contract import DataContract
6
+ from agentic_data_contracts.core.schema import (
7
+ AllowedTable,
8
+ DataContractSchema,
9
+ SemanticConfig,
10
+ )
11
+ from agentic_data_contracts.semantic.cube import CubeSource
12
+ from agentic_data_contracts.semantic.dbt import DbtSource
13
+ from agentic_data_contracts.semantic.yaml_source import YamlSource
14
+
15
+
16
+ def test_yaml_source_loads_relationships(fixtures_dir: Path) -> None:
17
+ source = YamlSource(fixtures_dir / "semantic_source.yml")
18
+ rels = source.get_relationships()
19
+ assert len(rels) == 1
20
+ assert rels[0].from_ == "analytics.orders.customer_id"
21
+ assert rels[0].to == "analytics.customers.id"
22
+ assert rels[0].type == "many_to_one"
23
+
24
+
25
+ def test_yaml_source_no_relationships(tmp_path: Path) -> None:
26
+ (tmp_path / "empty.yml").write_text("metrics: []")
27
+ source = YamlSource(tmp_path / "empty.yml")
28
+ assert source.get_relationships() == []
29
+
30
+
31
+ def test_dbt_source_returns_empty_relationships(
32
+ fixtures_dir: Path,
33
+ ) -> None:
34
+ source = DbtSource(fixtures_dir / "sample_dbt_manifest.json")
35
+ assert source.get_relationships() == []
36
+
37
+
38
+ def test_cube_source_returns_empty_relationships(
39
+ fixtures_dir: Path,
40
+ ) -> None:
41
+ source = CubeSource(fixtures_dir / "sample_cube_schema.yml")
42
+ assert source.get_relationships() == []
43
+
44
+
45
+ def test_system_prompt_includes_relationships(
46
+ fixtures_dir: Path,
47
+ ) -> None:
48
+ source = YamlSource(fixtures_dir / "semantic_source.yml")
49
+ schema = DataContractSchema(
50
+ name="test",
51
+ semantic=SemanticConfig(
52
+ allowed_tables=[
53
+ AllowedTable.model_validate(
54
+ {"schema": "analytics", "tables": ["orders", "customers"]}
55
+ ),
56
+ ],
57
+ ),
58
+ )
59
+ dc = DataContract(schema)
60
+ prompt = dc.to_system_prompt(semantic_source=source)
61
+ assert "Table Relationships" in prompt
62
+ assert "analytics.orders.customer_id" in prompt
63
+ assert "analytics.customers.id" in prompt
64
+ assert "many_to_one" in prompt
65
+
66
+
67
+ def test_system_prompt_no_relationships_when_empty(
68
+ fixtures_dir: Path,
69
+ ) -> None:
70
+ source = DbtSource(fixtures_dir / "sample_dbt_manifest.json")
71
+ schema = DataContractSchema(
72
+ name="test",
73
+ semantic=SemanticConfig(
74
+ allowed_tables=[
75
+ AllowedTable.model_validate(
76
+ {"schema": "analytics", "tables": ["orders"]}
77
+ ),
78
+ ],
79
+ ),
80
+ )
81
+ dc = DataContract(schema)
82
+ prompt = dc.to_system_prompt(semantic_source=source)
83
+ assert "Table Relationships" not in prompt
@@ -39,7 +39,7 @@ def test_get_metric_not_found(source: YamlSource) -> None:
39
39
  def test_get_table_schema(source: YamlSource) -> None:
40
40
  schema = source.get_table_schema("analytics", "orders")
41
41
  assert schema is not None
42
- assert len(schema.columns) == 4
42
+ assert len(schema.columns) == 5
43
43
  col_names = [c.name for c in schema.columns]
44
44
  assert "id" in col_names
45
45
  assert "amount" in col_names
@@ -0,0 +1,76 @@
1
+ """Tests for tools with wildcard table resolution."""
2
+
3
+ import json
4
+
5
+ import pytest
6
+
7
+ from agentic_data_contracts.adapters.duckdb import DuckDBAdapter
8
+ from agentic_data_contracts.core.contract import DataContract
9
+ from agentic_data_contracts.core.schema import (
10
+ AllowedTable,
11
+ DataContractSchema,
12
+ SemanticConfig,
13
+ )
14
+ from agentic_data_contracts.tools.factory import create_tools
15
+
16
+
17
+ @pytest.fixture
18
+ def adapter() -> DuckDBAdapter:
19
+ db = DuckDBAdapter(":memory:")
20
+ db.connection.execute("""
21
+ CREATE SCHEMA IF NOT EXISTS analytics;
22
+ CREATE TABLE analytics.orders (id INTEGER, amount DECIMAL);
23
+ CREATE TABLE analytics.customers (id INTEGER, name VARCHAR);
24
+ INSERT INTO analytics.orders VALUES (1, 100.00);
25
+ """)
26
+ return db
27
+
28
+
29
+ @pytest.fixture
30
+ def wildcard_contract() -> DataContract:
31
+ schema = DataContractSchema(
32
+ name="test",
33
+ semantic=SemanticConfig(
34
+ allowed_tables=[
35
+ AllowedTable.model_validate({"schema": "analytics", "tables": ["*"]}),
36
+ ],
37
+ ),
38
+ )
39
+ return DataContract(schema)
40
+
41
+
42
+ @pytest.mark.asyncio
43
+ async def test_list_tables_after_wildcard_resolve(
44
+ wildcard_contract: DataContract, adapter: DuckDBAdapter
45
+ ) -> None:
46
+ tools = create_tools(wildcard_contract, adapter=adapter)
47
+ tool = next(t for t in tools if t.name == "list_tables")
48
+ result = await tool.callable({})
49
+ text = result["content"][0]["text"]
50
+ data = json.loads(text)
51
+ table_names = [t["table"] for t in data["tables"]]
52
+ assert "orders" in table_names
53
+ assert "customers" in table_names
54
+
55
+
56
+ @pytest.mark.asyncio
57
+ async def test_run_query_with_wildcard_tables(
58
+ wildcard_contract: DataContract, adapter: DuckDBAdapter
59
+ ) -> None:
60
+ tools = create_tools(wildcard_contract, adapter=adapter)
61
+ tool = next(t for t in tools if t.name == "run_query")
62
+ result = await tool.callable({"sql": "SELECT id, amount FROM analytics.orders"})
63
+ text = result["content"][0]["text"]
64
+ assert "100" in text
65
+
66
+
67
+ @pytest.mark.asyncio
68
+ async def test_validate_query_with_wildcard_tables(
69
+ wildcard_contract: DataContract, adapter: DuckDBAdapter
70
+ ) -> None:
71
+ tools = create_tools(wildcard_contract, adapter=adapter)
72
+ tool = next(t for t in tools if t.name == "validate_query")
73
+ # analytics.orders should be allowed after wildcard resolution
74
+ result = await tool.callable({"sql": "SELECT id FROM analytics.orders"})
75
+ text = result["content"][0]["text"]
76
+ assert "valid" in text.lower()
@@ -9,7 +9,7 @@ resolution-markers = [
9
9
 
10
10
  [[package]]
11
11
  name = "agentic-data-contracts"
12
- version = "0.2.3"
12
+ version = "0.2.4"
13
13
  source = { editable = "." }
14
14
  dependencies = [
15
15
  { name = "pydantic" },