tai-sql 0.7.20__tar.gz → 0.8.0__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.7.20 → tai_sql-0.8.0}/PKG-INFO +2 -2
  2. {tai_sql-0.7.20 → tai_sql-0.8.0}/README.md +1 -1
  3. {tai_sql-0.7.20 → tai_sql-0.8.0}/pyproject.toml +1 -1
  4. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_schema/schemas/chatbot.py +118 -36
  5. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/__init__.py +23 -1
  6. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/context.py +38 -0
  7. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/base.py.jinja2 +58 -0
  8. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/domain/dao.py.jinja2 +68 -5
  9. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/domain/dtos.py.jinja2 +126 -18
  10. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/facade.py.jinja2 +4 -1
  11. tai_sql-0.8.0/tai_sql/generators/python_client/templates/macros/aggregation.jinja2 +456 -0
  12. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/macros/filters.jinja2 +24 -13
  13. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/macros/write.jinja2 +150 -54
  14. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/shared/__init__.py.jinja2 +21 -2
  15. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/shared/aggregation.py.jinja2 +80 -18
  16. tai_sql-0.8.0/tai_sql/generators/python_client/templates/shared/binary.py.jinja2 +99 -0
  17. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/shared/utils/__init__.py.jinja2 +5 -1
  18. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/shared/utils/funcs.py.jinja2 +7 -0
  19. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/shared/utils/rls.py.jinja2 +43 -1
  20. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/declarative/trigger_api.py +2 -2
  21. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/types.py +56 -0
  22. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/rules/static/errores.md +70 -0
  23. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/rules/static/modelo.md +125 -0
  24. tai_sql-0.7.20/tai_sql/generators/python_client/templates/macros/aggregation.jinja2 +0 -910
  25. {tai_sql-0.7.20 → tai_sql-0.8.0}/LICENSE +0 -0
  26. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/__init__.py +0 -0
  27. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/__init__.py +0 -0
  28. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_backup/__init__.py +0 -0
  29. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_backup/main.py +0 -0
  30. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_deploy/__init__.py +0 -0
  31. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_deploy/config.py +0 -0
  32. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_deploy/local.py +0 -0
  33. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_deploy/main.py +0 -0
  34. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_deploy/workflow.py +0 -0
  35. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_feed/__init__.py +0 -0
  36. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_feed/main.py +0 -0
  37. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_generate/__init__.py +0 -0
  38. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_generate/funcs.py +0 -0
  39. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_generate/main.py +0 -0
  40. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_hash_migrate/__init__.py +0 -0
  41. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_hash_migrate/main.py +0 -0
  42. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_info/__init__.py +0 -0
  43. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_info/main.py +0 -0
  44. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_info/status.py +0 -0
  45. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_init/__init__.py +0 -0
  46. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_init/main.py +0 -0
  47. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_init/model.py +0 -0
  48. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_install/__init__.py +0 -0
  49. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_install/main.py +0 -0
  50. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_ping/__init__.py +0 -0
  51. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_ping/main.py +0 -0
  52. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_pull/__init__.py +0 -0
  53. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_pull/funcs.py +0 -0
  54. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_pull/main.py +0 -0
  55. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_restore/__init__.py +0 -0
  56. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_restore/main.py +0 -0
  57. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_rules/__init__.py +0 -0
  58. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_rules/main.py +0 -0
  59. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_schema/__init__.py +0 -0
  60. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_schema/model.py +0 -0
  61. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_schema/new.py +0 -0
  62. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_schema/schemas/__init__.py +0 -0
  63. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_schema/schemas/auth.py +0 -0
  64. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_schema/schemas/base.py +0 -0
  65. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_schema/schemas/default.py +0 -0
  66. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/cmd_schema/set_default.py +0 -0
  67. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/prompts.py +0 -0
  68. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/utils/__init__.py +0 -0
  69. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/utils/connectivity.py +0 -0
  70. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/utils/create.py +0 -0
  71. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/utils/github/__init__.py +0 -0
  72. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/utils/github/auth.py +0 -0
  73. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/utils/github/client.py +0 -0
  74. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/commands/utils/github/models.py +0 -0
  75. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/errors.py +0 -0
  76. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/main.py +0 -0
  77. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/preamble.py +0 -0
  78. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/services/__init__.py +0 -0
  79. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/cli/services/backup.py +0 -0
  80. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/connection/README.md +0 -0
  81. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/connection/__init__.py +0 -0
  82. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/connection/provider.py +0 -0
  83. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/connection/recipe.py +0 -0
  84. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/connection/session.py +0 -0
  85. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/connection/sources.py +0 -0
  86. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/crypto.py +0 -0
  87. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/drivers/__init__.py +0 -0
  88. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/drivers/base.py +0 -0
  89. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/drivers/bigquery.py +0 -0
  90. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/drivers/capabilities.py +0 -0
  91. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/drivers/mysql.py +0 -0
  92. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/drivers/postgresql.py +0 -0
  93. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/drivers/sqlserver.py +0 -0
  94. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/errors/README.md +0 -0
  95. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/errors/__init__.py +0 -0
  96. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/errors/collector.py +0 -0
  97. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/errors/render.py +0 -0
  98. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/errors/types.py +0 -0
  99. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/feed/README.md +0 -0
  100. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/feed/__init__.py +0 -0
  101. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/feed/collect.py +0 -0
  102. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/feed/executor.py +0 -0
  103. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/feed/feeder.py +0 -0
  104. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/feed/plan.py +0 -0
  105. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/feed/presenter.py +0 -0
  106. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/feed/records.py +0 -0
  107. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/feed/transform.py +0 -0
  108. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/__init__.py +0 -0
  109. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/base.py +0 -0
  110. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/diagram/README.md +0 -0
  111. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/diagram/__init__.py +0 -0
  112. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/diagram/assets/README.md +0 -0
  113. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/diagram/assets/cytoscape.min.js +0 -0
  114. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/diagram/builder.py +0 -0
  115. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/diagram/generator.py +0 -0
  116. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/diagram/graphviz_render.py +0 -0
  117. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/diagram/html_render.py +0 -0
  118. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/diagram/layout.py +0 -0
  119. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/diagram/model.py +0 -0
  120. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/diagram/templates/er.html.jinja2 +0 -0
  121. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/diagram/themes.py +0 -0
  122. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/calculated.py +0 -0
  123. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/domain/__init__.py.jinja2 +0 -0
  124. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/domain/model.py.jinja2 +0 -0
  125. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/macros/dataframe.jinja2 +0 -0
  126. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/macros/hierarchy.jinja2 +0 -0
  127. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/macros/read.jinja2 +0 -0
  128. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/macros/triggers.jinja2 +0 -0
  129. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/macros/upsert.jinja2 +0 -0
  130. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/macros/vector.jinja2 +0 -0
  131. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/session.py.jinja2 +0 -0
  132. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/shared/context.py.jinja2 +0 -0
  133. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/shared/utils/encoders.py.jinja2 +0 -0
  134. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/shared/utils/hierarchy.py.jinja2 +0 -0
  135. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/templates/shared/vector.py.jinja2 +0 -0
  136. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/triggers/__init__.py +0 -0
  137. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/triggers/analyzer.py +0 -0
  138. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/triggers/renderer.py +0 -0
  139. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/triggers/scopes.py +0 -0
  140. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/triggers/steps.py +0 -0
  141. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/python_client/triggers/transformer.py +0 -0
  142. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/rules/__init__.py +0 -0
  143. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/generators/rules/templates/schema.md.jinja2 +0 -0
  144. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/introspect/README.md +0 -0
  145. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/introspect/__init__.py +0 -0
  146. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/introspect/blueprint.py +0 -0
  147. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/introspect/build.py +0 -0
  148. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/introspect/defaults.py +0 -0
  149. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/introspect/naming.py +0 -0
  150. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/introspect/reader.py +0 -0
  151. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/introspect/templates/schema.py.jinja2 +0 -0
  152. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/introspect/types.py +0 -0
  153. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/introspect/writer.py +0 -0
  154. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/README.md +0 -0
  155. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/__init__.py +0 -0
  156. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/analysis/README.md +0 -0
  157. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/analysis/__init__.py +0 -0
  158. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/analysis/calculated.py +0 -0
  159. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/analysis/collector.py +0 -0
  160. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/analysis/column_analyzer.py +0 -0
  161. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/analysis/constraints.py +0 -0
  162. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/analysis/hints.py +0 -0
  163. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/analysis/relation_analyzer.py +0 -0
  164. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/analysis/schema_analyzer.py +0 -0
  165. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/declarative/README.md +0 -0
  166. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/declarative/__init__.py +0 -0
  167. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/declarative/annotations.py +0 -0
  168. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/declarative/base.py +0 -0
  169. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/declarative/calculated.py +0 -0
  170. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/declarative/columns.py +0 -0
  171. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/declarative/constraints.py +0 -0
  172. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/declarative/datasource.py +0 -0
  173. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/declarative/descriptors.py +0 -0
  174. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/declarative/encoders.py +0 -0
  175. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/declarative/enumerations.py +0 -0
  176. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/declarative/relations.py +0 -0
  177. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/declarative/table.py +0 -0
  178. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/declarative/triggers.py +0 -0
  179. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/declarative/view.py +0 -0
  180. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/mapping/README.md +0 -0
  181. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/mapping/__init__.py +0 -0
  182. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/mapping/sqlalchemy_mapper.py +0 -0
  183. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/model/README.md +0 -0
  184. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/model/__init__.py +0 -0
  185. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/model/column.py +0 -0
  186. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/model/constraints.py +0 -0
  187. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/model/entity.py +0 -0
  188. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/model/enumeration.py +0 -0
  189. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/model/relation.py +0 -0
  190. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/model/schema.py +0 -0
  191. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/model/trigger.py +0 -0
  192. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/naming.py +0 -0
  193. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/ordering.py +0 -0
  194. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/orm/registry.py +0 -0
  195. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/project/README.md +0 -0
  196. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/project/__init__.py +0 -0
  197. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/project/config.py +0 -0
  198. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/project/context.py +0 -0
  199. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/project/manager.py +0 -0
  200. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/project/paths.py +0 -0
  201. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/rules/README.md +0 -0
  202. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/rules/__init__.py +0 -0
  203. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/rules/catalog.py +0 -0
  204. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/rules/installer.py +0 -0
  205. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/rules/meta.py +0 -0
  206. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/rules/service.py +0 -0
  207. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/rules/staleness.py +0 -0
  208. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/rules/static/comandos.md +0 -0
  209. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/schema.py +0 -0
  210. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/README.md +0 -0
  211. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/__init__.py +0 -0
  212. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/diff/__init__.py +0 -0
  213. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/diff/constraints.py +0 -0
  214. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/diff/indexes.py +0 -0
  215. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/diff/tables.py +0 -0
  216. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/diff/views.py +0 -0
  217. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/executor.py +0 -0
  218. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/ownership.py +0 -0
  219. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/plan/__init__.py +0 -0
  220. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/plan/planner.py +0 -0
  221. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/plan/statements.py +0 -0
  222. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/presenter.py +0 -0
  223. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/report.py +0 -0
  224. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/safety.py +0 -0
  225. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/state/__init__.py +0 -0
  226. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/state/local.py +0 -0
  227. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/state/model.py +0 -0
  228. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/state/normalize.py +0 -0
  229. {tai_sql-0.7.20 → tai_sql-0.8.0}/tai_sql/sync/state/remote.py +0 -0
  230. {tai_sql-0.7.20 → tai_sql-0.8.0}/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.7.20
3
+ Version: 0.8.0
4
4
  Summary: SQL database management and code generation tool
5
5
  License-Expression: LicenseRef-Proprietary
6
6
  License-File: LICENSE
@@ -192,7 +192,7 @@ usuarios = public_sync_api.usuario.find_many(limit=10, includes=['posts'])
192
192
  | [El schema](https://triplealpha-innovation.github.io/tai-sql/schema/) | Todo lo que se puede declarar, y cómo |
193
193
  | [El CLI](https://triplealpha-innovation.github.io/tai-sql/cli/) | Qué hace cada comando, y qué puede destruir `push` |
194
194
  | [El cliente generado](https://triplealpha-innovation.github.io/tai-sql/cliente/) | La API que vas a usar desde tu aplicación |
195
- | [Referencia](https://triplealpha-innovation.github.io/tai-sql/referencia/dsl/) | Firmas del DSL, motores, extensión y catálogo de errores |
195
+ | [Referencia](https://triplealpha-innovation.github.io/tai-sql/referencia/dsl/) | Firmas del DSL, motores, extensión, catálogo de errores y qué cambia al actualizar a la 0.8.0 |
196
196
 
197
197
  ---
198
198
 
@@ -129,7 +129,7 @@ usuarios = public_sync_api.usuario.find_many(limit=10, includes=['posts'])
129
129
  | [El schema](https://triplealpha-innovation.github.io/tai-sql/schema/) | Todo lo que se puede declarar, y cómo |
130
130
  | [El CLI](https://triplealpha-innovation.github.io/tai-sql/cli/) | Qué hace cada comando, y qué puede destruir `push` |
131
131
  | [El cliente generado](https://triplealpha-innovation.github.io/tai-sql/cliente/) | La API que vas a usar desde tu aplicación |
132
- | [Referencia](https://triplealpha-innovation.github.io/tai-sql/referencia/dsl/) | Firmas del DSL, motores, extensión y catálogo de errores |
132
+ | [Referencia](https://triplealpha-innovation.github.io/tai-sql/referencia/dsl/) | Firmas del DSL, motores, extensión, catálogo de errores y qué cambia al actualizar a la 0.8.0 |
133
133
 
134
134
  ---
135
135
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tai-sql"
3
- version = "0.7.20"
3
+ version = "0.8.0"
4
4
  description = "SQL database management and code generation tool"
5
5
  license = "LicenseRef-Proprietary"
6
6
  license-files = ["LICENSE"]
@@ -435,48 +435,130 @@ class ChatbotSchema(SchemaTemplate):
435
435
  ''').rstrip()
436
436
 
437
437
  consumo_diario_sql = dedent('''\
438
- -- Vista para el consumo por usuario, día y modelo
438
+ -- Vista para el consumo por usuario, día y modelo. Los tokens y el importe son todos los
439
+ -- apuntes, tengan mensaje o no; los mensajes, los que envió el usuario
440
+ WITH consumo AS (
441
+ SELECT
442
+ tu.username,
443
+ DATE(tu.timestamp) as date,
444
+ m.provider,
445
+ m.model_name,
446
+ SUM(tu.prompt_tokens) as total_prompt_tokens,
447
+ SUM(tu.completion_tokens) as total_completion_tokens,
448
+ SUM(tu.total_tokens) as total_tokens,
449
+ SUM(ROUND(((tu.prompt_tokens * t.precio_input_eur_millon
450
+ + tu.completion_tokens * t.precio_output_eur_millon)
451
+ * t.margen / 1000000)::numeric, 8))::double precision as total_cost_eur,
452
+ COUNT(DISTINCT COALESCE(tu.chat_id, me.chat_id)) as chat_count
453
+ FROM token_usage tu
454
+ JOIN tarifa t ON t.id = tu.tarifa_id
455
+ JOIN modelo m ON m.id = t.modelo_id
456
+ LEFT JOIN mensaje me ON me.id = tu.message_id
457
+ GROUP BY tu.username, DATE(tu.timestamp), m.provider, m.model_name
458
+ ),
459
+ -- Cada mensaje del usuario cuenta en el modelo al que se envió. Los anteriores a guardarlo,
460
+ -- en el de la respuesta que los siguió; sin ninguno de los dos no se cuentan
461
+ enviados AS (
462
+ SELECT
463
+ c.username,
464
+ DATE(u.timestamp) as date,
465
+ COALESCE(u.provider, r.provider) as provider,
466
+ COALESCE(u.model_name, r.model_name) as model_name,
467
+ COUNT(*) as message_count
468
+ FROM mensaje u
469
+ JOIN chat c ON c.id = u.chat_id
470
+ LEFT JOIN LATERAL (
471
+ SELECT a.provider, a.model_name
472
+ FROM mensaje a
473
+ WHERE a.chat_id = u.chat_id
474
+ AND a.role IN ('asistente', 'ai', 'assistant')
475
+ AND a.timestamp >= u.timestamp
476
+ ORDER BY a.timestamp
477
+ LIMIT 1
478
+ ) r ON u.model_name IS NULL
479
+ WHERE u.role IN ('usuario', 'human', 'user')
480
+ AND COALESCE(u.model_name, r.model_name) IS NOT NULL
481
+ GROUP BY c.username, DATE(u.timestamp), COALESCE(u.provider, r.provider), COALESCE(u.model_name, r.model_name)
482
+ )
439
483
  SELECT
440
- tu.username,
441
- DATE(tu.timestamp) as date,
442
- m.provider,
443
- m.model_name,
444
- SUM(tu.prompt_tokens) as total_prompt_tokens,
445
- SUM(tu.completion_tokens) as total_completion_tokens,
446
- SUM(tu.total_tokens) as total_tokens,
447
- SUM(ROUND(((tu.prompt_tokens * t.precio_input_eur_millon
448
- + tu.completion_tokens * t.precio_output_eur_millon)
449
- * t.margen / 1000000)::numeric, 8))::double precision as total_cost_eur,
450
- COUNT(DISTINCT COALESCE(tu.chat_id, me.chat_id)) as chat_count,
451
- COUNT(*) as message_count
452
- FROM token_usage tu
453
- JOIN tarifa t ON t.id = tu.tarifa_id
454
- JOIN modelo m ON m.id = t.modelo_id
455
- LEFT JOIN mensaje me ON me.id = tu.message_id
456
- GROUP BY tu.username, DATE(tu.timestamp), m.provider, m.model_name
457
- ORDER BY date DESC, total_tokens DESC;
484
+ COALESCE(co.username, e.username) as username,
485
+ COALESCE(co.date, e.date) as date,
486
+ COALESCE(co.provider, e.provider) as provider,
487
+ COALESCE(co.model_name, e.model_name) as model_name,
488
+ COALESCE(co.total_prompt_tokens, 0) as total_prompt_tokens,
489
+ COALESCE(co.total_completion_tokens, 0) as total_completion_tokens,
490
+ COALESCE(co.total_tokens, 0) as total_tokens,
491
+ COALESCE(co.total_cost_eur, 0) as total_cost_eur,
492
+ COALESCE(co.chat_count, 0) as chat_count,
493
+ COALESCE(e.message_count, 0) as message_count
494
+ FROM consumo co
495
+ FULL JOIN enviados e
496
+ ON e.username = co.username AND e.date = co.date
497
+ AND e.provider = co.provider AND e.model_name = co.model_name
498
+ ORDER BY date DESC, username, total_tokens DESC;
458
499
  ''').rstrip()
459
500
 
460
501
  consumo_mensual_sql = dedent('''\
461
502
  -- Vista para el consumo y el coste por usuario, mes y modelo. El importe se calcula aquí:
462
- -- el apunte guarda con qué tarifa se valora y el margen no sale de la tabla `tarifa`
503
+ -- el apunte guarda con qué tarifa se valora y el margen no sale de la tabla `tarifa`.
504
+ -- Los tokens y el importe son todos los apuntes, tengan mensaje o no (una redacción del modal
505
+ -- de incidencias también); los mensajes, los que envió el usuario
506
+ WITH consumo AS (
507
+ SELECT
508
+ tu.username,
509
+ DATE_TRUNC('month', tu.timestamp)::date as mes,
510
+ m.provider,
511
+ m.model_name,
512
+ SUM(tu.prompt_tokens) as total_prompt_tokens,
513
+ SUM(tu.completion_tokens) as total_completion_tokens,
514
+ SUM(tu.total_tokens) as total_tokens,
515
+ SUM(ROUND(((tu.prompt_tokens * t.precio_input_eur_millon
516
+ + tu.completion_tokens * t.precio_output_eur_millon)
517
+ * t.margen / 1000000)::numeric, 8))::double precision as coste_eur
518
+ FROM token_usage tu
519
+ JOIN tarifa t ON t.id = tu.tarifa_id
520
+ JOIN modelo m ON m.id = t.modelo_id
521
+ GROUP BY tu.username, DATE_TRUNC('month', tu.timestamp)::date, m.provider, m.model_name
522
+ ),
523
+ -- Cada mensaje del usuario cuenta en el modelo al que se envió. Los anteriores a guardarlo,
524
+ -- en el de la respuesta que los siguió; sin ninguno de los dos no se cuentan
525
+ enviados AS (
526
+ SELECT
527
+ c.username,
528
+ DATE_TRUNC('month', u.timestamp)::date as mes,
529
+ COALESCE(u.provider, r.provider) as provider,
530
+ COALESCE(u.model_name, r.model_name) as model_name,
531
+ COUNT(*) as message_count
532
+ FROM mensaje u
533
+ JOIN chat c ON c.id = u.chat_id
534
+ LEFT JOIN LATERAL (
535
+ SELECT a.provider, a.model_name
536
+ FROM mensaje a
537
+ WHERE a.chat_id = u.chat_id
538
+ AND a.role IN ('asistente', 'ai', 'assistant')
539
+ AND a.timestamp >= u.timestamp
540
+ ORDER BY a.timestamp
541
+ LIMIT 1
542
+ ) r ON u.model_name IS NULL
543
+ WHERE u.role IN ('usuario', 'human', 'user')
544
+ AND COALESCE(u.model_name, r.model_name) IS NOT NULL
545
+ GROUP BY c.username, DATE_TRUNC('month', u.timestamp)::date, COALESCE(u.provider, r.provider), COALESCE(u.model_name, r.model_name)
546
+ )
463
547
  SELECT
464
- tu.username,
465
- DATE_TRUNC('month', tu.timestamp)::date as mes,
466
- m.provider,
467
- m.model_name,
468
- SUM(tu.prompt_tokens) as total_prompt_tokens,
469
- SUM(tu.completion_tokens) as total_completion_tokens,
470
- SUM(tu.total_tokens) as total_tokens,
471
- SUM(ROUND(((tu.prompt_tokens * t.precio_input_eur_millon
472
- + tu.completion_tokens * t.precio_output_eur_millon)
473
- * t.margen / 1000000)::numeric, 8))::double precision as coste_eur,
474
- COUNT(*) as message_count
475
- FROM token_usage tu
476
- JOIN tarifa t ON t.id = tu.tarifa_id
477
- JOIN modelo m ON m.id = t.modelo_id
478
- GROUP BY tu.username, DATE_TRUNC('month', tu.timestamp), m.provider, m.model_name
479
- ORDER BY mes DESC, tu.username, total_tokens DESC;
548
+ COALESCE(co.username, e.username) as username,
549
+ COALESCE(co.mes, e.mes) as mes,
550
+ COALESCE(co.provider, e.provider) as provider,
551
+ COALESCE(co.model_name, e.model_name) as model_name,
552
+ COALESCE(co.total_prompt_tokens, 0) as total_prompt_tokens,
553
+ COALESCE(co.total_completion_tokens, 0) as total_completion_tokens,
554
+ COALESCE(co.total_tokens, 0) as total_tokens,
555
+ COALESCE(co.coste_eur, 0) as coste_eur,
556
+ COALESCE(e.message_count, 0) as message_count
557
+ FROM consumo co
558
+ FULL JOIN enviados e
559
+ ON e.username = co.username AND e.mes = co.mes
560
+ AND e.provider = co.provider AND e.model_name = co.model_name
561
+ ORDER BY mes DESC, username, total_tokens DESC;
480
562
  ''').rstrip()
481
563
 
482
564
  modelo_disponible_sql = dedent('''\
@@ -428,9 +428,31 @@ class PythonClientGenerator(BaseGenerator):
428
428
  for m in all_models_data
429
429
  )
430
430
 
431
+ # Las columnas bytes viajan en base64 en JSON: su tipo en los DTOs vive en binary.py
432
+ has_bytes = any(
433
+ col.get('dto_type') == 'JsonBytes'
434
+ for m in all_models_data
435
+ for col in m.get('columns', [])
436
+ )
437
+
431
438
  # _shared/__init__.py
432
439
  init_template = self.jinja_env.get_template('shared/__init__.py.jinja2')
433
- (shared_dir / '__init__.py').write_text(init_template.render(has_hierarchy=has_hierarchy, has_vector_encoders=has_vector_encoders), encoding='utf-8')
440
+ (shared_dir / '__init__.py').write_text(
441
+ init_template.render(
442
+ has_hierarchy=has_hierarchy,
443
+ has_vector_encoders=has_vector_encoders,
444
+ has_bytes=has_bytes,
445
+ ),
446
+ encoding='utf-8',
447
+ )
448
+
449
+ # _shared/binary.py — only when some column is bytes
450
+ binary_path = shared_dir / 'binary.py'
451
+ if has_bytes:
452
+ binary_template = self.jinja_env.get_template('shared/binary.py.jinja2')
453
+ binary_path.write_text(binary_template.render(), encoding='utf-8')
454
+ elif binary_path.exists():
455
+ binary_path.unlink()
434
456
 
435
457
  # _shared/context.py
436
458
  context_template = self.jinja_env.get_template('shared/context.py.jinja2')
@@ -204,6 +204,14 @@ class TemplateContextBuilder:
204
204
  constraints = self._constraints(column)
205
205
  sa_type, sa_type_args = _type_with_params(column)
206
206
 
207
+ # La clave autoincremental solo se filtra por lista. Igualdad no hace falta —para un
208
+ # registro está `find(id)`— y un rango sobre un contador que pone el motor no dice
209
+ # nada del dato; pero «estos ids» sí es una pregunta legítima (recorrer un grafo por
210
+ # niveles, traer lo que otra consulta devolvió), y sin `in_<pk>` solo se podía hacer
211
+ # con una consulta por id. Hasta 0.7.21 no tenía ningún filtro.
212
+ if constraints['autoincrement']:
213
+ filter_fields = ['in']
214
+
207
215
  context = {
208
216
  'name': column.name,
209
217
  'type': column.type,
@@ -231,6 +239,9 @@ class TemplateContextBuilder:
231
239
  # El tamaño máximo de un texto (`column(length=n)`), para quien valide antes de
232
240
  # escribir —los esquemas de formulario de tai-api—. `None` si no se declaró.
233
241
  'max_length': column.length if column.type == 'str' else None,
242
+ # Las agregaciones que admite: el DAO rechaza el resto con un error, y tai-api
243
+ # restringe con esto las columnas de cada operación en su cliente TypeScript.
244
+ 'aggregations': _aggregations(column, descriptor),
234
245
  }
235
246
 
236
247
  if column.is_vector:
@@ -250,6 +261,15 @@ class TemplateContextBuilder:
250
261
  'threshold': vector.threshold,
251
262
  })
252
263
 
264
+ # El tipo del campo en los DTOs. Es `pydantic_type` salvo en `bytes`, que en JSON
265
+ # viaja en base64 (`JsonBytes`, de `_shared/binary.py`): con `bytes` desnudo,
266
+ # pydantic lee y escribe el texto en UTF-8, así que guardaba el base64 que manda un
267
+ # frontend como texto y no podía devolver un binario de verdad. `pydantic_type` no
268
+ # cambia porque es contrato: tai-api lo lee y espera `'bytes'`.
269
+ context['dto_type'] = (
270
+ 'JsonBytes' if context['pydantic_type'] == 'bytes' else context['pydantic_type']
271
+ )
272
+
253
273
  return context
254
274
 
255
275
  def calculated_column(
@@ -566,6 +586,24 @@ def _template_default(default: Any) -> Any:
566
586
  return default
567
587
 
568
588
 
589
+ def _aggregations(column: ColumnModel, descriptor: Any) -> List[str]:
590
+ """
591
+ Las agregaciones que admite una columna: las de su tipo, menos lo que la columna misma
592
+ hace absurdo o peligroso. En esas tres solo queda `count`, que no mira el valor:
593
+
594
+ - **cifrada**: el motor ordenaría el texto cifrado —con un vector de inicialización
595
+ aleatorio, así que sin relación con el valor— y devolvería el cifrado sin descifrar.
596
+ - **hasheada**: `max` devolvería un hash bcrypt, que el cliente no expone en ninguna
597
+ lectura (tai-api lo saca de toda respuesta).
598
+ - **enumerado**: se guarda como texto en los cuatro motores, así que `max` sería el
599
+ último valor **por orden alfabético**, no el último declarado —que es lo que alguien
600
+ espera de un estado—. La respuesta honesta es `count` con `group_by`.
601
+ """
602
+ if column.encrypt or column.hash or column.options is not None:
603
+ return ['count']
604
+ return descriptor.aggregations() if descriptor else ['count']
605
+
606
+
569
607
  def _type_with_params(column: ColumnModel) -> tuple:
570
608
  """
571
609
  El tipo SQLAlchemy del modelo generado y sus argumentos: `('String', '(50)')`.
@@ -38,6 +38,64 @@ class TriggerAborted(ValueError):
38
38
  self.message = message
39
39
  super().__init__(f"Trigger abortado: {message}")
40
40
 
41
+
42
+ class NestedWriteError(Exception):
43
+ """
44
+ Una escritura anidada del `update` no se puede aplicar al registro que se actualiza.
45
+
46
+ Un anidado nombra hijos **de ese registro**: los crea colgando de él, o los actualiza y
47
+ los borra por su clave. Lo que no casa con eso no se ignora —hasta 0.7.21 un hijo que
48
+ no se encontraba se saltaba sin error— y se lanza una de sus dos subclases. La
49
+ transacción se deshace entera: ni el padre ni los demás hijos quedan escritos.
50
+
51
+ - `message`: el texto para quien hizo la petición, con la solución.
52
+ - `field`: dónde está en el payload, con la forma del JSON (`resenas.delete.0`,
53
+ `libros.update.0.values.resenas.update.1`; en una uno a uno, sin índice). Lo
54
+ construye cada nivel al subir, así que es la ruta completa desde el registro raíz.
55
+ - `table`: la tabla del hijo. `key`: su clave, tal como llegó.
56
+ - `reason`: `'not_found'` o `'parent_mismatch'`.
57
+ """
58
+
59
+ #: Para reconocerla sin importar la clase, como `tai_trigger_abort`: tai-api atiende a
60
+ #: los clientes de varios schemas con un solo manejador, y cada uno tiene la suya.
61
+ tai_nested_write = True
62
+ reason: str = ''
63
+
64
+ def __init__(self, message: str, *, field: str, table: str, key=None):
65
+ self.message = message
66
+ self.field = field
67
+ self.table = table
68
+ self.key = key
69
+ super().__init__(message)
70
+
71
+ def _under(self, prefix: str) -> 'NestedWriteError':
72
+ """La misma excepción, vista desde el padre: antepone su tramo a `field`."""
73
+ self.field = f'{prefix}.{self.field}'
74
+ return self
75
+
76
+
77
+ class NestedRowNotFound(NestedWriteError, LookupError):
78
+ """
79
+ Un `update` o `delete` anidado nombra una fila que no está bajo el registro que se
80
+ actualiza: no existe, cuelga de otro padre o la regla de RLS no la alcanza.
81
+
82
+ Los tres casos son el mismo a propósito —para quien llama, esa fila no es hija de este
83
+ registro— y distinguirlos delataría filas que la regla oculta. Es lo que tai-api
84
+ responde con un 404.
85
+ """
86
+
87
+ reason = 'not_found'
88
+
89
+
90
+ class NestedParentMismatch(NestedWriteError, ValueError):
91
+ """
92
+ Un `update` anidado cambia las columnas que unen el hijo con su padre: lo sacaría del
93
+ registro bajo el que se ha nombrado. Para mover una fila de padre se usa el DAO de su
94
+ tabla. Es una petición inválida (un 422 en tai-api), no una fila que falte.
95
+ """
96
+
97
+ reason = 'parent_mismatch'
98
+
41
99
  {% if has_encrypted_columns %}
42
100
 
43
101
  # Configuración de encriptación
@@ -59,7 +59,10 @@ from {{ import_prefix }}_shared import (
59
59
  RLS,
60
60
  _log_tx,
61
61
  _log_commit,
62
- OPERATION_TYPE_VALIDATORS,
62
+ AGG_OPERATIONS,
63
+ agg_field_error,
64
+ agg_expression_error,
65
+ agg_value,
63
66
  OPERATION_FUNCTIONS,
64
67
  GROUP_BY_FORBIDDEN_TYPES,
65
68
  GROUP_BY_DATETIME_TYPES,
@@ -91,6 +94,17 @@ from {{ import_prefix }}_base import verify_hash, HASH_DE_RELLENO
91
94
  {% if model.triggers is defined and (model.triggers['create'] or model.triggers['update'] or model.triggers['delete']) %}
92
95
  from {{ import_prefix }}_base import TriggerAborted
93
96
  {% endif %}
97
+ {# Las escrituras anidadas del `update`: se acotan al padre y lo que no casa se lanza. #}
98
+ {% set _dao_nested = [] %}
99
+ {% for relation in model.relations | default([]) if relation.supports_nested_write and not model.is_view %}
100
+ {% if models | selectattr('name', 'equalto', relation.target) | first %}
101
+ {% set _ = _dao_nested.append(relation.name) %}
102
+ {% endif %}
103
+ {% endfor %}
104
+ {% if _dao_nested %}
105
+ from {{ import_prefix }}_base import NestedWriteError, NestedRowNotFound, NestedParentMismatch
106
+ from {{ import_prefix }}_shared import scope_to_parent
107
+ {% endif %}
94
108
  {# ── Collect cross-DAO targets (relations + trigger cross-table ops) ── #}
95
109
  {% set _cross_dao_targets = [] %}
96
110
  {% for rel in model.relations | default([]) %}
@@ -140,6 +154,41 @@ def _para_log(valores: dict) -> dict:
140
154
  """
141
155
  return {clave: ('***' if clave in _COLUMNAS_PROTEGIDAS else valor) for clave, valor in valores.items()}
142
156
  {% endif %}
157
+ {% if _dao_nested %}
158
+
159
+
160
+ def _hijo_que_no_esta(tabla: str, clave: Any, field: str) -> NestedRowNotFound:
161
+ """La excepción de un `update`/`delete` anidado que no alcanza ninguna fila."""
162
+ return NestedRowNotFound(
163
+ f"{tabla} {clave!r} no es hija de este {{ model.tablename }}: no existe, cuelga de "
164
+ "otro registro o la regla de seguridad no la alcanza. Un update o delete anidado "
165
+ "solo llega a los hijos del registro que se actualiza: comprueba la clave, o "
166
+ "escribe esa fila con el DAO de su tabla.",
167
+ field=field, table=tabla, key=clave,
168
+ )
169
+
170
+
171
+ def _comprobar_union(valores: Any, bajo: Dict[str, Any], tabla: str, field: str) -> None:
172
+ """
173
+ Lanza si un `update` anidado cambia la unión del hijo con este registro.
174
+
175
+ Sacaría la fila de debajo del registro bajo el que se ha nombrado, que es justo lo que
176
+ acotar el anidado al padre impide. Reenviar el valor que ya tiene no es un cambio.
177
+ """
178
+ cambios = {
179
+ columna: getattr(valores, columna)
180
+ for columna, valor in bajo.items()
181
+ if columna in valores.model_fields_set and getattr(valores, columna) != valor
182
+ }
183
+ if cambios:
184
+ raise NestedParentMismatch(
185
+ f"Un update anidado no puede cambiar la unión de {tabla} con este "
186
+ f"{{ model.tablename }} ({cambios}): la sacaría del registro bajo el que se ha "
187
+ "nombrado. Quita esas columnas de los valores, o mueve la fila con el DAO de "
188
+ "su tabla.",
189
+ field=f"{field}.{next(iter(cambios))}", table=tabla, key=None,
190
+ )
191
+ {% endif %}
143
192
 
144
193
  {% set vector_cols_with_encoder = model.columns | selectattr('type', 'equalto', 'vector') | selectattr('has_encoder') | list %}
145
194
  {% if vector_cols_with_encoder %}
@@ -167,6 +216,16 @@ from {{ import_prefix }}_shared.utils.encoders import {{ 'aencode_pending' if is
167
216
  cambiado es quién contesta. Antes lo decidía aquí un bucle que no miraba las
168
217
  constraints compuestas, así que `libro` y `resena` se quedaban sin `upsert()`. #}
169
218
  {% set ns = namespace(all_pk_autoincrement=(model.natural_key | default([]) | length == 0)) %}
219
+ # Qué agregaciones admite cada columna, {columna: (tipo, operaciones)}. Sale del tipo del
220
+ # schema: el SQLAlchemy no distingue un enumerado ni una columna cifrada de un texto. Lo
221
+ # que no está aquí no se agrega, y pedirlo es un error con su motivo (`agg_field_error`).
222
+ _AGG_COLUMNS = {
223
+ {% for column in model.columns %}
224
+ '{{ column.name }}': ('{{ column.type }}{% if column.encrypt %}, cifrada{% elif column.hash %}, hasheada{% elif column.options is not none %}, enumerado{% endif %}', ({% for op in column.aggregations %}'{{ op }}'{% if not loop.last or loop.length == 1 %},{% endif %}{% if not loop.last %} {% endif %}{% endfor %})),
225
+ {% endfor %}
226
+ }
227
+
228
+
170
229
  class {{ model.name }}{{ prefix }}DAO:
171
230
  """
172
231
  DAO síncrono de acceso a datos para el modelo ``{{ model.name }}``.
@@ -229,9 +288,10 @@ class {{ model.name }}{{ prefix }}DAO:
229
288
  ``flush`` y ``_on_create_after`` después. Retorna el DTO del
230
289
  registro con los valores generados por la BD (IDs, timestamps...).
231
290
 
232
- create_many(records, *operative_fields, session) → int:
291
+ create_many(records, *operative_fields, session) → List[Read]:
233
292
  Inserción masiva con hooks de trigger por instancia.
234
- Retorna el número de registros creados.
293
+ Retorna los DTO de los registros creados, en el mismo orden, con
294
+ los valores que haya generado la BD — como ``create``.
235
295
 
236
296
  update(*pk_fields, updated_values, *operative_fields, session) → int:
237
297
  Actualiza un registro por PK ejecutando ``_on_update_before/after``.
@@ -280,13 +340,16 @@ class {{ model.name }}{{ prefix }}DAO:
280
340
  ``result.data`` → ``{"mean_<campo>": float | None}``.
281
341
 
282
342
  max(agg_fields, *filters, session) → AggregationResult:
283
- Valor máximo de campos numéricos o de fecha (ISO 8601).
343
+ Valor máximo de campos numéricos, de fecha (ISO 8601) o de texto.
284
344
  ``result.data`` → ``{"max_<campo>": float | str | None}``.
285
345
 
286
346
  min(agg_fields, *filters, session) → AggregationResult:
287
- Valor mínimo de campos numéricos o de fecha (ISO 8601).
347
+ Valor mínimo de campos numéricos, de fecha (ISO 8601) o de texto.
288
348
  ``result.data`` → ``{"min_<campo>": float | str | None}``.
289
349
 
350
+ Lo que el tipo de una columna no admite (``_AGG_COLUMNS``) es un error:
351
+ ``success=False``, sin consultar, y ``errors`` dice por qué.
352
+
290
353
  agg(request, *filters, verbose, session) → AggregationResult:
291
354
  Motor de agregaciones multi-operación. Acepta un ``AggRequest``
292
355
  con múltiples operaciones (sum, mean, max, min, count),