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.
- {tai_sql-0.8.2 → tai_sql-0.8.4}/PKG-INFO +1 -1
- {tai_sql-0.8.2 → tai_sql-0.8.4}/pyproject.toml +1 -1
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/auth.py +2 -1
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/chatbot.py +70 -14
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/drivers/base.py +34 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/drivers/mysql.py +32 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/errors/collector.py +51 -1
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/errors/render.py +5 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/errors/types.py +3 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/README.md +4 -2
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/builder.py +25 -15
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/generator.py +3 -2
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/model.py +3 -2
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/__init__.py +3 -3
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/context.py +42 -8
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/domain/dao.py.jinja2 +1 -1
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/domain/dtos.py.jinja2 +1 -1
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/hierarchy.jinja2 +38 -20
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/read.jinja2 +3 -3
- tai_sql-0.8.4/tai_sql/generators/python_client/templates/shared/utils/hierarchy.py.jinja2 +616 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/rules/__init__.py +5 -4
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/rules/templates/schema.md.jinja2 +6 -2
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/analysis/relation_analyzer.py +65 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/analysis/schema_analyzer.py +122 -16
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/columns.py +4 -1
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/relations.py +17 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/model/README.md +1 -1
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/model/__init__.py +2 -2
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/model/column.py +3 -1
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/model/entity.py +43 -13
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/model/relation.py +3 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/static/modelo.md +31 -1
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/schema.py +5 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/README.md +11 -6
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/plan/planner.py +19 -11
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/safety.py +26 -6
- tai_sql-0.8.2/tai_sql/generators/python_client/templates/shared/utils/hierarchy.py.jinja2 +0 -489
- {tai_sql-0.8.2 → tai_sql-0.8.4}/LICENSE +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/README.md +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_backup/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_backup/main.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/config.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/local.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/main.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/workflow.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_feed/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_feed/main.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_generate/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_generate/funcs.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_generate/main.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_hash_migrate/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_hash_migrate/main.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_info/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_info/main.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_info/status.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_init/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_init/main.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_init/model.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_install/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_install/main.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_ping/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_ping/main.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_pull/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_pull/funcs.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_pull/main.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_restore/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_restore/main.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_rules/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_rules/main.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/model.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/new.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/base.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/default.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/set_default.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/prompts.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/connectivity.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/create.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/github/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/github/auth.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/github/client.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/github/models.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/errors.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/main.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/preamble.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/services/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/cli/services/backup.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/connection/README.md +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/connection/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/connection/provider.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/connection/recipe.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/connection/session.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/connection/sources.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/crypto.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/drivers/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/drivers/bigquery.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/drivers/capabilities.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/drivers/postgresql.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/drivers/sqlserver.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/errors/README.md +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/errors/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/feed/README.md +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/feed/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/feed/collect.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/feed/executor.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/feed/feeder.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/feed/plan.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/feed/presenter.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/feed/records.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/feed/transform.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/base.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/assets/README.md +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/assets/cytoscape.min.js +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/graphviz_render.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/html_render.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/layout.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/templates/er.html.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/diagram/themes.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/calculated.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/base.py.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/domain/__init__.py.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/domain/model.py.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/facade.py.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/aggregation.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/dataframe.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/filters.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/triggers.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/upsert.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/vector.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/write.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/session.py.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/__init__.py.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/aggregation.py.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/binary.py.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/context.py.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/utils/__init__.py.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/utils/encoders.py.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/utils/funcs.py.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/utils/rls.py.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/vector.py.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/analyzer.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/renderer.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/scopes.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/steps.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/transformer.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/README.md +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/blueprint.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/build.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/defaults.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/naming.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/reader.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/templates/schema.py.jinja2 +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/types.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/introspect/writer.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/README.md +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/analysis/README.md +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/analysis/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/analysis/calculated.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/analysis/collector.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/analysis/column_analyzer.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/analysis/constraints.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/analysis/hints.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/README.md +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/annotations.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/base.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/calculated.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/constraints.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/datasource.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/descriptors.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/encoders.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/enumerations.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/table.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/trigger_api.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/triggers.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/declarative/view.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/mapping/README.md +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/mapping/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/mapping/sqlalchemy_mapper.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/model/constraints.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/model/enumeration.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/model/schema.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/model/trigger.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/naming.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/ordering.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/registry.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/orm/types.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/project/README.md +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/project/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/project/config.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/project/context.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/project/manager.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/project/paths.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/README.md +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/catalog.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/installer.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/meta.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/service.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/staleness.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/static/comandos.md +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/rules/static/errores.md +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/diff/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/diff/constraints.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/diff/indexes.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/diff/tables.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/diff/views.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/executor.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/ownership.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/plan/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/plan/statements.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/presenter.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/report.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/state/__init__.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/state/local.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/state/model.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/state/normalize.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/state/remote.py +0 -0
- {tai_sql-0.8.2 → tai_sql-0.8.4}/tai_sql/sync/synchronizer.py +0 -0
|
@@ -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
|
-
|
|
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,
|
|
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(
|
|
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
|
|
720
|
-
-- y lo que se apuntó con ellas.
|
|
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.
|
|
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:
|
|
@@ -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
|
|
41
|
-
|
|
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
|
|
16
|
-
|
|
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
|
-
|
|
99
|
-
entity.
|
|
100
|
-
if not entity.is_view and entity.
|
|
101
|
-
else
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
el
|
|
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.
|
|
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.
|
|
267
|
-
target_name=table.
|
|
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
|
|
58
|
-
`column(self_reference=…)` y las
|
|
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
|
|
169
|
-
#: que no produce `RelationModel` y
|
|
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
|
|
425
|
+
# Pre-compute whether any model declares a hierarchy
|
|
426
426
|
has_hierarchy = any(
|
|
427
|
-
m.get('
|
|
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
|
|
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')
|