tai-sql 0.8.3__tar.gz → 0.8.4__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 (230) hide show
  1. {tai_sql-0.8.3 → tai_sql-0.8.4}/PKG-INFO +1 -1
  2. {tai_sql-0.8.3 → tai_sql-0.8.4}/pyproject.toml +1 -1
  3. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/auth.py +2 -1
  4. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/chatbot.py +3 -2
  5. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/drivers/base.py +34 -0
  6. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/drivers/mysql.py +32 -0
  7. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/errors/collector.py +51 -1
  8. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/errors/render.py +5 -0
  9. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/errors/types.py +3 -0
  10. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/README.md +4 -2
  11. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/builder.py +25 -15
  12. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/generator.py +3 -2
  13. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/model.py +3 -2
  14. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/__init__.py +3 -3
  15. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/context.py +42 -8
  16. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/domain/dao.py.jinja2 +1 -1
  17. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/domain/dtos.py.jinja2 +1 -1
  18. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/hierarchy.jinja2 +38 -20
  19. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/read.jinja2 +3 -3
  20. tai_sql-0.8.4/tai_sql/generators/python_client/templates/shared/utils/hierarchy.py.jinja2 +616 -0
  21. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/rules/__init__.py +5 -4
  22. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/rules/templates/schema.md.jinja2 +6 -2
  23. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/analysis/relation_analyzer.py +65 -0
  24. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/analysis/schema_analyzer.py +122 -16
  25. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/columns.py +4 -1
  26. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/relations.py +17 -0
  27. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/model/README.md +1 -1
  28. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/model/__init__.py +2 -2
  29. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/model/column.py +3 -1
  30. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/model/entity.py +43 -13
  31. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/model/relation.py +3 -0
  32. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/static/modelo.md +31 -1
  33. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/schema.py +5 -0
  34. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/safety.py +26 -6
  35. tai_sql-0.8.3/tai_sql/generators/python_client/templates/shared/utils/hierarchy.py.jinja2 +0 -489
  36. {tai_sql-0.8.3 → tai_sql-0.8.4}/LICENSE +0 -0
  37. {tai_sql-0.8.3 → tai_sql-0.8.4}/README.md +0 -0
  38. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/__init__.py +0 -0
  39. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/__init__.py +0 -0
  40. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_backup/__init__.py +0 -0
  41. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_backup/main.py +0 -0
  42. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/__init__.py +0 -0
  43. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/config.py +0 -0
  44. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/local.py +0 -0
  45. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/main.py +0 -0
  46. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/workflow.py +0 -0
  47. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_feed/__init__.py +0 -0
  48. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_feed/main.py +0 -0
  49. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_generate/__init__.py +0 -0
  50. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_generate/funcs.py +0 -0
  51. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_generate/main.py +0 -0
  52. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_hash_migrate/__init__.py +0 -0
  53. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_hash_migrate/main.py +0 -0
  54. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_info/__init__.py +0 -0
  55. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_info/main.py +0 -0
  56. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_info/status.py +0 -0
  57. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_init/__init__.py +0 -0
  58. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_init/main.py +0 -0
  59. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_init/model.py +0 -0
  60. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_install/__init__.py +0 -0
  61. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_install/main.py +0 -0
  62. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_ping/__init__.py +0 -0
  63. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_ping/main.py +0 -0
  64. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_pull/__init__.py +0 -0
  65. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_pull/funcs.py +0 -0
  66. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_pull/main.py +0 -0
  67. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_restore/__init__.py +0 -0
  68. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_restore/main.py +0 -0
  69. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_rules/__init__.py +0 -0
  70. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_rules/main.py +0 -0
  71. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/__init__.py +0 -0
  72. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/model.py +0 -0
  73. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/new.py +0 -0
  74. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/__init__.py +0 -0
  75. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/base.py +0 -0
  76. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/default.py +0 -0
  77. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/set_default.py +0 -0
  78. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/prompts.py +0 -0
  79. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/__init__.py +0 -0
  80. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/connectivity.py +0 -0
  81. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/create.py +0 -0
  82. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/github/__init__.py +0 -0
  83. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/github/auth.py +0 -0
  84. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/github/client.py +0 -0
  85. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/github/models.py +0 -0
  86. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/errors.py +0 -0
  87. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/main.py +0 -0
  88. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/preamble.py +0 -0
  89. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/services/__init__.py +0 -0
  90. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/services/backup.py +0 -0
  91. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/connection/README.md +0 -0
  92. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/connection/__init__.py +0 -0
  93. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/connection/provider.py +0 -0
  94. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/connection/recipe.py +0 -0
  95. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/connection/session.py +0 -0
  96. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/connection/sources.py +0 -0
  97. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/crypto.py +0 -0
  98. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/drivers/__init__.py +0 -0
  99. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/drivers/bigquery.py +0 -0
  100. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/drivers/capabilities.py +0 -0
  101. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/drivers/postgresql.py +0 -0
  102. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/drivers/sqlserver.py +0 -0
  103. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/errors/README.md +0 -0
  104. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/errors/__init__.py +0 -0
  105. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/feed/README.md +0 -0
  106. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/feed/__init__.py +0 -0
  107. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/feed/collect.py +0 -0
  108. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/feed/executor.py +0 -0
  109. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/feed/feeder.py +0 -0
  110. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/feed/plan.py +0 -0
  111. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/feed/presenter.py +0 -0
  112. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/feed/records.py +0 -0
  113. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/feed/transform.py +0 -0
  114. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/__init__.py +0 -0
  115. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/base.py +0 -0
  116. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/__init__.py +0 -0
  117. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/assets/README.md +0 -0
  118. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/assets/cytoscape.min.js +0 -0
  119. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/graphviz_render.py +0 -0
  120. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/html_render.py +0 -0
  121. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/layout.py +0 -0
  122. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/templates/er.html.jinja2 +0 -0
  123. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/themes.py +0 -0
  124. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/calculated.py +0 -0
  125. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/base.py.jinja2 +0 -0
  126. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/domain/__init__.py.jinja2 +0 -0
  127. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/domain/model.py.jinja2 +0 -0
  128. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/facade.py.jinja2 +0 -0
  129. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/aggregation.jinja2 +0 -0
  130. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/dataframe.jinja2 +0 -0
  131. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/filters.jinja2 +0 -0
  132. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/triggers.jinja2 +0 -0
  133. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/upsert.jinja2 +0 -0
  134. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/vector.jinja2 +0 -0
  135. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/write.jinja2 +0 -0
  136. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/session.py.jinja2 +0 -0
  137. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/__init__.py.jinja2 +0 -0
  138. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/aggregation.py.jinja2 +0 -0
  139. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/binary.py.jinja2 +0 -0
  140. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/context.py.jinja2 +0 -0
  141. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/utils/__init__.py.jinja2 +0 -0
  142. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/utils/encoders.py.jinja2 +0 -0
  143. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/utils/funcs.py.jinja2 +0 -0
  144. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/utils/rls.py.jinja2 +0 -0
  145. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/vector.py.jinja2 +0 -0
  146. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/__init__.py +0 -0
  147. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/analyzer.py +0 -0
  148. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/renderer.py +0 -0
  149. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/scopes.py +0 -0
  150. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/steps.py +0 -0
  151. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/transformer.py +0 -0
  152. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/README.md +0 -0
  153. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/__init__.py +0 -0
  154. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/blueprint.py +0 -0
  155. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/build.py +0 -0
  156. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/defaults.py +0 -0
  157. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/naming.py +0 -0
  158. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/reader.py +0 -0
  159. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/templates/schema.py.jinja2 +0 -0
  160. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/types.py +0 -0
  161. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/writer.py +0 -0
  162. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/README.md +0 -0
  163. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/__init__.py +0 -0
  164. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/analysis/README.md +0 -0
  165. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/analysis/__init__.py +0 -0
  166. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/analysis/calculated.py +0 -0
  167. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/analysis/collector.py +0 -0
  168. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/analysis/column_analyzer.py +0 -0
  169. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/analysis/constraints.py +0 -0
  170. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/analysis/hints.py +0 -0
  171. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/README.md +0 -0
  172. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/__init__.py +0 -0
  173. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/annotations.py +0 -0
  174. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/base.py +0 -0
  175. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/calculated.py +0 -0
  176. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/constraints.py +0 -0
  177. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/datasource.py +0 -0
  178. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/descriptors.py +0 -0
  179. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/encoders.py +0 -0
  180. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/enumerations.py +0 -0
  181. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/table.py +0 -0
  182. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/trigger_api.py +0 -0
  183. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/triggers.py +0 -0
  184. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/view.py +0 -0
  185. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/mapping/README.md +0 -0
  186. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/mapping/__init__.py +0 -0
  187. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/mapping/sqlalchemy_mapper.py +0 -0
  188. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/model/constraints.py +0 -0
  189. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/model/enumeration.py +0 -0
  190. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/model/schema.py +0 -0
  191. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/model/trigger.py +0 -0
  192. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/naming.py +0 -0
  193. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/ordering.py +0 -0
  194. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/registry.py +0 -0
  195. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/types.py +0 -0
  196. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/project/README.md +0 -0
  197. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/project/__init__.py +0 -0
  198. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/project/config.py +0 -0
  199. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/project/context.py +0 -0
  200. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/project/manager.py +0 -0
  201. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/project/paths.py +0 -0
  202. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/README.md +0 -0
  203. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/__init__.py +0 -0
  204. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/catalog.py +0 -0
  205. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/installer.py +0 -0
  206. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/meta.py +0 -0
  207. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/service.py +0 -0
  208. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/staleness.py +0 -0
  209. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/static/comandos.md +0 -0
  210. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/static/errores.md +0 -0
  211. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/README.md +0 -0
  212. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/__init__.py +0 -0
  213. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/diff/__init__.py +0 -0
  214. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/diff/constraints.py +0 -0
  215. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/diff/indexes.py +0 -0
  216. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/diff/tables.py +0 -0
  217. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/diff/views.py +0 -0
  218. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/executor.py +0 -0
  219. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/ownership.py +0 -0
  220. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/plan/__init__.py +0 -0
  221. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/plan/planner.py +0 -0
  222. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/plan/statements.py +0 -0
  223. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/presenter.py +0 -0
  224. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/report.py +0 -0
  225. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/state/__init__.py +0 -0
  226. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/state/local.py +0 -0
  227. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/state/model.py +0 -0
  228. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/state/normalize.py +0 -0
  229. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/state/remote.py +0 -0
  230. {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/synchronizer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tai-sql
3
- Version: 0.8.3
3
+ Version: 0.8.4
4
4
  Summary: SQL database management and code generation tool
5
5
  License-Expression: LicenseRef-Proprietary
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tai-sql"
3
- version = "0.8.3"
3
+ version = "0.8.4"
4
4
  description = "SQL database management and code generation tool"
5
5
  license = "LicenseRef-Proprietary"
6
6
  license-files = ["LICENSE"]
@@ -154,7 +154,8 @@ class AuthSchema(SchemaTemplate):
154
154
  # se resolvía solo por nombre, así que un padre podía no existir o ser otro rol
155
155
  # homónimo de otro realm. `restrict`: borrar un rol con subroles es un error,
156
156
  # no una cascada que se los lleve (y `set null` vaciaría también `realm_name`).
157
- parent: manytoone[Role] = relation(fields=["parent_role_name", "realm_name"], references=["name", "realm_name"], backref="subroles", onDelete="restrict")
157
+ # `tree=True`: la jerarquía de roles, con consultas en árbol (`find_many(tree_mode=...)`).
158
+ parent: manytoone[Role] = relation(fields=["parent_role_name", "realm_name"], references=["name", "realm_name"], backref="subroles", onDelete="restrict", tree=True)
158
159
 
159
160
  subroles: onetomany[Role]
160
161
 
@@ -172,8 +172,9 @@ class ChatbotSchema(SchemaTemplate):
172
172
  # La llamada que creó el sub-agente que hizo esta: del mismo mensaje, por la clave compuesta
173
173
  # (message_id, padre) → (message_id, call_id). Por el id del modelo y no por el de la fila: las
174
174
  # filas se crean anidadas en el mensaje, y el id de la del padre todavía no existe. Con padre
175
- # vacío (las del agente principal) la clave no se comprueba. Borrar una se lleva sus hijas
176
- llamada_padre: manytoone[ToolCall] = relation(fields=['message_id', 'padre'], references=['message_id', 'call_id'], backref='hijas', onDelete='cascade')
175
+ # vacío (las del agente principal) la clave no se comprueba. Borrar una se lleva sus hijas.
176
+ # `tree=True`: el árbol de llamadas de una respuesta, con consultas en árbol
177
+ llamada_padre: manytoone[ToolCall] = relation(fields=['message_id', 'padre'], references=['message_id', 'call_id'], backref='hijas', onDelete='cascade', tree=True)
177
178
  hijas: onetomany[ToolCall]
178
179
 
179
180
 
@@ -1107,6 +1107,40 @@ class DatabaseDriver(ABC):
1107
1107
  f"WHERE {no_nulos} AND r.{self.quote_identifier(target_columns[0])} IS NULL"
1108
1108
  )
1109
1109
 
1110
+ def orphans_fix_statements(
1111
+ self, table_name: str, columns: Sequence[str],
1112
+ target_table: str, target_columns: Sequence[str],
1113
+ detach_columns: Sequence[str] = (), schema_name: str = None,
1114
+ ) -> Dict[str, Optional[str]]:
1115
+ """
1116
+ Las sentencias que propone `push` cuando una FK nueva tiene filas huérfanas.
1117
+
1118
+ - `delete`: elimina las filas cuyas columnas de FK no encuentran pareja.
1119
+ - `detach`: pone a NULL `detach_columns` en esas mismas filas, que es lo natural
1120
+ en una FK de la tabla consigo misma —la fila pasa a ser una raíz—. `None` si no
1121
+ se pide.
1122
+
1123
+ La pareja se busca por **todas** las columnas de la FK: con una compuesta, mirar
1124
+ solo la primera borraba filas que no son huérfanas. Es SQL estándar (`NOT
1125
+ EXISTS`); el motor que no acepte una subconsulta sobre la misma tabla que modifica
1126
+ —MySQL— lo sobrescribe.
1127
+ """
1128
+ local = self.qualified_table(table_name, schema_name)
1129
+ destino = self.qualified_table(target_table, schema_name)
1130
+ union = ' AND '.join(
1131
+ f"r.{self.quote_identifier(t)} = {local}.{self.quote_identifier(c)}"
1132
+ for c, t in zip(columns, target_columns)
1133
+ )
1134
+ no_nulos = ' AND '.join(
1135
+ f"{self.quote_identifier(c)} IS NOT NULL" for c in columns
1136
+ )
1137
+ condicion = f"{no_nulos} AND NOT EXISTS (SELECT 1 FROM {destino} r WHERE {union})"
1138
+ detach = None
1139
+ if detach_columns:
1140
+ nulos = ', '.join(f"{self.quote_identifier(c)} = NULL" for c in detach_columns)
1141
+ detach = f"UPDATE {local} SET {nulos} WHERE {condicion}"
1142
+ return {'delete': f"DELETE FROM {local} WHERE {condicion}", 'detach': detach}
1143
+
1110
1144
  # ── Tipos ────────────────────────────────────────────────────────────
1111
1145
 
1112
1146
  def is_sequence_default(self, value: str) -> bool:
@@ -508,6 +508,38 @@ class MySQLDriver(DatabaseDriver):
508
508
 
509
509
  # ── Escritura de datos ───────────────────────────────────────────────
510
510
 
511
+ def orphans_fix_statements(
512
+ self, table_name: str, columns: Sequence[str],
513
+ target_table: str, target_columns: Sequence[str],
514
+ detach_columns: Sequence[str] = (), schema_name: str = None,
515
+ ) -> Dict[str, Optional[str]]:
516
+ """
517
+ Con `LEFT JOIN` y no con `NOT EXISTS`.
518
+
519
+ MySQL no deja modificar una tabla con una subconsulta sobre ella misma (error
520
+ 1093), que es justo el caso de una FK de la tabla consigo misma. El `UPDATE` y el
521
+ `DELETE` de varias tablas sí admiten unir la tabla consigo misma.
522
+ """
523
+ local = self.qualified_table(table_name, schema_name)
524
+ destino = self.qualified_table(target_table, schema_name)
525
+ union = ' AND '.join(
526
+ f"r.{self.quote_identifier(t)} = l.{self.quote_identifier(c)}"
527
+ for c, t in zip(columns, target_columns)
528
+ )
529
+ no_nulos = ' AND '.join(
530
+ f"l.{self.quote_identifier(c)} IS NOT NULL" for c in columns
531
+ )
532
+ huerfana = f"r.{self.quote_identifier(target_columns[0])} IS NULL"
533
+ joined = f"{local} l LEFT JOIN {destino} r ON {union}"
534
+ detach = None
535
+ if detach_columns:
536
+ nulos = ', '.join(f"l.{self.quote_identifier(c)} = NULL" for c in detach_columns)
537
+ detach = f"UPDATE {joined} SET {nulos} WHERE {no_nulos} AND {huerfana}"
538
+ return {
539
+ 'delete': f"DELETE l FROM {joined} WHERE {no_nulos} AND {huerfana}",
540
+ 'detach': detach,
541
+ }
542
+
511
543
  def upsert_statement(self, table, values, conflict_columns, update_columns,
512
544
  returning=()):
513
545
  """
@@ -19,6 +19,11 @@ decir las cinco, y no obligar a arreglarlas de una en una, recompilando entre me
19
19
 
20
20
  collector.abort_if_errors() # los presenta todos y sale
21
21
 
22
+ Recoge también **avisos**: lo que conviene cambiar sin que impida seguir, como una forma
23
+ del DSL que se va a retirar. Tienen la misma forma que un error —mensaje, solución,
24
+ ubicación, código— y van aparte porque no cuentan para `has_errors()`: un schema con
25
+ avisos se carga, se genera y se sincroniza igual.
26
+
22
27
  Vive aquí y no en `orm/analysis/`, que es donde nació, porque acumular no es un concepto
23
28
  del análisis: es la forma general de reportar varios problemas. La prueba es que
24
29
  `SafetyChecker` había reimplementado exactamente esto por su cuenta —una lista de
@@ -32,7 +37,7 @@ from __future__ import annotations
32
37
 
33
38
  from typing import Iterable, List, Optional
34
39
 
35
- from .render import render_diagnostics
40
+ from .render import render_diagnostics, render_warning
36
41
  from .types import Diagnostic, ErrorCode
37
42
 
38
43
 
@@ -50,6 +55,7 @@ class ErrorCollector:
50
55
  self.context = context
51
56
  self.enable_colors = enable_colors
52
57
  self.errors: List[Diagnostic] = []
58
+ self.warnings: List[Diagnostic] = []
53
59
 
54
60
  # ── Registro ─────────────────────────────────────────────────────────
55
61
 
@@ -81,6 +87,26 @@ class ErrorCollector:
81
87
  )
82
88
  )
83
89
 
90
+ def add_warning(
91
+ self,
92
+ message: str,
93
+ solution: str,
94
+ location: Optional[str] = None,
95
+ code: Optional[ErrorCode] = None,
96
+ ) -> None:
97
+ """
98
+ Anota un aviso: algo que conviene cambiar, pero que no impide seguir.
99
+
100
+ Args:
101
+ message: Qué conviene saber.
102
+ solution: Qué cambiar. Obligatorio, igual que en un error.
103
+ location: Fichero, clase o campo donde ocurre.
104
+ code: Código para buscarlo en la documentación.
105
+ """
106
+ self.warnings.append(
107
+ Diagnostic(message=message, solution=solution, location=location, code=code)
108
+ )
109
+
84
110
  def extend(self, diagnostics: Iterable[Diagnostic]) -> None:
85
111
  """Anota varios problemas ya construidos."""
86
112
  self.errors.extend(diagnostics)
@@ -88,6 +114,7 @@ class ErrorCollector:
88
114
  def clear(self) -> None:
89
115
  """Olvida lo acumulado."""
90
116
  self.errors.clear()
117
+ self.warnings.clear()
91
118
 
92
119
  # ── Consulta ─────────────────────────────────────────────────────────
93
120
 
@@ -99,6 +126,14 @@ class ErrorCollector:
99
126
  """Copia de lo acumulado."""
100
127
  return self.errors.copy()
101
128
 
129
+ def has_warnings(self) -> bool:
130
+ """Si hay algún aviso acumulado."""
131
+ return bool(self.warnings)
132
+
133
+ def get_warnings(self) -> List[Diagnostic]:
134
+ """Copia de los avisos acumulados."""
135
+ return self.warnings.copy()
136
+
102
137
  # Nota: deliberadamente SIN __len__. Un colector se usa como objeto, no como
103
138
  # contenedor, y darle longitud lo vuelve falsy cuando está vacío — que es el estado
104
139
  # normal al empezar. El idioma `collector or ErrorCollector()` pasaba entonces a
@@ -112,6 +147,21 @@ class ErrorCollector:
112
147
  self.errors, context=self.context, colors=self.enable_colors
113
148
  )
114
149
 
150
+ def show_warnings(self) -> None:
151
+ """Presenta los avisos acumulados, uno detrás de otro."""
152
+ for warning in self.warnings:
153
+ where = ' · '.join(
154
+ p for p in (
155
+ self.context, warning.location, warning.code and warning.code.value
156
+ ) if p
157
+ )
158
+ render_warning(
159
+ warning.message,
160
+ hint=warning.solution,
161
+ location=where or None,
162
+ colors=self.enable_colors,
163
+ )
164
+
115
165
  def abort_if_errors(self) -> None:
116
166
  """
117
167
  Si hay problemas, los presenta todos y termina con código 1.
@@ -237,6 +237,11 @@ def render_warning(
237
237
  stream: Dónde escribir. Por defecto stderr.
238
238
  colors: `False` fuerza texto pelado.
239
239
  """
240
+ # Una solución de varias líneas —un trozo de código que pegar— sigue alineada bajo
241
+ # el 💡 en vez de volver al margen
242
+ if hint:
243
+ hint = hint.replace('\n', '\n ')
244
+
240
245
  console = _console(stream) if colors else None
241
246
 
242
247
  if console is None:
@@ -62,6 +62,9 @@ class ErrorCode(str, Enum):
62
62
  # Recursos generados
63
63
  GENERATION_FAILED = "E030"
64
64
 
65
+ # Avisos: lo que conviene cambiar, sin impedir seguir
66
+ DEPRECATED = "W001"
67
+
65
68
 
66
69
  @dataclass
67
70
  class Diagnostic:
@@ -37,8 +37,10 @@ lado aunque el schema las declarase:
37
37
  dibujaba como `STR`, porque el tipo canónico de la columna es el de sus valores.
38
38
  - Las **UNIQUE e índices de varias columnas**, que no caben en la fila de una columna. La
39
39
  UNIQUE compuesta es además la clave natural del feed.
40
- - Las **jerarquías** de `column(self_reference=…)`, que no producen `RelationModel`
41
- —no hay dos entidades que relacionar— y por eso el árbol no se veía.
40
+ - Las **jerarquías**. La de `relation(..., tree=True)` es una relación más, y su arista se
41
+ marca como jerarquía. La de `column(self_reference=…)` —deprecada— no produce
42
+ `RelationModel` —no hay dos entidades que relacionar— y por eso el árbol no se veía:
43
+ se añade como arista propia.
42
44
  - Las **dimensiones** de una columna vectorial, que es lo único que hay que saber de ella.
43
45
 
44
46
  Cada bloque se apaga con su `include_*`: `views`, `columns`, `relationships`, `triggers`,
@@ -12,8 +12,9 @@ un render.
12
12
  Tres cosas que el diagrama enseña y que no salen de recorrer las columnas:
13
13
 
14
14
  - los **enumerados**, que no son entidades del schema pero tipan columnas;
15
- - las **jerarquías** de `column(self_reference=…)`, que no producen `RelationModel`
16
- porque no hay dos entidades que relacionar;
15
+ - las **jerarquías**: la relación con `tree=True` se dibuja como cualquier otra y se marca
16
+ como jerarquía, y la de `column(self_reference=…)` —deprecada—, que no produce
17
+ `RelationModel`, se añade como arista propia;
17
18
  - las **UNIQUE e índices de varias columnas**, que no caben en la fila de una columna.
18
19
  """
19
20
  from __future__ import annotations
@@ -95,14 +96,14 @@ class DiagramBuilder:
95
96
 
96
97
  def entity(self, entity) -> EREntity:
97
98
  """Construye una entidad de dibujo a partir de una del modelo lógico."""
98
- hierarchy_column = (
99
- entity.self_reference.column_name
100
- if not entity.is_view and entity.has_self_reference
101
- else None
99
+ hierarchy_columns = (
100
+ set(entity.hierarchy.pointer_columns)
101
+ if not entity.is_view and entity.has_hierarchy
102
+ else set()
102
103
  )
103
104
 
104
105
  columns = [
105
- self.column(column, hierarchy_column)
106
+ self.column(column, hierarchy_columns)
106
107
  for column in entity.columns.values()
107
108
  if self.include_operative_fields or not column.is_operative
108
109
  ]
@@ -140,7 +141,7 @@ class DiagramBuilder:
140
141
  query=getattr(entity, 'query', None),
141
142
  )
142
143
 
143
- def column(self, column, hierarchy_column: Optional[str]) -> ERColumn:
144
+ def column(self, column, hierarchy_columns=frozenset()) -> ERColumn:
144
145
  """Traduce una columna, resolviendo ya la etiqueta de su tipo."""
145
146
  return ERColumn(
146
147
  name=column.name,
@@ -154,7 +155,7 @@ class DiagramBuilder:
154
155
  calculated=column.is_calculated,
155
156
  description=column.description,
156
157
  is_vector=column.is_vector,
157
- hierarchy=column.name == hierarchy_column,
158
+ hierarchy=column.name in hierarchy_columns,
158
159
  operative=column.is_operative,
159
160
  )
160
161
 
@@ -228,14 +229,17 @@ class DiagramBuilder:
228
229
  declarada y su inversa implícita), y quedarse con la implícita hace que la
229
230
  arista salga siempre del lado "uno".
230
231
 
231
- Las jerarquías son aparte: `column(self_reference=…)` no produce `RelationModel`
232
- —no hay dos entidades que relacionar—, así que sin esto el árbol no se veía en
233
- el diagrama por ningún lado.
232
+ La jerarquía de `relation(..., tree=True)` es una relación más, marcada como
233
+ jerarquía. La de `column(self_reference=…)` es aparte: no produce `RelationModel`
234
+ —no hay dos entidades que relacionar—, así que sin esto el árbol no se veía en el
235
+ diagrama por ningún lado.
234
236
  """
235
237
  relationships: List[ERRelationship] = []
236
238
 
237
239
  for table in self.schema.tables.values():
238
240
 
241
+ tree = table.hierarchy.relation if table.has_hierarchy else None
242
+
239
243
  for relation in table.relations.values():
240
244
  if not relation.implicit:
241
245
  continue
@@ -252,10 +256,16 @@ class DiagramBuilder:
252
256
  relationship_type=relation.direction,
253
257
  local_name=relation.name,
254
258
  target_name=relation.backref,
259
+ # La inversa de la relación con `tree=True`, que es la que se dibuja
260
+ hierarchy=(
261
+ tree is not None
262
+ and relation.backref == tree
263
+ and relation.target == relation.local
264
+ ),
255
265
  )
256
266
  )
257
267
 
258
- if table.has_self_reference:
268
+ if table.has_hierarchy and table.hierarchy.relation is None:
259
269
  if drawn is not None and table.tablename not in drawn:
260
270
  continue
261
271
  relationships.append(
@@ -263,8 +273,8 @@ class DiagramBuilder:
263
273
  from_table=table.tablename,
264
274
  to_table=table.tablename,
265
275
  relationship_type=RelationshipType.ONE_TO_MANY.value,
266
- local_name=table.self_reference.column_name,
267
- target_name=table.self_reference.references,
276
+ local_name=table.hierarchy.parent_columns[0],
277
+ target_name=table.hierarchy.key_columns[0],
268
278
  hierarchy=True,
269
279
  )
270
280
  )
@@ -54,8 +54,9 @@ class ERDiagramGenerator(BaseGenerator):
54
54
  Graphviz ni conexión.
55
55
 
56
56
  Lo que dibuja, además de tablas, vistas y columnas: los enumerados con sus valores y
57
- qué columna los usa, las UNIQUE e índices de varias columnas, las jerarquías de
58
- `column(self_reference=…)` y las dimensiones de una columna vectorial.
57
+ qué columna los usa, las UNIQUE e índices de varias columnas, las jerarquías
58
+ (`relation(..., tree=True)` y la deprecada `column(self_reference=…)`) y las
59
+ dimensiones de una columna vectorial.
59
60
  """
60
61
 
61
62
  def __init__(
@@ -165,8 +165,9 @@ class ERRelationship:
165
165
  relationship_type: str
166
166
  local_name: str
167
167
  target_name: Optional[str]
168
- #: `True` cuando la arista es una jerarquía declarada con `column(self_reference=…)`,
169
- #: que no produce `RelationModel` y por tanto no llegaba al diagrama.
168
+ #: `True` cuando la arista es la jerarquía de la tabla: la relación con `tree=True`, o
169
+ #: la de `column(self_reference=…)`, que no produce `RelationModel` y sin esto no
170
+ #: llegaba al diagrama.
170
171
  hierarchy: bool = False
171
172
 
172
173
  @property
@@ -422,9 +422,9 @@ class PythonClientGenerator(BaseGenerator):
422
422
  for col in m.get('columns', [])
423
423
  )
424
424
 
425
- # Pre-compute whether any model has self_reference (hierarchy)
425
+ # Pre-compute whether any model declares a hierarchy
426
426
  has_hierarchy = any(
427
- m.get('has_self_reference', False)
427
+ m.get('has_hierarchy', False)
428
428
  for m in all_models_data
429
429
  )
430
430
 
@@ -494,7 +494,7 @@ class PythonClientGenerator(BaseGenerator):
494
494
  elif encoders_path.exists():
495
495
  encoders_path.unlink()
496
496
 
497
- # _shared/utils/hierarchy.py — only when self_reference columns exist
497
+ # _shared/utils/hierarchy.py — only when some table declares a hierarchy
498
498
  hierarchy_path = utils_dir / 'hierarchy.py'
499
499
  if has_hierarchy:
500
500
  hierarchy_template = self.jinja_env.get_template('shared/utils/hierarchy.py.jinja2')
@@ -155,8 +155,15 @@ class TemplateContextBuilder:
155
155
  for cc in table.calculated_columns.values()
156
156
  ],
157
157
  'has_calculated_columns': table.has_calculated_columns,
158
- 'has_self_reference': table.has_self_reference,
159
- 'self_reference': self._self_reference(table),
158
+ 'has_hierarchy': table.has_hierarchy,
159
+ 'hierarchy': self._hierarchy(table),
160
+ # Las dos de antes, con su forma: el contexto es contrato con tai-api, que lee
161
+ # `has_self_reference` para tipar `tree_level`, `ancestors` y `descendants`.
162
+ # Valen para cualquier jerarquía, también la de `tree=True`; `self_reference`
163
+ # solo tiene forma para una clave de una columna. Se retiran con
164
+ # `column(self_reference=...)`.
165
+ 'has_self_reference': table.has_hierarchy,
166
+ 'self_reference': self._legacy_self_reference(table),
160
167
  }
161
168
 
162
169
  def view(self, view: ViewModel) -> Dict[str, Any]:
@@ -493,14 +500,41 @@ class TemplateContextBuilder:
493
500
  for event in TRIGGER_EVENTS
494
501
  }
495
502
 
496
- def _self_reference(self, table: TableModel) -> Optional[Dict[str, Any]]:
497
- """Metadatos de la jerarquía declarada, o None si la tabla no declara ninguna."""
498
- if table.self_reference is None:
503
+ def _hierarchy(self, table: TableModel) -> Optional[Dict[str, Any]]:
504
+ """
505
+ La jerarquía de la tabla, o None si no declara ninguna.
506
+
507
+ `root_type` es el tipo de `tree_root` en el `find_many` generado: el valor de la
508
+ clave si es de una columna, y una tupla en el orden de `key_columns` o un dict por
509
+ nombre si es compuesta.
510
+ """
511
+ hierarchy = table.hierarchy
512
+ if hierarchy is None:
513
+ return None
514
+ if hierarchy.is_composite:
515
+ root_type = (
516
+ f"Union[tuple[{', '.join(hierarchy.key_python_types)}], Dict[str, Any]]"
517
+ )
518
+ else:
519
+ root_type = hierarchy.key_python_types[0]
520
+ return {
521
+ 'key_columns': list(hierarchy.key_columns),
522
+ 'parent_columns': list(hierarchy.parent_columns),
523
+ 'is_composite': hierarchy.is_composite,
524
+ 'root_type': root_type,
525
+ 'relation': hierarchy.relation,
526
+ }
527
+
528
+ @staticmethod
529
+ def _legacy_self_reference(table: TableModel) -> Optional[Dict[str, Any]]:
530
+ """La clave `self_reference` de antes, para una jerarquía de una columna."""
531
+ hierarchy = table.hierarchy
532
+ if hierarchy is None or hierarchy.is_composite:
499
533
  return None
500
534
  return {
501
- 'column_name': table.self_reference.column_name,
502
- 'references': table.self_reference.references,
503
- 'pk_python_type': table.self_reference.pk_python_type,
535
+ 'column_name': hierarchy.parent_columns[0],
536
+ 'references': hierarchy.key_columns[0],
537
+ 'pk_python_type': hierarchy.key_python_types[0],
504
538
  }
505
539
 
506
540
  def _constraints(self, column: ColumnModel) -> Dict[str, Any]:
@@ -72,7 +72,7 @@ from {{ import_prefix }}_shared import (
72
72
  AggField,
73
73
  GroupByField
74
74
  )
75
- {% if model.has_self_reference %}
75
+ {% if model.has_hierarchy %}
76
76
  from {{ import_prefix }}_shared import (
77
77
  HierarchyResolver,
78
78
  HierarchyConfig,
@@ -138,7 +138,7 @@ class {{ model.name }}Read(PrettyModel):
138
138
  {% endif %}
139
139
  {% endfor %}
140
140
 
141
- {% if model.has_self_reference %}
141
+ {% if model.has_hierarchy %}
142
142
  tree_level: Optional[int] = Field(default=None, description="Nivel en el árbol jerárquico (0 = nodo raíz de la consulta)")
143
143
  descendants: Optional[List[{{ model.name }}Read]] = Field(default=None, description="Subárbol de descendientes (poblado en tree_mode='descendants' o 'full_tree')")
144
144
  ancestors: Optional[List[{{ model.name }}Read]] = Field(default=None, description="Cadena de ancestros (poblado en tree_mode='ancestors' o 'full_tree')")
@@ -1,8 +1,10 @@
1
1
  {# ─────────────────────────────────────────────────────────────────────────────
2
2
  Hierarchy macros — generates CTE-based recursive queries for self-referencing tables.
3
-
4
- Activated when a model has `has_self_reference == True` (i.e., a column with
5
- self_reference='pk_column' is defined).
3
+
4
+ Activated when a model has `has_hierarchy == True`: a self relation declared with
5
+ `relation(..., tree=True)`, or the deprecated `column(self_reference='pk_column')`.
6
+ Both arrive with the same shape: `key_columns` (the node key) and `parent_columns`
7
+ (what points to the parent), one or several of each.
6
8
 
7
9
  Delegates to HierarchyResolver (in _shared/utils/hierarchy.py) for all CTE
8
10
  construction and tree assembly logic.
@@ -11,9 +13,9 @@
11
13
 
12
14
  {# ── Parameters for find_many signature ─────────────────────────────────── #}
13
15
  {% macro generate_hierarchy_parameters(model) %}
14
- {% if model.has_self_reference %}
16
+ {% if model.has_hierarchy %}
15
17
  tree_mode: Literal["flat", "descendants", "ancestors", "full_tree"] = "flat",
16
- tree_root: Optional[{{ model.self_reference.pk_python_type }}] = None,
18
+ tree_root: Optional[{{ model.hierarchy.root_type }}] = None,
17
19
  tree_depth: Optional[int] = None,
18
20
  {% endif %}
19
21
  {% endmacro %}
@@ -21,34 +23,50 @@ tree_depth: Optional[int] = None,
21
23
 
22
24
  {# ── Docstring args for hierarchy parameters ────────────────────────────── #}
23
25
  {% macro generate_hierarchy_args(model) %}
24
- {% if model.has_self_reference %}
26
+ {% if model.has_hierarchy %}
25
27
  - tree_mode: Modo de consulta jerárquica. 'flat' (por defecto) desactiva la recursión.
26
- 'descendants' obtiene todos los subequipos del nodo raíz.
27
- 'ancestors' obtiene todos los equipos superiores hasta la raíz.
28
+ 'descendants' obtiene todos los descendientes del nodo raíz.
29
+ 'ancestors' obtiene todos sus ancestros, hasta la raíz del árbol.
28
30
  'full_tree' obtiene tanto descendientes como ancestros.
29
- - tree_root: PK del nodo raíz para la consulta jerárquica. Si es None y tree_mode != 'flat',
30
- se usan los registros encontrados por los filtros como nodos raíz.
31
- - tree_depth: Profundidad máxima de recursión. None = sin límite.
31
+ {% if model.hierarchy.is_composite %}
32
+ - tree_root: Clave del nodo raíz para la consulta jerárquica: una tupla
33
+ ({{ model.hierarchy.key_columns | join(', ') }}) o un dict con esas claves. Si es None y
34
+ tree_mode != 'flat', se usan los registros encontrados por los filtros como nodos raíz.
35
+ {% else %}
36
+ - tree_root: Clave ({{ model.hierarchy.key_columns[0] }}) del nodo raíz para la consulta
37
+ jerárquica. Si es None y tree_mode != 'flat', se usan los registros encontrados por
38
+ los filtros como nodos raíz.
39
+ {% endif %}
40
+ - tree_depth: Profundidad máxima de recursión. None = hasta 100 niveles:
41
+ al alcanzarlo se corta y queda un aviso en el log.
32
42
  {% endif %}
33
43
  {% endmacro %}
34
44
 
35
45
 
36
- {# ── Root ID resolution block (before filters are logged) ──────────────── #}
46
+ {# ── Root ID resolution block: after the filters AND the rls, so a root is a row the user sees ── #}
37
47
  {% macro generate_hierarchy_query(model) %}
38
- {% if model.has_self_reference %}
39
- {% set sr = model.self_reference %}
48
+ {% if model.has_hierarchy %}
49
+ {% set hr = model.hierarchy %}
40
50
  # ── Hierarchy: resolver nodos raíz ────────────────────────────────────
41
51
  _hierarchy_mode = TreeMode(tree_mode)
52
+ _hierarchy_config = HierarchyConfig(
53
+ {{ model.name }},
54
+ key_columns=({% for c in hr.key_columns %}'{{ c }}', {% endfor %}),
55
+ parent_columns=({% for c in hr.parent_columns %}'{{ c }}', {% endfor %}),
56
+ )
42
57
  _hierarchy_root_ids = None
43
58
 
44
59
  if _hierarchy_mode != TreeMode.FLAT:
45
60
  if tree_root is not None:
46
- _hierarchy_root_ids = [tree_root]
61
+ _hierarchy_root_ids = [_hierarchy_config.root_key(tree_root)]
47
62
  else:
48
- # Ejecutar query filtrada para obtener PKs raíz
63
+ # Las raíces son las filas de la consulta ya filtrada y acotada por rls
49
64
  {{ def_keyword }} _get_root_ids(_sess):
50
65
  _root_result = {{ await_keyword -}} _sess.execute(query)
51
- return [row.{{ sr.references }} for row in _root_result.scalars().all()]
66
+ return [
67
+ _hierarchy_config.key(instance)
68
+ for instance in _root_result.scalars().all()
69
+ ]
52
70
 
53
71
  if session is not None:
54
72
  _hierarchy_root_ids = {{ await_keyword -}} _get_root_ids(session)
@@ -64,8 +82,7 @@ tree_depth: Optional[int] = None,
64
82
 
65
83
  {# ── Execute block using HierarchyResolver ──────────────────────────────── #}
66
84
  {% macro generate_hierarchy_execute(model, max_depth) %}
67
- {% if model.has_self_reference %}
68
- {% set sr = model.self_reference %}
85
+ {% if model.has_hierarchy %}
69
86
  {% set vector_cols_with_encoder = [] -%}
70
87
  {% for vcol in model.columns | selectattr('type', 'equalto', 'vector') | list -%}
71
88
  {% if vcol.get('has_encoder') -%}
@@ -74,7 +91,7 @@ tree_depth: Optional[int] = None,
74
91
  {% endfor -%}
75
92
  # ── Hierarchy resolver instance ───────────────────────────────────────
76
93
  _hierarchy_resolver = HierarchyResolver(
77
- config=HierarchyConfig({{ model.name }}, '{{ sr.references }}', '{{ sr.column_name }}'),
94
+ config=_hierarchy_config,
78
95
  dto_factory=lambda instance, includes, max_depth: {{ model.name }}Read.from_instance(
79
96
  instance,
80
97
  {% if not model.is_view %}
@@ -82,6 +99,7 @@ tree_depth: Optional[int] = None,
82
99
  max_depth=max_depth
83
100
  {% endif %}
84
101
  ),
102
+ rls=rls,
85
103
  )
86
104
 
87
105
  {{ def_keyword }} execute_query(session: {{ session_type }}) -> List[{{ model.name }}Read]:
@@ -207,8 +207,6 @@
207
207
 
208
208
  {{ vector_macros.generate_vector_query(model) }}
209
209
 
210
- {{ hierarchy_macros.generate_hierarchy_query(model) }}
211
-
212
210
  # Log de parámetros aplicados
213
211
  if filters:
214
212
  _log_tx(session, f"[{{ schema_name }}] filters={filters}")
@@ -216,6 +214,8 @@
216
214
  # Aplicar regla RLS (si existe)
217
215
  if rls is not None:
218
216
  query = RLSQueryApplicator.apply_rls(query, {{ model.name }}, rls)
217
+
218
+ {{ hierarchy_macros.generate_hierarchy_query(model) }}
219
219
 
220
220
  # Aplicar opciones de carga optimizada
221
221
  if includes:
@@ -269,7 +269,7 @@
269
269
  {% endif %}
270
270
 
271
271
  # Ejecutar query
272
- {% if model.has_self_reference %}
272
+ {% if model.has_hierarchy %}
273
273
  {% if vector_cols_with_encoder %}
274
274
  {# El modo `flat` de la jerarquía es la consulta vectorial de siempre: se emite con otro
275
275
  nombre y el bloque jerárquico la llama, en vez de repetirla a medias. #}