modelable 1.0.1__tar.gz → 1.0.2__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.

Potentially problematic release.


This version of modelable might be problematic. Click here for more details.

Files changed (219) hide show
  1. {modelable-1.0.1 → modelable-1.0.2}/PKG-INFO +1 -1
  2. {modelable-1.0.1 → modelable-1.0.2}/pyproject.toml +1 -1
  3. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/rust.py +131 -26
  4. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/typescript.py +3 -4
  5. {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_rust.py +204 -8
  6. {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_typescript.py +36 -5
  7. {modelable-1.0.1 → modelable-1.0.2}/uv.lock +1 -1
  8. {modelable-1.0.1 → modelable-1.0.2}/.gitignore +0 -0
  9. {modelable-1.0.1 → modelable-1.0.2}/.python-version +0 -0
  10. {modelable-1.0.1 → modelable-1.0.2}/LICENSE +0 -0
  11. {modelable-1.0.1 → modelable-1.0.2}/README.md +0 -0
  12. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/__init__.py +0 -0
  13. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/__main__.py +0 -0
  14. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/_pydantic_py314_compat.py +0 -0
  15. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/cli.py +0 -0
  16. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/__init__.py +0 -0
  17. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/apicurio.py +0 -0
  18. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/codegen.py +0 -0
  19. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/common.py +0 -0
  20. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/compile.py +0 -0
  21. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/create.py +0 -0
  22. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/diff.py +0 -0
  23. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/graph.py +0 -0
  24. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/llm.py +0 -0
  25. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/lsp.py +0 -0
  26. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/runtime.py +0 -0
  27. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/scenario.py +0 -0
  28. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/spec.py +0 -0
  29. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/commands/workspace.py +0 -0
  30. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/compat/__init__.py +0 -0
  31. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/compat/checker.py +0 -0
  32. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/compat/diff.py +0 -0
  33. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/compiler/__init__.py +0 -0
  34. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/compiler/compiler.py +0 -0
  35. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/compiler/workspace.py +0 -0
  36. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/diagnostics/__init__.py +0 -0
  37. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/diagnostics/model.py +0 -0
  38. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/__init__.py +0 -0
  39. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/base.py +0 -0
  40. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/csharp.py +0 -0
  41. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/dbt_yaml.py +0 -0
  42. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/diagnostics.py +0 -0
  43. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/fhir.py +0 -0
  44. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/fhir_validator.py +0 -0
  45. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/go.py +0 -0
  46. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/java.py +0 -0
  47. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/json_schema.py +0 -0
  48. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/markdown.py +0 -0
  49. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/odcs.py +0 -0
  50. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/openlineage.py +0 -0
  51. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/openmetadata.py +0 -0
  52. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/python.py +0 -0
  53. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/shapes.py +0 -0
  54. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/sql.py +0 -0
  55. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/emitters/targets.py +0 -0
  56. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/expressions/__init__.py +0 -0
  57. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/expressions/cel.py +0 -0
  58. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/governance/__init__.py +0 -0
  59. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/governance/checker.py +0 -0
  60. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/governance/por.py +0 -0
  61. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/grammar/__init__.py +0 -0
  62. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/grammar/modelable.lark +0 -0
  63. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/graph/__init__.py +0 -0
  64. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/graph/export.py +0 -0
  65. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/__init__.py +0 -0
  66. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/chat.py +0 -0
  67. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/config.py +0 -0
  68. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/context.py +0 -0
  69. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/engine.py +0 -0
  70. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/importers.py +0 -0
  71. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/provenance.py +0 -0
  72. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/providers.py +0 -0
  73. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/qa.py +0 -0
  74. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/recommendations.py +0 -0
  75. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/redaction.py +0 -0
  76. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/render.py +0 -0
  77. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/update_plan.py +0 -0
  78. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/llm/validation_help.py +0 -0
  79. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/__init__.py +0 -0
  80. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/__main__.py +0 -0
  81. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/code_actions.py +0 -0
  82. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/completion.py +0 -0
  83. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/definition.py +0 -0
  84. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/diagnostics.py +0 -0
  85. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/document_symbols.py +0 -0
  86. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/federation.py +0 -0
  87. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/folding.py +0 -0
  88. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/formatting.py +0 -0
  89. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/highlight.py +0 -0
  90. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/hover.py +0 -0
  91. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/inlay_hints.py +0 -0
  92. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/references.py +0 -0
  93. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/rename.py +0 -0
  94. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/semantic_tokens.py +0 -0
  95. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/server.py +0 -0
  96. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/workspace.py +0 -0
  97. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/lsp/workspace_symbols.py +0 -0
  98. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/parser/__init__.py +0 -0
  99. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/parser/ir.py +0 -0
  100. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/parser/parse.py +0 -0
  101. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/parser/transformer.py +0 -0
  102. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/parser/wire.py +0 -0
  103. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/planner/__init__.py +0 -0
  104. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/planner/lineage.py +0 -0
  105. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/planner/planner.py +0 -0
  106. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/planner/plans.py +0 -0
  107. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/py.typed +0 -0
  108. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/__init__.py +0 -0
  109. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/apicurio.py +0 -0
  110. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/base.py +0 -0
  111. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/factory.py +0 -0
  112. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/index.py +0 -0
  113. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/local.py +0 -0
  114. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/oci.py +0 -0
  115. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/resolver.py +0 -0
  116. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/schema.sql +0 -0
  117. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/registry/signature.py +0 -0
  118. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/release.py +0 -0
  119. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/runtime/__init__.py +0 -0
  120. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/runtime/adapter/__init__.py +0 -0
  121. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/runtime/adapter/base.py +0 -0
  122. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/runtime/adapter/postgres.py +0 -0
  123. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/specs/__init__.py +0 -0
  124. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/specs/tracking.py +0 -0
  125. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/validation/__init__.py +0 -0
  126. {modelable-1.0.1 → modelable-1.0.2}/src/modelable/validation/semantic.py +0 -0
  127. {modelable-1.0.1 → modelable-1.0.2}/tests/conftest.py +0 -0
  128. {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/auto_projection_complex.mdl +0 -0
  129. {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/billing_projection.mdl +0 -0
  130. {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/breaking_changes.mdl +0 -0
  131. {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/cel_error_cases.mdl +0 -0
  132. {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/customer.mdl +0 -0
  133. {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/federation_multi_registry/consumer.mdl +0 -0
  134. {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/federation_multi_registry/provider.mdl +0 -0
  135. {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/federation_multi_registry/workspace.mdl +0 -0
  136. {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/fhir_patient_profile.mdl +0 -0
  137. {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/governance_export_model.mdl +0 -0
  138. {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/materialized_projection_chain.mdl +0 -0
  139. {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/missing_header.mdl +0 -0
  140. {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/multi_domain_joins.mdl +0 -0
  141. {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/multi_language_target.mdl +0 -0
  142. {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/no_owner.mdl +0 -0
  143. {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/pii_governance.mdl +0 -0
  144. {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/projection_of_projection.mdl +0 -0
  145. {modelable-1.0.1 → modelable-1.0.2}/tests/fixtures/sql_and_dbt_targets.mdl +0 -0
  146. {modelable-1.0.1 → modelable-1.0.2}/tests/helpers.py +0 -0
  147. {modelable-1.0.1 → modelable-1.0.2}/tests/test_apicurio_registry.py +0 -0
  148. {modelable-1.0.1 → modelable-1.0.2}/tests/test_apicurio_testcontainers.py +0 -0
  149. {modelable-1.0.1 → modelable-1.0.2}/tests/test_auto_projection.py +0 -0
  150. {modelable-1.0.1 → modelable-1.0.2}/tests/test_cel_validation.py +0 -0
  151. {modelable-1.0.1 → modelable-1.0.2}/tests/test_cli.py +0 -0
  152. {modelable-1.0.1 → modelable-1.0.2}/tests/test_cli_create.py +0 -0
  153. {modelable-1.0.1 → modelable-1.0.2}/tests/test_cli_inspect.py +0 -0
  154. {modelable-1.0.1 → modelable-1.0.2}/tests/test_codegen_docker_smoke.py +0 -0
  155. {modelable-1.0.1 → modelable-1.0.2}/tests/test_codegen_targets.py +0 -0
  156. {modelable-1.0.1 → modelable-1.0.2}/tests/test_compatibility.py +0 -0
  157. {modelable-1.0.1 → modelable-1.0.2}/tests/test_compiler.py +0 -0
  158. {modelable-1.0.1 → modelable-1.0.2}/tests/test_conformance_fixture.py +0 -0
  159. {modelable-1.0.1 → modelable-1.0.2}/tests/test_diagnostics.py +0 -0
  160. {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_csharp.py +0 -0
  161. {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_dbt_yaml.py +0 -0
  162. {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_fhir.py +0 -0
  163. {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_go.py +0 -0
  164. {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_java.py +0 -0
  165. {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_json_schema.py +0 -0
  166. {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_markdown.py +0 -0
  167. {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_odcs.py +0 -0
  168. {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_openlineage.py +0 -0
  169. {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_openmetadata.py +0 -0
  170. {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_python.py +0 -0
  171. {modelable-1.0.1 → modelable-1.0.2}/tests/test_emit_sql.py +0 -0
  172. {modelable-1.0.1 → modelable-1.0.2}/tests/test_fhir_validator.py +0 -0
  173. {modelable-1.0.1 → modelable-1.0.2}/tests/test_fixtures_regression.py +0 -0
  174. {modelable-1.0.1 → modelable-1.0.2}/tests/test_governance.py +0 -0
  175. {modelable-1.0.1 → modelable-1.0.2}/tests/test_grammar.py +0 -0
  176. {modelable-1.0.1 → modelable-1.0.2}/tests/test_graph_export.py +0 -0
  177. {modelable-1.0.1 → modelable-1.0.2}/tests/test_impact_analysis.py +0 -0
  178. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lineage.py +0 -0
  179. {modelable-1.0.1 → modelable-1.0.2}/tests/test_llm_features.py +0 -0
  180. {modelable-1.0.1 → modelable-1.0.2}/tests/test_llm_provider_integration.py +0 -0
  181. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_code_actions.py +0 -0
  182. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_completion.py +0 -0
  183. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_completion_mirrors.py +0 -0
  184. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_definition.py +0 -0
  185. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_diagnostics.py +0 -0
  186. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_document_symbols.py +0 -0
  187. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_features.py +0 -0
  188. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_federation_diagnostics.py +0 -0
  189. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_folding.py +0 -0
  190. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_formatting.py +0 -0
  191. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_highlight.py +0 -0
  192. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_hover.py +0 -0
  193. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_inlay_hints.py +0 -0
  194. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_integration.py +0 -0
  195. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_references.py +0 -0
  196. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_rename.py +0 -0
  197. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_semantic_tokens.py +0 -0
  198. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_server.py +0 -0
  199. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_workspace.py +0 -0
  200. {modelable-1.0.1 → modelable-1.0.2}/tests/test_lsp_workspace_symbols.py +0 -0
  201. {modelable-1.0.1 → modelable-1.0.2}/tests/test_openmetadata_testcontainers.py +0 -0
  202. {modelable-1.0.1 → modelable-1.0.2}/tests/test_planner.py +0 -0
  203. {modelable-1.0.1 → modelable-1.0.2}/tests/test_por.py +0 -0
  204. {modelable-1.0.1 → modelable-1.0.2}/tests/test_postgres_adapter.py +0 -0
  205. {modelable-1.0.1 → modelable-1.0.2}/tests/test_py314_compat.py +0 -0
  206. {modelable-1.0.1 → modelable-1.0.2}/tests/test_registry.py +0 -0
  207. {modelable-1.0.1 → modelable-1.0.2}/tests/test_registry_index.py +0 -0
  208. {modelable-1.0.1 → modelable-1.0.2}/tests/test_release_metadata.py +0 -0
  209. {modelable-1.0.1 → modelable-1.0.2}/tests/test_release_workflow.py +0 -0
  210. {modelable-1.0.1 → modelable-1.0.2}/tests/test_repository_release.py +0 -0
  211. {modelable-1.0.1 → modelable-1.0.2}/tests/test_resolver.py +0 -0
  212. {modelable-1.0.1 → modelable-1.0.2}/tests/test_samples.py +0 -0
  213. {modelable-1.0.1 → modelable-1.0.2}/tests/test_semantic.py +0 -0
  214. {modelable-1.0.1 → modelable-1.0.2}/tests/test_serialization_hints.py +0 -0
  215. {modelable-1.0.1 → modelable-1.0.2}/tests/test_spec_tracking.py +0 -0
  216. {modelable-1.0.1 → modelable-1.0.2}/tests/test_transformer.py +0 -0
  217. {modelable-1.0.1 → modelable-1.0.2}/tests/test_validate_surface_detection.py +0 -0
  218. {modelable-1.0.1 → modelable-1.0.2}/tests/test_version_range_resolution.py +0 -0
  219. {modelable-1.0.1 → modelable-1.0.2}/tests/test_workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modelable
3
- Version: 1.0.1
3
+ Version: 1.0.2
4
4
  Summary: Compiler and language server for versioned, domain-owned data models
5
5
  Project-URL: Homepage, https://github.com/ktjn/modelable
6
6
  Project-URL: Documentation, https://github.com/ktjn/modelable/tree/main/docs
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "modelable"
7
- version = "1.0.1"
7
+ version = "1.0.2"
8
8
  description = "Compiler and language server for versioned, domain-owned data models"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -9,7 +9,16 @@ from modelable.compiler.workspace import Workspace
9
9
  from modelable.emitters.base import EmittedArtifact, compute_content_hash
10
10
  from modelable.emitters.diagnostics import missing_metadata, type_loss
11
11
  from modelable.emitters.shapes import TypeShape
12
- from modelable.parser.ir import DirectMapping, DomainDef, MdlFile, ModelVersion, NamedType, ProjectionVersion
12
+ from modelable.parser.ir import (
13
+ ArrayType,
14
+ DirectMapping,
15
+ DomainDef,
16
+ MapType,
17
+ MdlFile,
18
+ ModelVersion,
19
+ NamedType,
20
+ ProjectionVersion,
21
+ )
13
22
  from modelable.registry.resolver import resolve_model_ref
14
23
 
15
24
 
@@ -27,33 +36,35 @@ def _append_cross_enum_from_impls(
27
36
  enum_registry: dict[str, dict],
28
37
  ) -> list[EmittedArtifact]:
29
38
  """For each pair of enum types with identical variants in the same domain,
30
- append From impl blocks so the types convert into each other without manual match arms.
39
+ append From impl blocks into projection files without manual match arms.
31
40
  """
32
- # Group by domain: domain -> [(artifact_id, module_name, enums_dict)]
33
- by_domain: dict[str, list[tuple[str, str, dict[str, list[str]]]]] = {}
41
+ # Group by domain: domain -> [(artifact_id, module_name, enums_dict, kind)]
42
+ by_domain: dict[str, list[tuple[str, str, dict[str, list[str]], str]]] = {}
34
43
  for art_id, info in enum_registry.items():
35
44
  domain = info["domain"]
36
- by_domain.setdefault(domain, []).append((art_id, info["module_name"], info["enums"]))
45
+ by_domain.setdefault(domain, []).append((art_id, info["module_name"], info["enums"], info["kind"]))
37
46
 
38
47
  # Build: frozenset(raw_variants) -> [(artifact_id, module_name, enum_type_name)]
39
48
  # Per domain — From impls only work within the same Rust module tree (super:: path).
40
49
  extra: dict[str, list[str]] = {} # artifact_id -> lines to append
41
50
  for _domain, entries in by_domain.items():
42
- variant_map: dict[frozenset, list[tuple[str, str, str]]] = {}
43
- for art_id, module_name, enums in entries:
51
+ variant_map: dict[frozenset, list[tuple[str, str, str, str]]] = {}
52
+ for art_id, module_name, enums, kind in entries:
44
53
  for enum_type_name, raw_variants in enums.items():
45
54
  key = frozenset(raw_variants)
46
- variant_map.setdefault(key, []).append((art_id, module_name, enum_type_name))
55
+ variant_map.setdefault(key, []).append((art_id, module_name, enum_type_name, kind))
47
56
 
48
57
  for variant_set, enum_list in variant_map.items():
49
58
  if len(enum_list) < 2:
50
59
  continue
51
60
  sorted_variants = sorted(variant_set)
52
61
  # For each ordered pair (src → tgt): put From<src> for tgt in tgt's file.
53
- for src_art_id, src_module, src_enum in enum_list:
54
- for tgt_art_id, _tgt_module, tgt_enum in enum_list:
62
+ for src_art_id, src_module, src_enum, _src_kind in enum_list:
63
+ for tgt_art_id, _tgt_module, tgt_enum, tgt_kind in enum_list:
55
64
  if src_art_id == tgt_art_id and src_enum == tgt_enum:
56
65
  continue
66
+ if tgt_kind == "model":
67
+ continue
57
68
  lines: list[str] = [
58
69
  "",
59
70
  f"use super::{src_module}::{src_enum};",
@@ -100,7 +111,9 @@ def emit_rust(workspace: Workspace, out_dir: Path) -> list[EmittedArtifact]:
100
111
  for domain in workspace.mdl.domains:
101
112
  for model_name, versions in domain.models.items():
102
113
  for version in versions:
103
- artifacts.append(_emit_model(domain, model_name, version, out_dir, enum_registry=enum_registry))
114
+ artifacts.append(
115
+ _emit_model(domain, model_name, version, out_dir, enum_registry=enum_registry, mdl=workspace.mdl)
116
+ )
104
117
  for projection_name, versions in domain.projections.items():
105
118
  for version in versions:
106
119
  source = version.source.model
@@ -157,6 +170,40 @@ def _stable_type_name(domain: str, name: str, version: int) -> str:
157
170
  return f"{_pascalize(domain)}{_pascalize(name)}V{version}"
158
171
 
159
172
 
173
+ def _collect_named_type_refs(field_type, result: set) -> None:
174
+ """Recursively collect NamedType names from a field type."""
175
+ if isinstance(field_type, NamedType):
176
+ result.add(field_type.name)
177
+ elif isinstance(field_type, ArrayType):
178
+ _collect_named_type_refs(field_type.item, result)
179
+ elif isinstance(field_type, MapType):
180
+ _collect_named_type_refs(field_type.key, result)
181
+ _collect_named_type_refs(field_type.value, result)
182
+
183
+
184
+ def _resolve_named_type_map(named_refs: set, mdl: MdlFile | None) -> tuple[dict[str, str], list[str]]:
185
+ """Resolve NamedType references to Rust type names from the workspace.
186
+
187
+ Returns (name -> rust_type_name, list of use statements).
188
+ """
189
+ if not named_refs or mdl is None:
190
+ return {}, []
191
+ resolved_map: dict[str, str] = {}
192
+ use_statements: list[str] = []
193
+ for name in named_refs:
194
+ for domain in mdl.domains:
195
+ if name in domain.models:
196
+ versions = domain.models[name]
197
+ if versions:
198
+ latest = versions[-1]
199
+ rust_name = _stable_type_name(domain.name, name, latest.version)
200
+ module = _snake_case(rust_name)
201
+ resolved_map[name] = rust_name
202
+ use_statements.append(f"use super::{module}::{rust_name};")
203
+ break
204
+ return resolved_map, use_statements
205
+
206
+
160
207
  def _emit_model(
161
208
  domain: DomainDef,
162
209
  model_name: str,
@@ -164,30 +211,46 @@ def _emit_model(
164
211
  out_dir: Path,
165
212
  *,
166
213
  enum_registry: dict[str, dict] | None = None,
214
+ mdl: MdlFile | None = None,
167
215
  ) -> EmittedArtifact:
168
216
  artifact_id = _artifact_id(domain.name, model_name, version.version)
169
217
  type_name = _stable_type_name(domain.name, model_name, version.version)
170
218
  nested_definitions: dict[str, list[str]] = {}
171
219
  local_enum_info: dict[str, list[str]] = {}
220
+
221
+ # Resolve NamedType references from the workspace
222
+ named_refs: set[str] = set()
223
+ for field in version.fields:
224
+ _collect_named_type_refs(field.type, named_refs)
225
+ named_type_map, use_statements = _resolve_named_type_map(named_refs, mdl)
226
+
172
227
  field_specs = _field_specs_from_model_fields(
173
- version.fields, owner_type=type_name, path=[], definitions=nested_definitions, enum_info=local_enum_info
228
+ version.fields,
229
+ owner_type=type_name,
230
+ path=[],
231
+ definitions=nested_definitions,
232
+ enum_info=local_enum_info,
233
+ named_type_map=named_type_map,
174
234
  )
175
235
  if enum_registry is not None:
176
236
  enum_registry[artifact_id] = {
177
237
  "enums": local_enum_info,
178
238
  "module_name": _snake_case(type_name),
179
239
  "domain": domain.name,
240
+ "kind": "model",
180
241
  }
181
242
 
182
243
  warnings: list[str] = []
183
244
  for field in version.fields:
184
- if isinstance(field.type, NamedType):
245
+ if isinstance(field.type, NamedType) and field.type.name not in named_type_map:
185
246
  warnings.append(missing_metadata(f"{domain.name}.{model_name}.{field.name}"))
186
247
 
187
248
  needs_serde_with = _any_needs_serde_with(field_specs)
188
249
  needs_uuid = _any_needs_uuid(field_specs)
189
250
  needs_serde_json = _any_needs_serde_json(field_specs)
190
- lines = _header_lines(serde_with=needs_serde_with, uuid=needs_uuid, serde_json=needs_serde_json)
251
+ lines = _header_lines(
252
+ serde_with=needs_serde_with, uuid=needs_uuid, serde_json=needs_serde_json, extra_uses=use_statements
253
+ )
191
254
  lines.extend(_render_struct_definition(type_name, field_specs))
192
255
  lines.extend(_render_nested_definitions(nested_definitions))
193
256
 
@@ -228,18 +291,23 @@ def _emit_projection(
228
291
  field_specs.append(_FieldSpec(index=index, name=field.name, annotation="String", optional=False))
229
292
  continue
230
293
  wire = _resolve_merged_projection_wire(field, version, mdl)
231
- annotation = _shape_annotation(
232
- field_shape,
233
- owner_type=type_name,
234
- path=[field.name],
235
- definitions=nested_definitions,
236
- rust_hint=wire.get("rust"),
237
- clickhouse_hint=wire.get("clickhouse"),
238
- enum_info=local_enum_info,
239
- )
294
+ if clickhouse_row and field_shape.kind == "enum":
295
+ # clickhouse-rs 0.15 panics on serialize_unit_variant for typed enums;
296
+ # force String for all ClickHouse-bound enum fields.
297
+ annotation = "String"
298
+ else:
299
+ annotation = _shape_annotation(
300
+ field_shape,
301
+ owner_type=type_name,
302
+ path=[field.name],
303
+ definitions=nested_definitions,
304
+ rust_hint=wire.get("rust"),
305
+ clickhouse_hint=wire.get("clickhouse"),
306
+ enum_info=local_enum_info,
307
+ )
240
308
  optional = field_shape.optional or field_shape.nullable
241
309
  serde_attrs = _serde_attrs_for_field(wire, field_shape, clickhouse=clickhouse_row)
242
- if field_shape.optional:
310
+ if field_shape.optional and not clickhouse_row:
243
311
  serde_attrs = ['#[serde(skip_serializing_if = "Option::is_none")]', *serde_attrs]
244
312
  field_specs.append(
245
313
  _FieldSpec(index=index, name=field.name, annotation=annotation, optional=optional, serde_attrs=serde_attrs)
@@ -267,13 +335,18 @@ def _emit_projection(
267
335
  _render_struct_definition(type_name, field_specs, extra_derives=extra_derives, storage_gated=storage_gated)
268
336
  )
269
337
  lines.extend(_render_nested_definitions(nested_definitions))
270
- lines.extend(_emit_from_impl(type_name, domain.name, version, mdl, storage_gated=storage_gated))
338
+ lines.extend(
339
+ _emit_from_impl(
340
+ type_name, domain.name, version, mdl, storage_gated=storage_gated, clickhouse_row=clickhouse_row
341
+ )
342
+ )
271
343
 
272
344
  if enum_registry is not None:
273
345
  enum_registry[artifact_id] = {
274
346
  "enums": local_enum_info,
275
347
  "module_name": _snake_case(type_name),
276
348
  "domain": domain.name,
349
+ "kind": "projection",
277
350
  }
278
351
 
279
352
  text = "\n".join(lines) + "\n"
@@ -320,6 +393,7 @@ def _emit_from_impl(
320
393
  mdl: MdlFile,
321
394
  *,
322
395
  storage_gated: bool = False,
396
+ clickhouse_row: bool = False,
323
397
  ) -> list[str]:
324
398
  """Emit impl From<SourceModel> for Projection.
325
399
 
@@ -352,6 +426,16 @@ def _emit_from_impl(
352
426
  if storage_gated:
353
427
  lines.append('#[cfg(feature = "storage")]')
354
428
  lines.append(f"use super::{src_module}::{src_type_name};")
429
+ if clickhouse_row:
430
+ for proj_field in version.fields:
431
+ if not isinstance(proj_field.mapping, DirectMapping):
432
+ continue
433
+ field_shape = _resolve_projection_field_shape(proj_field, version, mdl)
434
+ if field_shape is not None and field_shape.kind == "enum":
435
+ enum_type = _nested_type_name(src_type_name, [proj_field.mapping.source_field])
436
+ if storage_gated:
437
+ lines.append('#[cfg(feature = "storage")]')
438
+ lines.append(f"use super::{src_module}::{enum_type};")
355
439
  if storage_gated:
356
440
  lines.append('#[cfg(feature = "storage")]')
357
441
  lines.append(f"impl From<{src_type_name}> for {proj_type_name} {{")
@@ -363,7 +447,15 @@ def _emit_from_impl(
363
447
  if isinstance(proj_field.mapping, DirectMapping):
364
448
  src_rust_name = _field_name(proj_field.mapping.source_field)
365
449
  field_shape = _resolve_projection_field_shape(proj_field, version, mdl)
366
- if _projection_field_is_json_passthrough_to_string(proj_field, version, mdl):
450
+ if clickhouse_row and field_shape is not None and field_shape.kind == "enum":
451
+ # ClickHouse-bound enum fields are stored as String; generate explicit match.
452
+ src_enum_type = _nested_type_name(src_type_name, [proj_field.mapping.source_field])
453
+ lines.append(f" {rust_name}: match src.{src_rust_name} {{")
454
+ for raw_v in field_shape.enum_values:
455
+ member = _enum_member_name(raw_v)
456
+ lines.append(f' {src_enum_type}::{member} => "{raw_v}".to_string(),')
457
+ lines.append(" },")
458
+ elif _projection_field_is_json_passthrough_to_string(proj_field, version, mdl):
367
459
  lines.append(
368
460
  f" {rust_name}: serde_json::to_string(&src.{src_rust_name}).unwrap_or_default(),"
369
461
  )
@@ -435,6 +527,7 @@ def _header_lines(
435
527
  clickhouse: bool = False,
436
528
  uuid: bool = False,
437
529
  serde_json: bool = False,
530
+ extra_uses: list[str] | None = None,
438
531
  ) -> list[str]:
439
532
  lines = [
440
533
  "// @generated by Modelable",
@@ -451,6 +544,9 @@ def _header_lines(
451
544
  lines.insert(1, "// requires: uuid (https://docs.rs/uuid)")
452
545
  if serde_json:
453
546
  lines.insert(1, "// requires: serde_json (https://docs.rs/serde_json)")
547
+ if extra_uses:
548
+ # Insert use statements just before the trailing empty string
549
+ lines[-1:-1] = extra_uses
454
550
  return lines
455
551
 
456
552
 
@@ -540,6 +636,7 @@ def _field_specs_from_model_fields(
540
636
  path: list[str],
541
637
  definitions: dict[str, list[str]],
542
638
  enum_info: dict[str, list[str]] | None = None,
639
+ named_type_map: dict[str, str] | None = None,
543
640
  ) -> list[_FieldSpec]:
544
641
  specs: list[_FieldSpec] = []
545
642
  for index, field in enumerate(fields):
@@ -552,6 +649,7 @@ def _field_specs_from_model_fields(
552
649
  definitions=definitions,
553
650
  rust_hint=wire.get("rust"),
554
651
  enum_info=enum_info,
652
+ named_type_map=named_type_map,
555
653
  )
556
654
  is_optional = shape.optional or shape.nullable
557
655
  serde_attrs = _serde_attrs_for_field(wire, shape)
@@ -616,6 +714,7 @@ def _shape_annotation(
616
714
  rust_hint=None,
617
715
  clickhouse_hint=None,
618
716
  enum_info: dict[str, list[str]] | None = None,
717
+ named_type_map: dict[str, str] | None = None,
619
718
  ) -> str:
620
719
  base = _shape_base_annotation(
621
720
  shape,
@@ -625,6 +724,7 @@ def _shape_annotation(
625
724
  rust_hint=rust_hint,
626
725
  clickhouse_hint=clickhouse_hint,
627
726
  enum_info=enum_info,
727
+ named_type_map=named_type_map,
628
728
  )
629
729
  if shape.optional or shape.nullable:
630
730
  return f"Option<{base}>"
@@ -640,6 +740,7 @@ def _shape_base_annotation(
640
740
  rust_hint=None,
641
741
  clickhouse_hint=None,
642
742
  enum_info: dict[str, list[str]] | None = None,
743
+ named_type_map: dict[str, str] | None = None,
643
744
  ) -> str:
644
745
  clickhouse_string = clickhouse_hint is not None and getattr(clickhouse_hint, "encoding", None) == "string"
645
746
  if shape.kind == "primitive":
@@ -659,6 +760,7 @@ def _shape_base_annotation(
659
760
  definitions=definitions,
660
761
  rust_hint=rust_hint,
661
762
  enum_info=enum_info,
763
+ named_type_map=named_type_map,
662
764
  )
663
765
  return f"Vec<{element_type}>"
664
766
  if shape.kind == "map":
@@ -671,6 +773,7 @@ def _shape_base_annotation(
671
773
  path=[*path, "Value"],
672
774
  definitions=definitions,
673
775
  enum_info=enum_info,
776
+ named_type_map=named_type_map,
674
777
  )
675
778
  return f"HashMap<String, {value_type}>"
676
779
  if shape.kind == "ref":
@@ -683,6 +786,8 @@ def _shape_base_annotation(
683
786
  enum_info[enum_type_name] = list(shape.enum_values)
684
787
  return enum_type_name
685
788
  if shape.kind == "named":
789
+ if named_type_map is not None and shape.ref in named_type_map:
790
+ return named_type_map[shape.ref]
686
791
  return _pascalize(shape.ref or "Named")
687
792
  if shape.kind == "object":
688
793
  type_name = _nested_type_name(owner_type, path)
@@ -93,7 +93,7 @@ def _collect_ref_imports(field_type, mdl, resolved_refs: dict[str, str]) -> None
93
93
 
94
94
 
95
95
  def _collect_named_imports(field_type, mdl, named_imports: dict[str, tuple[str, str]]) -> None:
96
- """Recursively resolve NamedType refs to (stable_iface_name, artifact_id) in named_imports."""
96
+ """Recursively resolve NamedType refs to (exported_alias, artifact_id) in named_imports."""
97
97
  if isinstance(field_type, NamedType):
98
98
  name = field_type.name
99
99
  if name not in named_imports and mdl is not None:
@@ -102,9 +102,8 @@ def _collect_named_imports(field_type, mdl, named_imports: dict[str, tuple[str,
102
102
  versions = domain.models[name]
103
103
  if versions:
104
104
  latest = max(versions, key=lambda v: v.version)
105
- iface = _stable_interface_name(domain.name, name, latest.version)
106
105
  aid = _artifact_id(domain.name, name, latest.version)
107
- named_imports[name] = (iface, aid)
106
+ named_imports[name] = (name, aid)
108
107
  return
109
108
  elif isinstance(field_type, ArrayType):
110
109
  _collect_named_imports(field_type.item, mdl, named_imports)
@@ -160,7 +159,7 @@ def _emit_model(domain: DomainDef, model_name: str, version: ModelVersion, out_d
160
159
  body_lines.append("}")
161
160
  body_lines.append(f"export type {model_name} = {interface_name};")
162
161
 
163
- all_lines = import_lines + ([""] if import_lines else []) + meta_lines + body_lines
162
+ all_lines = meta_lines + ([*import_lines, ""] if import_lines else []) + body_lines
164
163
  content = "\n".join(all_lines) + "\n"
165
164
  return EmittedArtifact(
166
165
  target="typescript",
@@ -461,6 +461,55 @@ binding span-row-binding {
461
461
  assert '#[cfg(feature = "storage")]' not in model.content
462
462
 
463
463
 
464
+ def test_emit_rust_clickhouse_row_omits_skip_serializing_if_on_optional_fields(tmp_path):
465
+ """Nullable ClickHouse row columns must serialize as NULL, not disappear."""
466
+ (tmp_path / "model.mdl").write_text(
467
+ """
468
+ domain telemetry {
469
+ owner: "test-team"
470
+ entity Log @ 1 (additive) {
471
+ @key logId: uuid
472
+ traceId?: string
473
+ message: string
474
+ }
475
+
476
+ projection LogRow @ 1
477
+ from telemetry.Log @ 1 as l
478
+ {
479
+ logId <- l.logId
480
+ traceId <- l.traceId
481
+ message <- l.message
482
+ }
483
+ }
484
+ """,
485
+ encoding="utf-8",
486
+ )
487
+ (tmp_path / "bindings.mdl").write_text(
488
+ """
489
+ binding ch-conn {
490
+ adapter: clickhouse
491
+ }
492
+
493
+ binding log-row-binding {
494
+ model: telemetry.Log @ 1
495
+ adapter: ch-conn
496
+ table: "logs"
497
+ }
498
+ """,
499
+ encoding="utf-8",
500
+ )
501
+ workspace = load_workspace(tmp_path)
502
+ artifacts = emit_rust(workspace, tmp_path / "out")
503
+
504
+ row = next(a for a in artifacts if a.ref == "telemetry.LogRow@1")
505
+ assert "clickhouse::Row" in row.content
506
+ assert "pub trace_id: Option<String>," in row.content
507
+ assert '#[serde(skip_serializing_if = "Option::is_none")]' not in row.content
508
+
509
+ model = next(a for a in artifacts if a.ref == "telemetry.Log@1")
510
+ assert '#[serde(skip_serializing_if = "Option::is_none")]' in model.content
511
+
512
+
464
513
  def test_emit_rust_clickhouse_row_indirect_connector_binding(tmp_path):
465
514
  """Model binding via connector-binding name → adapter type resolved correctly for clickhouse."""
466
515
  (tmp_path / "all.mdl").write_text(
@@ -1002,17 +1051,14 @@ domain catalog {
1002
1051
 
1003
1052
 
1004
1053
  def test_emit_rust_warns_on_named_type_field(tmp_path):
1005
- """Rust emitter emits EMIT003 for NamedType field references (issue #120)."""
1054
+ """Rust emitter emits EMIT003 for NamedType field references to unknown external types (issue #120)."""
1006
1055
  (tmp_path / "test.mdl").write_text(
1007
1056
  """
1008
1057
  domain customer {
1009
1058
  owner: "test-team"
1010
- value Address {
1011
- line1: string
1012
- }
1013
1059
  entity Customer @ 1 (additive) {
1014
1060
  @key customerId: uuid
1015
- address: Address
1061
+ address: ExternalAddress
1016
1062
  }
1017
1063
  }
1018
1064
  """,
@@ -1084,6 +1130,156 @@ domain tracing {
1084
1130
  assert "impl From<TracingSpanV1SpanKind> for TracingSpanRowV1SpanKind" in proj_art.content
1085
1131
  assert "use super::tracing_span_v1::TracingSpanV1SpanKind;" in proj_art.content
1086
1132
 
1087
- # Model file gets From<ProjEnum> for ModelEnum
1088
- assert "impl From<TracingSpanRowV1SpanKind> for TracingSpanV1SpanKind" in model_art.content
1089
- assert "use super::tracing_span_row_v1::TracingSpanRowV1SpanKind;" in model_art.content
1133
+ # Domain model files must not import projection/storage enum types.
1134
+ assert "TracingSpanRowV1SpanKind" not in model_art.content
1135
+ assert "use super::tracing_span_row_v1" not in model_art.content
1136
+
1137
+
1138
+ def test_emit_rust_named_type_in_same_domain_resolves_without_warning(tmp_path):
1139
+ """NamedType references to models in the same workspace resolve without EMIT003 (issue #120)."""
1140
+ (tmp_path / "test.mdl").write_text(
1141
+ """
1142
+ domain nlq {
1143
+ owner: "test-team"
1144
+ value NlqTimeRange {
1145
+ from: string
1146
+ to: string
1147
+ }
1148
+ value NlqIr {
1149
+ timeRange: NlqTimeRange
1150
+ query: string
1151
+ }
1152
+ }
1153
+ """,
1154
+ encoding="utf-8",
1155
+ )
1156
+ workspace = load_workspace(tmp_path)
1157
+ artifacts = emit_rust(workspace, tmp_path / "out")
1158
+ ir_art = next(a for a in artifacts if a.ref == "nlq.NlqIr@0")
1159
+ # The field must use the stable Rust type name (value types without @ N default to version 0)
1160
+ assert "pub time_range: NlqNlqTimeRangeV0," in ir_art.content
1161
+ # A use statement must be emitted
1162
+ assert "use super::nlq_nlq_time_range_v0::NlqNlqTimeRangeV0;" in ir_art.content
1163
+ # No EMIT003 warning
1164
+ assert not any("EMIT003" in w for w in ir_art.warnings)
1165
+
1166
+
1167
+ def test_emit_rust_named_type_array_in_same_domain_resolves(tmp_path):
1168
+ """NamedType inside array<...> resolves to Vec<StableTypeName> (issue #120)."""
1169
+ (tmp_path / "test.mdl").write_text(
1170
+ """
1171
+ domain nlq {
1172
+ owner: "test-team"
1173
+ value NlqFilter {
1174
+ field: string
1175
+ value: string
1176
+ }
1177
+ value NlqQuery {
1178
+ filters: array<NlqFilter>
1179
+ query: string
1180
+ }
1181
+ }
1182
+ """,
1183
+ encoding="utf-8",
1184
+ )
1185
+ workspace = load_workspace(tmp_path)
1186
+ artifacts = emit_rust(workspace, tmp_path / "out")
1187
+ query_art = next(a for a in artifacts if a.ref == "nlq.NlqQuery@0")
1188
+ assert "pub filters: Vec<NlqNlqFilterV0>," in query_art.content
1189
+ assert "use super::nlq_nlq_filter_v0::NlqNlqFilterV0;" in query_art.content
1190
+ assert not any("EMIT003" in w for w in query_art.warnings)
1191
+
1192
+
1193
+ def test_emit_rust_clickhouse_row_enum_field_uses_string(tmp_path):
1194
+ """ClickHouse-bound projection enum fields must be String, not typed enum (issue #119)."""
1195
+ (tmp_path / "model.mdl").write_text(
1196
+ """
1197
+ domain telemetry {
1198
+ owner: "test-team"
1199
+ entity Span @ 1 (additive) {
1200
+ @key spanId: uuid
1201
+ spanKind: enum(INTERNAL, SERVER, CLIENT)
1202
+ name: string
1203
+ }
1204
+
1205
+ projection SpanRow @ 1
1206
+ from telemetry.Span @ 1 as s
1207
+ {
1208
+ spanId <- s.spanId
1209
+ spanKind <- s.spanKind
1210
+ name <- s.name
1211
+ }
1212
+ }
1213
+ """,
1214
+ encoding="utf-8",
1215
+ )
1216
+ (tmp_path / "bindings.mdl").write_text(
1217
+ """
1218
+ binding ch-conn {
1219
+ adapter: clickhouse
1220
+ }
1221
+
1222
+ binding span-binding {
1223
+ model: telemetry.Span @ 1
1224
+ adapter: ch-conn
1225
+ table: "spans"
1226
+ }
1227
+ """,
1228
+ encoding="utf-8",
1229
+ )
1230
+ workspace = load_workspace(tmp_path)
1231
+ artifacts = emit_rust(workspace, tmp_path / "out")
1232
+ proj = next(a for a in artifacts if a.ref == "telemetry.SpanRow@1")
1233
+ # Enum field must be String in the ClickHouse projection
1234
+ assert "pub span_kind: String," in proj.content
1235
+ # No typed enum definition for the projection
1236
+ assert "pub enum TelemetrySpanRowV1SpanKind" not in proj.content
1237
+ # Ensure typed enum is not used as field type
1238
+ assert "pub span_kind: TelemetrySpanV1SpanKind," not in proj.content
1239
+
1240
+
1241
+ def test_emit_rust_clickhouse_row_from_impl_enum_becomes_match_arm(tmp_path):
1242
+ """From impl for ClickHouse row with enum fields uses explicit match → String (issue #119)."""
1243
+ (tmp_path / "model.mdl").write_text(
1244
+ """
1245
+ domain telemetry {
1246
+ owner: "test-team"
1247
+ entity Span @ 1 (additive) {
1248
+ @key spanId: uuid
1249
+ spanKind: enum(INTERNAL, SERVER, CLIENT)
1250
+ name: string
1251
+ }
1252
+
1253
+ projection SpanRow @ 1
1254
+ from telemetry.Span @ 1 as s
1255
+ {
1256
+ spanId <- s.spanId
1257
+ spanKind <- s.spanKind
1258
+ name <- s.name
1259
+ }
1260
+ }
1261
+ """,
1262
+ encoding="utf-8",
1263
+ )
1264
+ (tmp_path / "bindings.mdl").write_text(
1265
+ """
1266
+ binding ch-conn {
1267
+ adapter: clickhouse
1268
+ }
1269
+
1270
+ binding span-binding {
1271
+ model: telemetry.Span @ 1
1272
+ adapter: ch-conn
1273
+ table: "spans"
1274
+ }
1275
+ """,
1276
+ encoding="utf-8",
1277
+ )
1278
+ workspace = load_workspace(tmp_path)
1279
+ artifacts = emit_rust(workspace, tmp_path / "out")
1280
+ proj = next(a for a in artifacts if a.ref == "telemetry.SpanRow@1")
1281
+ # Match arm generated for enum → String conversion
1282
+ assert "span_kind: match src.span_kind {" in proj.content
1283
+ assert 'TelemetrySpanV1SpanKind::INTERNAL => "INTERNAL".to_string(),' in proj.content
1284
+ # Must NOT use .into() for the enum field
1285
+ assert "span_kind: src.span_kind.into()," not in proj.content
@@ -677,15 +677,46 @@ domain nlq {
677
677
  artifacts = emit_typescript(workspace, tmp_path / "out")
678
678
  ir_art = next(a for a in artifacts if "NlqIr" in a.ref)
679
679
 
680
- # Resolved named type uses stable interface name, not bare name
681
- assert "NlqNlqTimeRangeV" in ir_art.content
682
- # An import type statement is emitted
683
- assert "import type {" in ir_art.content
684
- assert "NlqNlqTimeRange" in ir_art.content
680
+ # Resolved named type uses the exported short alias as the canonical name.
681
+ assert "timeRange: NlqTimeRange;" in ir_art.content
682
+ assert 'import type { NlqTimeRange } from "./nlq.NlqTimeRange.v0";' in ir_art.content
683
+ assert "NlqNlqTimeRangeV" not in ir_art.content
685
684
  # EMIT003 must NOT be emitted when the type is successfully resolved
686
685
  assert not any("EMIT003" in w for w in ir_art.warnings)
687
686
 
688
687
 
688
+ def test_emit_typescript_named_type_imports_after_docblock_and_uses_alias(tmp_path):
689
+ """NamedType imports use the exported short alias after the metadata docblock."""
690
+ (tmp_path / "test.mdl").write_text(
691
+ """
692
+ domain nlq {
693
+ owner: "test-team"
694
+ value NlqFilter {
695
+ field: string
696
+ value: string
697
+ }
698
+ value NlqIr {
699
+ filters: array<NlqFilter>
700
+ query: string
701
+ }
702
+ }
703
+ """,
704
+ encoding="utf-8",
705
+ )
706
+ workspace = load_workspace(tmp_path)
707
+ artifacts = emit_typescript(workspace, tmp_path / "out")
708
+ ir_art = next(a for a in artifacts if "NlqIr" in a.ref)
709
+ lines = ir_art.content.splitlines()
710
+
711
+ docblock_end = lines.index(" */")
712
+ import_index = lines.index('import type { NlqFilter } from "./nlq.NlqFilter.v0";')
713
+ interface_index = lines.index("export interface NlqNlqIrV0 {")
714
+
715
+ assert docblock_end < import_index < interface_index
716
+ assert "filters: NlqFilter[];" in ir_art.content
717
+ assert "NlqNlqFilterV" not in ir_art.content
718
+
719
+
689
720
  def test_emit_typescript_named_type_unresolvable_still_warns(tmp_path):
690
721
  """NamedType not found in workspace still emits EMIT003 (issue #118)."""
691
722
  (tmp_path / "test.mdl").write_text(
@@ -348,7 +348,7 @@ wheels = [
348
348
 
349
349
  [[package]]
350
350
  name = "modelable"
351
- version = "1.0.1"
351
+ version = "1.0.2"
352
352
  source = { editable = "." }
353
353
  dependencies = [
354
354
  { name = "click" },