tai-sql 0.8.2__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.2 → tai_sql-0.8.4}/PKG-INFO +1 -1
  2. {tai_sql-0.8.2 → tai_sql-0.8.4}/pyproject.toml +1 -1
  3. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/auth.py +2 -1
  4. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/chatbot.py +70 -14
  5. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/drivers/base.py +34 -0
  6. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/drivers/mysql.py +32 -0
  7. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/errors/collector.py +51 -1
  8. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/errors/render.py +5 -0
  9. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/errors/types.py +3 -0
  10. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/README.md +4 -2
  11. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/builder.py +25 -15
  12. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/generator.py +3 -2
  13. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/model.py +3 -2
  14. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/__init__.py +3 -3
  15. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/context.py +42 -8
  16. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/domain/dao.py.jinja2 +1 -1
  17. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/domain/dtos.py.jinja2 +1 -1
  18. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/hierarchy.jinja2 +38 -20
  19. {tai_sql-0.8.2 → 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.2 → tai_sql-0.8.4}/tai_sql/generators/rules/__init__.py +5 -4
  22. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/rules/templates/schema.md.jinja2 +6 -2
  23. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/analysis/relation_analyzer.py +65 -0
  24. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/analysis/schema_analyzer.py +122 -16
  25. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/columns.py +4 -1
  26. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/relations.py +17 -0
  27. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/model/README.md +1 -1
  28. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/model/__init__.py +2 -2
  29. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/model/column.py +3 -1
  30. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/model/entity.py +43 -13
  31. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/model/relation.py +3 -0
  32. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/static/modelo.md +31 -1
  33. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/schema.py +5 -0
  34. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/README.md +11 -6
  35. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/plan/planner.py +19 -11
  36. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/safety.py +26 -6
  37. tai_sql-0.8.2/tai_sql/generators/python_client/templates/shared/utils/hierarchy.py.jinja2 +0 -489
  38. {tai_sql-0.8.2 → tai_sql-0.8.4}/LICENSE +0 -0
  39. {tai_sql-0.8.2 → tai_sql-0.8.4}/README.md +0 -0
  40. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/__init__.py +0 -0
  41. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/__init__.py +0 -0
  42. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_backup/__init__.py +0 -0
  43. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_backup/main.py +0 -0
  44. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/__init__.py +0 -0
  45. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/config.py +0 -0
  46. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/local.py +0 -0
  47. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/main.py +0 -0
  48. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/workflow.py +0 -0
  49. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_feed/__init__.py +0 -0
  50. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_feed/main.py +0 -0
  51. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_generate/__init__.py +0 -0
  52. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_generate/funcs.py +0 -0
  53. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_generate/main.py +0 -0
  54. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_hash_migrate/__init__.py +0 -0
  55. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_hash_migrate/main.py +0 -0
  56. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_info/__init__.py +0 -0
  57. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_info/main.py +0 -0
  58. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_info/status.py +0 -0
  59. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_init/__init__.py +0 -0
  60. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_init/main.py +0 -0
  61. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_init/model.py +0 -0
  62. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_install/__init__.py +0 -0
  63. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_install/main.py +0 -0
  64. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_ping/__init__.py +0 -0
  65. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_ping/main.py +0 -0
  66. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_pull/__init__.py +0 -0
  67. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_pull/funcs.py +0 -0
  68. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_pull/main.py +0 -0
  69. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_restore/__init__.py +0 -0
  70. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_restore/main.py +0 -0
  71. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_rules/__init__.py +0 -0
  72. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_rules/main.py +0 -0
  73. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/__init__.py +0 -0
  74. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/model.py +0 -0
  75. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/new.py +0 -0
  76. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/__init__.py +0 -0
  77. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/base.py +0 -0
  78. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/default.py +0 -0
  79. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/set_default.py +0 -0
  80. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/prompts.py +0 -0
  81. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/__init__.py +0 -0
  82. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/connectivity.py +0 -0
  83. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/create.py +0 -0
  84. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/github/__init__.py +0 -0
  85. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/github/auth.py +0 -0
  86. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/github/client.py +0 -0
  87. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/github/models.py +0 -0
  88. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/errors.py +0 -0
  89. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/main.py +0 -0
  90. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/preamble.py +0 -0
  91. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/services/__init__.py +0 -0
  92. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/services/backup.py +0 -0
  93. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/connection/README.md +0 -0
  94. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/connection/__init__.py +0 -0
  95. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/connection/provider.py +0 -0
  96. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/connection/recipe.py +0 -0
  97. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/connection/session.py +0 -0
  98. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/connection/sources.py +0 -0
  99. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/crypto.py +0 -0
  100. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/drivers/__init__.py +0 -0
  101. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/drivers/bigquery.py +0 -0
  102. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/drivers/capabilities.py +0 -0
  103. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/drivers/postgresql.py +0 -0
  104. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/drivers/sqlserver.py +0 -0
  105. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/errors/README.md +0 -0
  106. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/errors/__init__.py +0 -0
  107. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/feed/README.md +0 -0
  108. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/feed/__init__.py +0 -0
  109. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/feed/collect.py +0 -0
  110. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/feed/executor.py +0 -0
  111. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/feed/feeder.py +0 -0
  112. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/feed/plan.py +0 -0
  113. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/feed/presenter.py +0 -0
  114. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/feed/records.py +0 -0
  115. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/feed/transform.py +0 -0
  116. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/__init__.py +0 -0
  117. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/base.py +0 -0
  118. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/__init__.py +0 -0
  119. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/assets/README.md +0 -0
  120. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/assets/cytoscape.min.js +0 -0
  121. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/graphviz_render.py +0 -0
  122. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/html_render.py +0 -0
  123. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/layout.py +0 -0
  124. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/templates/er.html.jinja2 +0 -0
  125. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/themes.py +0 -0
  126. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/calculated.py +0 -0
  127. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/base.py.jinja2 +0 -0
  128. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/domain/__init__.py.jinja2 +0 -0
  129. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/domain/model.py.jinja2 +0 -0
  130. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/facade.py.jinja2 +0 -0
  131. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/aggregation.jinja2 +0 -0
  132. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/dataframe.jinja2 +0 -0
  133. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/filters.jinja2 +0 -0
  134. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/triggers.jinja2 +0 -0
  135. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/upsert.jinja2 +0 -0
  136. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/vector.jinja2 +0 -0
  137. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/write.jinja2 +0 -0
  138. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/session.py.jinja2 +0 -0
  139. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/__init__.py.jinja2 +0 -0
  140. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/aggregation.py.jinja2 +0 -0
  141. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/binary.py.jinja2 +0 -0
  142. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/context.py.jinja2 +0 -0
  143. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/utils/__init__.py.jinja2 +0 -0
  144. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/utils/encoders.py.jinja2 +0 -0
  145. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/utils/funcs.py.jinja2 +0 -0
  146. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/utils/rls.py.jinja2 +0 -0
  147. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/vector.py.jinja2 +0 -0
  148. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/__init__.py +0 -0
  149. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/analyzer.py +0 -0
  150. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/renderer.py +0 -0
  151. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/scopes.py +0 -0
  152. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/steps.py +0 -0
  153. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/transformer.py +0 -0
  154. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/README.md +0 -0
  155. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/__init__.py +0 -0
  156. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/blueprint.py +0 -0
  157. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/build.py +0 -0
  158. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/defaults.py +0 -0
  159. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/naming.py +0 -0
  160. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/reader.py +0 -0
  161. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/templates/schema.py.jinja2 +0 -0
  162. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/types.py +0 -0
  163. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/writer.py +0 -0
  164. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/README.md +0 -0
  165. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/__init__.py +0 -0
  166. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/analysis/README.md +0 -0
  167. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/analysis/__init__.py +0 -0
  168. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/analysis/calculated.py +0 -0
  169. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/analysis/collector.py +0 -0
  170. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/analysis/column_analyzer.py +0 -0
  171. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/analysis/constraints.py +0 -0
  172. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/analysis/hints.py +0 -0
  173. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/README.md +0 -0
  174. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/__init__.py +0 -0
  175. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/annotations.py +0 -0
  176. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/base.py +0 -0
  177. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/calculated.py +0 -0
  178. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/constraints.py +0 -0
  179. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/datasource.py +0 -0
  180. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/descriptors.py +0 -0
  181. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/encoders.py +0 -0
  182. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/enumerations.py +0 -0
  183. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/table.py +0 -0
  184. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/trigger_api.py +0 -0
  185. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/triggers.py +0 -0
  186. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/view.py +0 -0
  187. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/mapping/README.md +0 -0
  188. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/mapping/__init__.py +0 -0
  189. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/mapping/sqlalchemy_mapper.py +0 -0
  190. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/model/constraints.py +0 -0
  191. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/model/enumeration.py +0 -0
  192. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/model/schema.py +0 -0
  193. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/model/trigger.py +0 -0
  194. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/naming.py +0 -0
  195. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/ordering.py +0 -0
  196. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/registry.py +0 -0
  197. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/types.py +0 -0
  198. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/project/README.md +0 -0
  199. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/project/__init__.py +0 -0
  200. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/project/config.py +0 -0
  201. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/project/context.py +0 -0
  202. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/project/manager.py +0 -0
  203. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/project/paths.py +0 -0
  204. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/README.md +0 -0
  205. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/__init__.py +0 -0
  206. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/catalog.py +0 -0
  207. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/installer.py +0 -0
  208. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/meta.py +0 -0
  209. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/service.py +0 -0
  210. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/staleness.py +0 -0
  211. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/static/comandos.md +0 -0
  212. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/static/errores.md +0 -0
  213. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/__init__.py +0 -0
  214. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/diff/__init__.py +0 -0
  215. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/diff/constraints.py +0 -0
  216. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/diff/indexes.py +0 -0
  217. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/diff/tables.py +0 -0
  218. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/diff/views.py +0 -0
  219. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/executor.py +0 -0
  220. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/ownership.py +0 -0
  221. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/plan/__init__.py +0 -0
  222. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/plan/statements.py +0 -0
  223. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/presenter.py +0 -0
  224. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/report.py +0 -0
  225. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/state/__init__.py +0 -0
  226. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/state/local.py +0 -0
  227. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/state/model.py +0 -0
  228. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/state/normalize.py +0 -0
  229. {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/state/remote.py +0 -0
  230. {tai_sql-0.8.2 → 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.2
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.2"
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
 
@@ -6,8 +6,8 @@ from .base import SchemaTemplate
6
6
 
7
7
 
8
8
  class ChatbotSchema(SchemaTemplate):
9
- """Plantilla chatbot: Chat, Mensaje, TokenUsage, TokenUsageParte, ToolCall, Sugerencia, Modelo, Tarifa, Bolsa,
10
- LimitePresupuesto y vistas"""
9
+ """Plantilla chatbot: Chat, Mensaje, TokenUsage, TokenUsageParte, ToolCall, Sugerencia, InformeConversacion, Modelo,
10
+ Tarifa, Bolsa, LimitePresupuesto y vistas"""
11
11
 
12
12
  def get_content(self) -> str:
13
13
  return dedent(f'''
@@ -57,7 +57,7 @@ class ChatbotSchema(SchemaTemplate):
57
57
 
58
58
  id: col[bigint] = column(primary_key=True, autoincrement=True, description='UUID del chat')
59
59
  title: col[str] = column(description='Título de la conversación')
60
- username: col[str] = column(description='ID del usuario propietario del chat')
60
+ username: col[str] = column(index=True, description='ID del usuario propietario del chat')
61
61
  is_active: col[bool] = column(default=True, description='Estado activo del chat')
62
62
 
63
63
  visible: col[bool] = column(default=True, index=True, description='Si no, la conversación no se lista')
@@ -70,6 +70,7 @@ class ChatbotSchema(SchemaTemplate):
70
70
 
71
71
  messages: onetomany[Mensaje] # Relación one-to-many con la tabla Mensaje
72
72
  token_usage: onetomany[TokenUsage] # El consumo de la conversación, tenga mensaje o no
73
+ informes: onetomany[InformeConversacion] # Sus informes: los resúmenes que se pidieron de ella
73
74
 
74
75
 
75
76
  class Feedback(Enum):
@@ -93,7 +94,7 @@ class ChatbotSchema(SchemaTemplate):
93
94
  content: col[str] = column(description='Contenido del mensaje')
94
95
  role: col[MessageRole] = column(description='Rol del mensaje (user, assistant, system)')
95
96
  timestamp: col[datetime] = column(default=datetime.now, description='Timestamp del mensaje')
96
- feedback: col[Feedback] = column(default=Feedback.GOOD, description='Feedback del mensaje')
97
+ feedback: col[Feedback | None] = column(description='La valoración del usuario (good o bad); vacía si no la valoró')
97
98
  feedback_comment: col[str | None] = column(description='Comentario adicional del feedback')
98
99
  reasoning: col[str | None] = column(description='Texto de razonamiento del agente antes de usar herramientas')
99
100
  agent_iterations: col[int] = column(default=0, description='Número de iteraciones del agente')
@@ -101,11 +102,12 @@ class ChatbotSchema(SchemaTemplate):
101
102
  model_name: col[str | None] = column(description='Modelo con el que se respondió')
102
103
  provider: col[str | None] = column(description='Proveedor del modelo con el que se respondió')
103
104
 
104
- chat_id: col[bigint] = column(description='ID del chat al que pertenece el mensaje')
105
+ chat_id: col[bigint] = column(index=True, description='ID del chat al que pertenece el mensaje')
105
106
 
106
107
  token_usage: onetomany[TokenUsage] # Relación one-to-many con la tabla TokenUsage
107
108
  tool_calls: onetomany[ToolCall] # Relación one-to-many con la tabla ToolCall
108
109
  sugerencias: onetomany[Sugerencia] # Lo que se sugiere preguntar tras esta respuesta
110
+ informes: onetomany[InformeConversacion] # El informe que entrega este mensaje de sistema
109
111
 
110
112
  chat: manytoone[Chat] = relation(fields=['chat_id'], references=['id'], backref='messages') # Relación many-to-one con Chat
111
113
 
@@ -123,10 +125,10 @@ class ChatbotSchema(SchemaTemplate):
123
125
  prompt_tokens: col[int] = column(default=0, description='Tokens consumidos en el prompt')
124
126
  completion_tokens: col[int] = column(default=0, description='Tokens consumidos en la respuesta')
125
127
  total_tokens: col[int] = column(default=0, description='Total de tokens consumidos')
126
- timestamp: col[datetime] = column(default=datetime.now, description='Timestamp del consumo')
128
+ timestamp: col[datetime] = column(default=datetime.now, index=True, description='Timestamp del consumo')
127
129
 
128
130
  username: col[str] = column(index=True, description='Usuario que consumió')
129
- message_id: col[bigint | None] = column(description='Mensaje que lo generó; vacío si se borró la conversación')
131
+ message_id: col[bigint | None] = column(index=True, description='Mensaje que lo generó; vacío si se borró la conversación')
130
132
  chat_id: col[bigint | None] = column(index=True, description='Conversación en la que se consumió; vacío si no es de ninguna')
131
133
 
132
134
  tarifa_id: col[bigint | None] = column(index=True, description='Tarifa con la que se valora')
@@ -150,6 +152,10 @@ class ChatbotSchema(SchemaTemplate):
150
152
  class ToolCall(Table):
151
153
  """Tabla que almacena las llamadas a herramientas realizadas por el agente durante la generación de respuestas"""
152
154
  __tablename__ = "tool_call"
155
+ # El call_id es único dentro de su mensaje (el del modelo, no el de la fila): así una llamada de
156
+ # un sub-agente puede apuntar a la que lo creó. El índice de esta restricción sirve también para
157
+ # buscar las llamadas de un mensaje, que empieza por message_id
158
+ __constraints__ = unique_constraint("message_id", "call_id")
153
159
 
154
160
  id: col[bigint] = column(primary_key=True, autoincrement=True, description='ID de la llamada a herramienta')
155
161
  tool_name: col[str] = column(description='Nombre de la herramienta invocada')
@@ -157,10 +163,19 @@ class ChatbotSchema(SchemaTemplate):
157
163
  tool_result: col[dict | None] = column(description='Resultado de la herramienta en formato JSON')
158
164
  duration_ms: col[int | None] = column(description='Duración de la ejecución en milisegundos')
159
165
  success: col[bool] = column(default=True, description='Si la llamada fue exitosa')
166
+ call_id: col[str | None] = column(description='El id de la llamada que hizo el modelo')
167
+ padre: col[str | None] = column(description='El call_id de la herramienta que creó el sub-agente que hizo esta llamada; vacío en las del agente principal')
160
168
  timestamp: col[datetime] = column(default=datetime.now, description='Timestamp de la llamada')
161
169
  message_id: col[bigint] = column(description='ID del mensaje asociado')
162
170
 
163
171
  message: manytoone[Mensaje] = relation(fields=['message_id'], references=['id'], backref='tool_calls') # Relación many-to-one con Mensaje
172
+ # La llamada que creó el sub-agente que hizo esta: del mismo mensaje, por la clave compuesta
173
+ # (message_id, padre) → (message_id, call_id). Por el id del modelo y no por el de la fila: las
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
+ # `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)
178
+ hijas: onetomany[ToolCall]
164
179
 
165
180
 
166
181
  class TokenUsageParte(Table):
@@ -191,6 +206,33 @@ class ChatbotSchema(SchemaTemplate):
191
206
  message: manytoone[Mensaje] = relation(fields=['message_id'], references=['id'], backref='sugerencias', onDelete='cascade')
192
207
 
193
208
 
209
+ class InformeConversacion(Table):
210
+ """Tabla con los informes de una conversación: su resumen, redactado por el modelo; se descarga en DOCX o en PDF"""
211
+ __tablename__ = "informe_conversacion"
212
+
213
+ id: col[bigint] = column(primary_key=True, autoincrement=True)
214
+ extension: col[str] = column(description="'corto', 'medio' o 'largo'")
215
+ indicaciones: col[str | None] = column(description='Lo que pidió quien lo pidió (centrarse en algo, las tablas…); vacío si nada. No sale en el documento')
216
+ estado: col[str] = column(index=True, description="'preparando', 'listo' o 'error'")
217
+ error: col[str | None] = column(description='Por qué no se pudo preparar')
218
+ titulo: col[str | None] = column(description='El título que le puso el modelo')
219
+ contenido: col[str | None] = column(description='El informe, en markdown: el documento se pinta al descargarlo')
220
+ hasta_mensaje_id: col[bigint | None] = column(description='El último mensaje que resume: con el mismo, se reutiliza sin llamar al modelo')
221
+ mensajes: col[int] = column(default=0, description='Cuántos mensajes resume')
222
+ compactado: col[bool] = column(default=False, description='Si la conversación no cabía y se resumió por tandas antes de redactarlo')
223
+ reutilizado_de: col[bigint | None] = column(description='El informe del que se copió el contenido, sin llamar al modelo')
224
+ model_name: col[str | None] = column(description='Modelo con el que se redactó')
225
+ provider: col[str | None] = column(description='Proveedor del modelo con el que se redactó')
226
+ timestamp: col[datetime] = column(default=datetime.now, description='Cuándo se pidió')
227
+ chat_id: col[bigint] = column(index=True, description='La conversación que resume')
228
+ message_id: col[bigint | None] = column(index=True, description='El mensaje de sistema que lo entrega; vacío mientras se prepara')
229
+
230
+ # `chat` y `message`, como en TokenUsage: los triggers en cascada comparan las relaciones
231
+ # por su nombre, y estos no repiten el de ningún padre
232
+ chat: manytoone[Chat] = relation(fields=['chat_id'], references=['id'], backref='informes', onDelete='cascade')
233
+ message: manytoone[Mensaje] = relation(fields=['message_id'], references=['id'], backref='informes', onDelete='set null')
234
+
235
+
194
236
  class Modelo(Table):
195
237
  """Tabla que almacena los modelos con los que puede responder el agente"""
196
238
  __tablename__ = "modelo"
@@ -278,6 +320,8 @@ class ChatbotSchema(SchemaTemplate):
278
320
  total_completion_tokens: col[int]
279
321
  total_tokens: col[int]
280
322
  total_cost_eur: col[float]
323
+ total_cache_read: col[int | None]
324
+ total_cache_write: col[int | None]
281
325
  chat_count: col[int]
282
326
  message_count: col[int]
283
327
 
@@ -295,6 +339,8 @@ class ChatbotSchema(SchemaTemplate):
295
339
  total_completion_tokens: col[int]
296
340
  total_tokens: col[int]
297
341
  coste_eur: col[float]
342
+ total_cache_read: col[int | None]
343
+ total_cache_write: col[int | None]
298
344
  message_count: col[int]
299
345
 
300
346
 
@@ -379,7 +425,7 @@ class ChatbotSchema(SchemaTemplate):
379
425
  content: col[str]
380
426
  # 'good' o 'bad'
381
427
  feedback: col[str]
382
- feedback_comment: col[str]
428
+ feedback_comment: col[str | None]
383
429
  total_duration_ms: col[int | None]
384
430
  model_name: col[str | None]
385
431
  chat_id: col[bigint]
@@ -517,6 +563,10 @@ class ChatbotSchema(SchemaTemplate):
517
563
  + COALESCE(tu.cache_write, 0) * (COALESCE(t.precio_cache_write_eur_millon, t.precio_input_eur_millon) - t.precio_input_eur_millon)
518
564
  + tu.completion_tokens * t.precio_output_eur_millon)
519
565
  * t.margen / 1000000)::numeric, 8))::double precision as total_cost_eur,
566
+ -- Lo leído de la caché del proveedor y lo escrito en ella: parte de la entrada. Sin COALESCE: un
567
+ -- día o un mes sin ningún apunte que lo diga (anteriores a apuntarlo) queda vacío, no a 0
568
+ SUM(tu.cache_read) as total_cache_read,
569
+ SUM(tu.cache_write) as total_cache_write,
520
570
  COUNT(DISTINCT COALESCE(tu.chat_id, me.chat_id)) as chat_count
521
571
  FROM token_usage tu
522
572
  JOIN tarifa t ON t.id = tu.tarifa_id
@@ -557,6 +607,8 @@ class ChatbotSchema(SchemaTemplate):
557
607
  COALESCE(co.total_completion_tokens, 0) as total_completion_tokens,
558
608
  COALESCE(co.total_tokens, 0) as total_tokens,
559
609
  COALESCE(co.total_cost_eur, 0) as total_cost_eur,
610
+ co.total_cache_read,
611
+ co.total_cache_write,
560
612
  COALESCE(co.chat_count, 0) as chat_count,
561
613
  COALESCE(e.message_count, 0) as message_count
562
614
  FROM consumo co
@@ -586,7 +638,11 @@ class ChatbotSchema(SchemaTemplate):
586
638
  + COALESCE(tu.cache_read, 0) * (COALESCE(t.precio_cache_input_eur_millon, t.precio_input_eur_millon) - t.precio_input_eur_millon)
587
639
  + COALESCE(tu.cache_write, 0) * (COALESCE(t.precio_cache_write_eur_millon, t.precio_input_eur_millon) - t.precio_input_eur_millon)
588
640
  + tu.completion_tokens * t.precio_output_eur_millon)
589
- * t.margen / 1000000)::numeric, 8))::double precision as coste_eur
641
+ * t.margen / 1000000)::numeric, 8))::double precision as coste_eur,
642
+ -- Lo leído de la caché del proveedor y lo escrito en ella: parte de la entrada. Sin COALESCE: un
643
+ -- día o un mes sin ningún apunte que lo diga (anteriores a apuntarlo) queda vacío, no a 0
644
+ SUM(tu.cache_read) as total_cache_read,
645
+ SUM(tu.cache_write) as total_cache_write
590
646
  FROM token_usage tu
591
647
  JOIN tarifa t ON t.id = tu.tarifa_id
592
648
  JOIN modelo m ON m.id = t.modelo_id
@@ -625,6 +681,8 @@ class ChatbotSchema(SchemaTemplate):
625
681
  COALESCE(co.total_completion_tokens, 0) as total_completion_tokens,
626
682
  COALESCE(co.total_tokens, 0) as total_tokens,
627
683
  COALESCE(co.coste_eur, 0) as coste_eur,
684
+ co.total_cache_read,
685
+ co.total_cache_write,
628
686
  COALESCE(e.message_count, 0) as message_count
629
687
  FROM consumo co
630
688
  FULL JOIN enviados e
@@ -716,9 +774,8 @@ class ChatbotSchema(SchemaTemplate):
716
774
  ''').rstrip()
717
775
 
718
776
  valoracion_sql = dedent('''\
719
- -- Vista con las respuestas que el usuario valoró con un comentario, con su conversación
720
- -- y lo que se apuntó con ellas. Solo las que tienen comentario: la columna feedback nace
721
- -- 'good', así que una valoración sin texto no se distingue de un mensaje sin valorar
777
+ -- Vista con las respuestas que el usuario valoró (con comentario o sin él), con su
778
+ -- conversación y lo que se apuntó con ellas. Un mensaje sin valorar tiene feedback vacío
722
779
  SELECT
723
780
  m.id as message_id,
724
781
  m.timestamp,
@@ -734,8 +791,7 @@ class ChatbotSchema(SchemaTemplate):
734
791
  FROM mensaje m
735
792
  JOIN chat c ON c.id = m.chat_id
736
793
  WHERE m.role = 'asistente'
737
- AND m.feedback_comment IS NOT NULL
738
- AND trim(m.feedback_comment) <> ''
794
+ AND m.feedback IS NOT NULL
739
795
  ORDER BY m.timestamp DESC;
740
796
  ''').rstrip()
741
797
 
@@ -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')