tai-sql 0.8.3__tar.gz → 0.8.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tai_sql-0.8.3 → tai_sql-0.8.4}/PKG-INFO +1 -1
- {tai_sql-0.8.3 → tai_sql-0.8.4}/pyproject.toml +1 -1
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/auth.py +2 -1
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/chatbot.py +3 -2
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/drivers/base.py +34 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/drivers/mysql.py +32 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/errors/collector.py +51 -1
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/errors/render.py +5 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/errors/types.py +3 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/README.md +4 -2
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/builder.py +25 -15
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/generator.py +3 -2
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/model.py +3 -2
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/__init__.py +3 -3
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/context.py +42 -8
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/domain/dao.py.jinja2 +1 -1
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/domain/dtos.py.jinja2 +1 -1
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/hierarchy.jinja2 +38 -20
- {tai_sql-0.8.3 → 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.3 → tai_sql-0.8.4}/tai_sql/generators/rules/__init__.py +5 -4
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/rules/templates/schema.md.jinja2 +6 -2
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/analysis/relation_analyzer.py +65 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/analysis/schema_analyzer.py +122 -16
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/columns.py +4 -1
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/relations.py +17 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/model/README.md +1 -1
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/model/__init__.py +2 -2
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/model/column.py +3 -1
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/model/entity.py +43 -13
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/model/relation.py +3 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/static/modelo.md +31 -1
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/schema.py +5 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/safety.py +26 -6
- tai_sql-0.8.3/tai_sql/generators/python_client/templates/shared/utils/hierarchy.py.jinja2 +0 -489
- {tai_sql-0.8.3 → tai_sql-0.8.4}/LICENSE +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/README.md +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_backup/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_backup/main.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/config.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/local.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/main.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_deploy/workflow.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_feed/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_feed/main.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_generate/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_generate/funcs.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_generate/main.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_hash_migrate/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_hash_migrate/main.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_info/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_info/main.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_info/status.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_init/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_init/main.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_init/model.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_install/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_install/main.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_ping/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_ping/main.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_pull/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_pull/funcs.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_pull/main.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_restore/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_restore/main.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_rules/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_rules/main.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/model.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/new.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/base.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/schemas/default.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/cmd_schema/set_default.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/prompts.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/connectivity.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/create.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/github/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/github/auth.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/github/client.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/commands/utils/github/models.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/errors.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/main.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/preamble.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/services/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/cli/services/backup.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/connection/README.md +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/connection/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/connection/provider.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/connection/recipe.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/connection/session.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/connection/sources.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/crypto.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/drivers/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/drivers/bigquery.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/drivers/capabilities.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/drivers/postgresql.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/drivers/sqlserver.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/errors/README.md +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/errors/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/feed/README.md +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/feed/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/feed/collect.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/feed/executor.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/feed/feeder.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/feed/plan.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/feed/presenter.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/feed/records.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/feed/transform.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/base.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/assets/README.md +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/assets/cytoscape.min.js +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/graphviz_render.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/html_render.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/layout.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/templates/er.html.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/diagram/themes.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/calculated.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/base.py.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/domain/__init__.py.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/domain/model.py.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/facade.py.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/aggregation.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/dataframe.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/filters.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/triggers.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/upsert.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/vector.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/write.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/session.py.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/__init__.py.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/aggregation.py.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/binary.py.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/context.py.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/utils/__init__.py.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/utils/encoders.py.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/utils/funcs.py.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/utils/rls.py.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/shared/vector.py.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/analyzer.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/renderer.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/scopes.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/steps.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/triggers/transformer.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/README.md +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/blueprint.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/build.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/defaults.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/naming.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/reader.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/templates/schema.py.jinja2 +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/types.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/introspect/writer.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/README.md +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/analysis/README.md +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/analysis/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/analysis/calculated.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/analysis/collector.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/analysis/column_analyzer.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/analysis/constraints.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/analysis/hints.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/README.md +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/annotations.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/base.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/calculated.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/constraints.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/datasource.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/descriptors.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/encoders.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/enumerations.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/table.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/trigger_api.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/triggers.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/declarative/view.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/mapping/README.md +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/mapping/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/mapping/sqlalchemy_mapper.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/model/constraints.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/model/enumeration.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/model/schema.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/model/trigger.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/naming.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/ordering.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/registry.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/orm/types.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/project/README.md +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/project/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/project/config.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/project/context.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/project/manager.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/project/paths.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/README.md +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/catalog.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/installer.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/meta.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/service.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/staleness.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/static/comandos.md +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/rules/static/errores.md +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/README.md +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/diff/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/diff/constraints.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/diff/indexes.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/diff/tables.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/diff/views.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/executor.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/ownership.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/plan/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/plan/planner.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/plan/statements.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/presenter.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/report.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/state/__init__.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/state/local.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/state/model.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/state/normalize.py +0 -0
- {tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/sync/state/remote.py +0 -0
- {tai_sql-0.8.3 → 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
|
|
|
@@ -172,8 +172,9 @@ class ChatbotSchema(SchemaTemplate):
|
|
|
172
172
|
# La llamada que creó el sub-agente que hizo esta: del mismo mensaje, por la clave compuesta
|
|
173
173
|
# (message_id, padre) → (message_id, call_id). Por el id del modelo y no por el de la fila: las
|
|
174
174
|
# filas se crean anidadas en el mensaje, y el id de la del padre todavía no existe. Con padre
|
|
175
|
-
# vacío (las del agente principal) la clave no se comprueba. Borrar una se lleva sus hijas
|
|
176
|
-
|
|
175
|
+
# vacío (las del agente principal) la clave no se comprueba. Borrar una se lleva sus hijas.
|
|
176
|
+
# `tree=True`: el árbol de llamadas de una respuesta, con consultas en árbol
|
|
177
|
+
llamada_padre: manytoone[ToolCall] = relation(fields=['message_id', 'padre'], references=['message_id', 'call_id'], backref='hijas', onDelete='cascade', tree=True)
|
|
177
178
|
hijas: onetomany[ToolCall]
|
|
178
179
|
|
|
179
180
|
|
|
@@ -1107,6 +1107,40 @@ class DatabaseDriver(ABC):
|
|
|
1107
1107
|
f"WHERE {no_nulos} AND r.{self.quote_identifier(target_columns[0])} IS NULL"
|
|
1108
1108
|
)
|
|
1109
1109
|
|
|
1110
|
+
def orphans_fix_statements(
|
|
1111
|
+
self, table_name: str, columns: Sequence[str],
|
|
1112
|
+
target_table: str, target_columns: Sequence[str],
|
|
1113
|
+
detach_columns: Sequence[str] = (), schema_name: str = None,
|
|
1114
|
+
) -> Dict[str, Optional[str]]:
|
|
1115
|
+
"""
|
|
1116
|
+
Las sentencias que propone `push` cuando una FK nueva tiene filas huérfanas.
|
|
1117
|
+
|
|
1118
|
+
- `delete`: elimina las filas cuyas columnas de FK no encuentran pareja.
|
|
1119
|
+
- `detach`: pone a NULL `detach_columns` en esas mismas filas, que es lo natural
|
|
1120
|
+
en una FK de la tabla consigo misma —la fila pasa a ser una raíz—. `None` si no
|
|
1121
|
+
se pide.
|
|
1122
|
+
|
|
1123
|
+
La pareja se busca por **todas** las columnas de la FK: con una compuesta, mirar
|
|
1124
|
+
solo la primera borraba filas que no son huérfanas. Es SQL estándar (`NOT
|
|
1125
|
+
EXISTS`); el motor que no acepte una subconsulta sobre la misma tabla que modifica
|
|
1126
|
+
—MySQL— lo sobrescribe.
|
|
1127
|
+
"""
|
|
1128
|
+
local = self.qualified_table(table_name, schema_name)
|
|
1129
|
+
destino = self.qualified_table(target_table, schema_name)
|
|
1130
|
+
union = ' AND '.join(
|
|
1131
|
+
f"r.{self.quote_identifier(t)} = {local}.{self.quote_identifier(c)}"
|
|
1132
|
+
for c, t in zip(columns, target_columns)
|
|
1133
|
+
)
|
|
1134
|
+
no_nulos = ' AND '.join(
|
|
1135
|
+
f"{self.quote_identifier(c)} IS NOT NULL" for c in columns
|
|
1136
|
+
)
|
|
1137
|
+
condicion = f"{no_nulos} AND NOT EXISTS (SELECT 1 FROM {destino} r WHERE {union})"
|
|
1138
|
+
detach = None
|
|
1139
|
+
if detach_columns:
|
|
1140
|
+
nulos = ', '.join(f"{self.quote_identifier(c)} = NULL" for c in detach_columns)
|
|
1141
|
+
detach = f"UPDATE {local} SET {nulos} WHERE {condicion}"
|
|
1142
|
+
return {'delete': f"DELETE FROM {local} WHERE {condicion}", 'detach': detach}
|
|
1143
|
+
|
|
1110
1144
|
# ── Tipos ────────────────────────────────────────────────────────────
|
|
1111
1145
|
|
|
1112
1146
|
def is_sequence_default(self, value: str) -> bool:
|
|
@@ -508,6 +508,38 @@ class MySQLDriver(DatabaseDriver):
|
|
|
508
508
|
|
|
509
509
|
# ── Escritura de datos ───────────────────────────────────────────────
|
|
510
510
|
|
|
511
|
+
def orphans_fix_statements(
|
|
512
|
+
self, table_name: str, columns: Sequence[str],
|
|
513
|
+
target_table: str, target_columns: Sequence[str],
|
|
514
|
+
detach_columns: Sequence[str] = (), schema_name: str = None,
|
|
515
|
+
) -> Dict[str, Optional[str]]:
|
|
516
|
+
"""
|
|
517
|
+
Con `LEFT JOIN` y no con `NOT EXISTS`.
|
|
518
|
+
|
|
519
|
+
MySQL no deja modificar una tabla con una subconsulta sobre ella misma (error
|
|
520
|
+
1093), que es justo el caso de una FK de la tabla consigo misma. El `UPDATE` y el
|
|
521
|
+
`DELETE` de varias tablas sí admiten unir la tabla consigo misma.
|
|
522
|
+
"""
|
|
523
|
+
local = self.qualified_table(table_name, schema_name)
|
|
524
|
+
destino = self.qualified_table(target_table, schema_name)
|
|
525
|
+
union = ' AND '.join(
|
|
526
|
+
f"r.{self.quote_identifier(t)} = l.{self.quote_identifier(c)}"
|
|
527
|
+
for c, t in zip(columns, target_columns)
|
|
528
|
+
)
|
|
529
|
+
no_nulos = ' AND '.join(
|
|
530
|
+
f"l.{self.quote_identifier(c)} IS NOT NULL" for c in columns
|
|
531
|
+
)
|
|
532
|
+
huerfana = f"r.{self.quote_identifier(target_columns[0])} IS NULL"
|
|
533
|
+
joined = f"{local} l LEFT JOIN {destino} r ON {union}"
|
|
534
|
+
detach = None
|
|
535
|
+
if detach_columns:
|
|
536
|
+
nulos = ', '.join(f"l.{self.quote_identifier(c)} = NULL" for c in detach_columns)
|
|
537
|
+
detach = f"UPDATE {joined} SET {nulos} WHERE {no_nulos} AND {huerfana}"
|
|
538
|
+
return {
|
|
539
|
+
'delete': f"DELETE l FROM {joined} WHERE {no_nulos} AND {huerfana}",
|
|
540
|
+
'detach': detach,
|
|
541
|
+
}
|
|
542
|
+
|
|
511
543
|
def upsert_statement(self, table, values, conflict_columns, update_columns,
|
|
512
544
|
returning=()):
|
|
513
545
|
"""
|
|
@@ -19,6 +19,11 @@ decir las cinco, y no obligar a arreglarlas de una en una, recompilando entre me
|
|
|
19
19
|
|
|
20
20
|
collector.abort_if_errors() # los presenta todos y sale
|
|
21
21
|
|
|
22
|
+
Recoge también **avisos**: lo que conviene cambiar sin que impida seguir, como una forma
|
|
23
|
+
del DSL que se va a retirar. Tienen la misma forma que un error —mensaje, solución,
|
|
24
|
+
ubicación, código— y van aparte porque no cuentan para `has_errors()`: un schema con
|
|
25
|
+
avisos se carga, se genera y se sincroniza igual.
|
|
26
|
+
|
|
22
27
|
Vive aquí y no en `orm/analysis/`, que es donde nació, porque acumular no es un concepto
|
|
23
28
|
del análisis: es la forma general de reportar varios problemas. La prueba es que
|
|
24
29
|
`SafetyChecker` había reimplementado exactamente esto por su cuenta —una lista de
|
|
@@ -32,7 +37,7 @@ from __future__ import annotations
|
|
|
32
37
|
|
|
33
38
|
from typing import Iterable, List, Optional
|
|
34
39
|
|
|
35
|
-
from .render import render_diagnostics
|
|
40
|
+
from .render import render_diagnostics, render_warning
|
|
36
41
|
from .types import Diagnostic, ErrorCode
|
|
37
42
|
|
|
38
43
|
|
|
@@ -50,6 +55,7 @@ class ErrorCollector:
|
|
|
50
55
|
self.context = context
|
|
51
56
|
self.enable_colors = enable_colors
|
|
52
57
|
self.errors: List[Diagnostic] = []
|
|
58
|
+
self.warnings: List[Diagnostic] = []
|
|
53
59
|
|
|
54
60
|
# ── Registro ─────────────────────────────────────────────────────────
|
|
55
61
|
|
|
@@ -81,6 +87,26 @@ class ErrorCollector:
|
|
|
81
87
|
)
|
|
82
88
|
)
|
|
83
89
|
|
|
90
|
+
def add_warning(
|
|
91
|
+
self,
|
|
92
|
+
message: str,
|
|
93
|
+
solution: str,
|
|
94
|
+
location: Optional[str] = None,
|
|
95
|
+
code: Optional[ErrorCode] = None,
|
|
96
|
+
) -> None:
|
|
97
|
+
"""
|
|
98
|
+
Anota un aviso: algo que conviene cambiar, pero que no impide seguir.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
message: Qué conviene saber.
|
|
102
|
+
solution: Qué cambiar. Obligatorio, igual que en un error.
|
|
103
|
+
location: Fichero, clase o campo donde ocurre.
|
|
104
|
+
code: Código para buscarlo en la documentación.
|
|
105
|
+
"""
|
|
106
|
+
self.warnings.append(
|
|
107
|
+
Diagnostic(message=message, solution=solution, location=location, code=code)
|
|
108
|
+
)
|
|
109
|
+
|
|
84
110
|
def extend(self, diagnostics: Iterable[Diagnostic]) -> None:
|
|
85
111
|
"""Anota varios problemas ya construidos."""
|
|
86
112
|
self.errors.extend(diagnostics)
|
|
@@ -88,6 +114,7 @@ class ErrorCollector:
|
|
|
88
114
|
def clear(self) -> None:
|
|
89
115
|
"""Olvida lo acumulado."""
|
|
90
116
|
self.errors.clear()
|
|
117
|
+
self.warnings.clear()
|
|
91
118
|
|
|
92
119
|
# ── Consulta ─────────────────────────────────────────────────────────
|
|
93
120
|
|
|
@@ -99,6 +126,14 @@ class ErrorCollector:
|
|
|
99
126
|
"""Copia de lo acumulado."""
|
|
100
127
|
return self.errors.copy()
|
|
101
128
|
|
|
129
|
+
def has_warnings(self) -> bool:
|
|
130
|
+
"""Si hay algún aviso acumulado."""
|
|
131
|
+
return bool(self.warnings)
|
|
132
|
+
|
|
133
|
+
def get_warnings(self) -> List[Diagnostic]:
|
|
134
|
+
"""Copia de los avisos acumulados."""
|
|
135
|
+
return self.warnings.copy()
|
|
136
|
+
|
|
102
137
|
# Nota: deliberadamente SIN __len__. Un colector se usa como objeto, no como
|
|
103
138
|
# contenedor, y darle longitud lo vuelve falsy cuando está vacío — que es el estado
|
|
104
139
|
# normal al empezar. El idioma `collector or ErrorCollector()` pasaba entonces a
|
|
@@ -112,6 +147,21 @@ class ErrorCollector:
|
|
|
112
147
|
self.errors, context=self.context, colors=self.enable_colors
|
|
113
148
|
)
|
|
114
149
|
|
|
150
|
+
def show_warnings(self) -> None:
|
|
151
|
+
"""Presenta los avisos acumulados, uno detrás de otro."""
|
|
152
|
+
for warning in self.warnings:
|
|
153
|
+
where = ' · '.join(
|
|
154
|
+
p for p in (
|
|
155
|
+
self.context, warning.location, warning.code and warning.code.value
|
|
156
|
+
) if p
|
|
157
|
+
)
|
|
158
|
+
render_warning(
|
|
159
|
+
warning.message,
|
|
160
|
+
hint=warning.solution,
|
|
161
|
+
location=where or None,
|
|
162
|
+
colors=self.enable_colors,
|
|
163
|
+
)
|
|
164
|
+
|
|
115
165
|
def abort_if_errors(self) -> None:
|
|
116
166
|
"""
|
|
117
167
|
Si hay problemas, los presenta todos y termina con código 1.
|
|
@@ -237,6 +237,11 @@ def render_warning(
|
|
|
237
237
|
stream: Dónde escribir. Por defecto stderr.
|
|
238
238
|
colors: `False` fuerza texto pelado.
|
|
239
239
|
"""
|
|
240
|
+
# Una solución de varias líneas —un trozo de código que pegar— sigue alineada bajo
|
|
241
|
+
# el 💡 en vez de volver al margen
|
|
242
|
+
if hint:
|
|
243
|
+
hint = hint.replace('\n', '\n ')
|
|
244
|
+
|
|
240
245
|
console = _console(stream) if colors else None
|
|
241
246
|
|
|
242
247
|
if console is None:
|
|
@@ -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')
|
|
@@ -155,8 +155,15 @@ class TemplateContextBuilder:
|
|
|
155
155
|
for cc in table.calculated_columns.values()
|
|
156
156
|
],
|
|
157
157
|
'has_calculated_columns': table.has_calculated_columns,
|
|
158
|
-
'
|
|
159
|
-
'
|
|
158
|
+
'has_hierarchy': table.has_hierarchy,
|
|
159
|
+
'hierarchy': self._hierarchy(table),
|
|
160
|
+
# Las dos de antes, con su forma: el contexto es contrato con tai-api, que lee
|
|
161
|
+
# `has_self_reference` para tipar `tree_level`, `ancestors` y `descendants`.
|
|
162
|
+
# Valen para cualquier jerarquía, también la de `tree=True`; `self_reference`
|
|
163
|
+
# solo tiene forma para una clave de una columna. Se retiran con
|
|
164
|
+
# `column(self_reference=...)`.
|
|
165
|
+
'has_self_reference': table.has_hierarchy,
|
|
166
|
+
'self_reference': self._legacy_self_reference(table),
|
|
160
167
|
}
|
|
161
168
|
|
|
162
169
|
def view(self, view: ViewModel) -> Dict[str, Any]:
|
|
@@ -493,14 +500,41 @@ class TemplateContextBuilder:
|
|
|
493
500
|
for event in TRIGGER_EVENTS
|
|
494
501
|
}
|
|
495
502
|
|
|
496
|
-
def
|
|
497
|
-
"""
|
|
498
|
-
|
|
503
|
+
def _hierarchy(self, table: TableModel) -> Optional[Dict[str, Any]]:
|
|
504
|
+
"""
|
|
505
|
+
La jerarquía de la tabla, o None si no declara ninguna.
|
|
506
|
+
|
|
507
|
+
`root_type` es el tipo de `tree_root` en el `find_many` generado: el valor de la
|
|
508
|
+
clave si es de una columna, y una tupla en el orden de `key_columns` o un dict por
|
|
509
|
+
nombre si es compuesta.
|
|
510
|
+
"""
|
|
511
|
+
hierarchy = table.hierarchy
|
|
512
|
+
if hierarchy is None:
|
|
513
|
+
return None
|
|
514
|
+
if hierarchy.is_composite:
|
|
515
|
+
root_type = (
|
|
516
|
+
f"Union[tuple[{', '.join(hierarchy.key_python_types)}], Dict[str, Any]]"
|
|
517
|
+
)
|
|
518
|
+
else:
|
|
519
|
+
root_type = hierarchy.key_python_types[0]
|
|
520
|
+
return {
|
|
521
|
+
'key_columns': list(hierarchy.key_columns),
|
|
522
|
+
'parent_columns': list(hierarchy.parent_columns),
|
|
523
|
+
'is_composite': hierarchy.is_composite,
|
|
524
|
+
'root_type': root_type,
|
|
525
|
+
'relation': hierarchy.relation,
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
@staticmethod
|
|
529
|
+
def _legacy_self_reference(table: TableModel) -> Optional[Dict[str, Any]]:
|
|
530
|
+
"""La clave `self_reference` de antes, para una jerarquía de una columna."""
|
|
531
|
+
hierarchy = table.hierarchy
|
|
532
|
+
if hierarchy is None or hierarchy.is_composite:
|
|
499
533
|
return None
|
|
500
534
|
return {
|
|
501
|
-
'column_name':
|
|
502
|
-
'references':
|
|
503
|
-
'pk_python_type':
|
|
535
|
+
'column_name': hierarchy.parent_columns[0],
|
|
536
|
+
'references': hierarchy.key_columns[0],
|
|
537
|
+
'pk_python_type': hierarchy.key_python_types[0],
|
|
504
538
|
}
|
|
505
539
|
|
|
506
540
|
def _constraints(self, column: ColumnModel) -> Dict[str, Any]:
|
{tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/domain/dtos.py.jinja2
RENAMED
|
@@ -138,7 +138,7 @@ class {{ model.name }}Read(PrettyModel):
|
|
|
138
138
|
{% endif %}
|
|
139
139
|
{% endfor %}
|
|
140
140
|
|
|
141
|
-
{% if model.
|
|
141
|
+
{% if model.has_hierarchy %}
|
|
142
142
|
tree_level: Optional[int] = Field(default=None, description="Nivel en el árbol jerárquico (0 = nodo raíz de la consulta)")
|
|
143
143
|
descendants: Optional[List[{{ model.name }}Read]] = Field(default=None, description="Subárbol de descendientes (poblado en tree_mode='descendants' o 'full_tree')")
|
|
144
144
|
ancestors: Optional[List[{{ model.name }}Read]] = Field(default=None, description="Cadena de ancestros (poblado en tree_mode='ancestors' o 'full_tree')")
|
{tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/hierarchy.jinja2
RENAMED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
{# ─────────────────────────────────────────────────────────────────────────────
|
|
2
2
|
Hierarchy macros — generates CTE-based recursive queries for self-referencing tables.
|
|
3
|
-
|
|
4
|
-
Activated when a model has `
|
|
5
|
-
self_reference='pk_column'
|
|
3
|
+
|
|
4
|
+
Activated when a model has `has_hierarchy == True`: a self relation declared with
|
|
5
|
+
`relation(..., tree=True)`, or the deprecated `column(self_reference='pk_column')`.
|
|
6
|
+
Both arrive with the same shape: `key_columns` (the node key) and `parent_columns`
|
|
7
|
+
(what points to the parent), one or several of each.
|
|
6
8
|
|
|
7
9
|
Delegates to HierarchyResolver (in _shared/utils/hierarchy.py) for all CTE
|
|
8
10
|
construction and tree assembly logic.
|
|
@@ -11,9 +13,9 @@
|
|
|
11
13
|
|
|
12
14
|
{# ── Parameters for find_many signature ─────────────────────────────────── #}
|
|
13
15
|
{% macro generate_hierarchy_parameters(model) %}
|
|
14
|
-
{% if model.
|
|
16
|
+
{% if model.has_hierarchy %}
|
|
15
17
|
tree_mode: Literal["flat", "descendants", "ancestors", "full_tree"] = "flat",
|
|
16
|
-
tree_root: Optional[{{ model.
|
|
18
|
+
tree_root: Optional[{{ model.hierarchy.root_type }}] = None,
|
|
17
19
|
tree_depth: Optional[int] = None,
|
|
18
20
|
{% endif %}
|
|
19
21
|
{% endmacro %}
|
|
@@ -21,34 +23,50 @@ tree_depth: Optional[int] = None,
|
|
|
21
23
|
|
|
22
24
|
{# ── Docstring args for hierarchy parameters ────────────────────────────── #}
|
|
23
25
|
{% macro generate_hierarchy_args(model) %}
|
|
24
|
-
{% if model.
|
|
26
|
+
{% if model.has_hierarchy %}
|
|
25
27
|
- tree_mode: Modo de consulta jerárquica. 'flat' (por defecto) desactiva la recursión.
|
|
26
|
-
'descendants' obtiene todos los
|
|
27
|
-
'ancestors' obtiene todos
|
|
28
|
+
'descendants' obtiene todos los descendientes del nodo raíz.
|
|
29
|
+
'ancestors' obtiene todos sus ancestros, hasta la raíz del árbol.
|
|
28
30
|
'full_tree' obtiene tanto descendientes como ancestros.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
{% if model.hierarchy.is_composite %}
|
|
32
|
+
- tree_root: Clave del nodo raíz para la consulta jerárquica: una tupla
|
|
33
|
+
({{ model.hierarchy.key_columns | join(', ') }}) o un dict con esas claves. Si es None y
|
|
34
|
+
tree_mode != 'flat', se usan los registros encontrados por los filtros como nodos raíz.
|
|
35
|
+
{% else %}
|
|
36
|
+
- tree_root: Clave ({{ model.hierarchy.key_columns[0] }}) del nodo raíz para la consulta
|
|
37
|
+
jerárquica. Si es None y tree_mode != 'flat', se usan los registros encontrados por
|
|
38
|
+
los filtros como nodos raíz.
|
|
39
|
+
{% endif %}
|
|
40
|
+
- tree_depth: Profundidad máxima de recursión. None = hasta 100 niveles:
|
|
41
|
+
al alcanzarlo se corta y queda un aviso en el log.
|
|
32
42
|
{% endif %}
|
|
33
43
|
{% endmacro %}
|
|
34
44
|
|
|
35
45
|
|
|
36
|
-
{# ── Root ID resolution block
|
|
46
|
+
{# ── Root ID resolution block: after the filters AND the rls, so a root is a row the user sees ── #}
|
|
37
47
|
{% macro generate_hierarchy_query(model) %}
|
|
38
|
-
{% if model.
|
|
39
|
-
{% set
|
|
48
|
+
{% if model.has_hierarchy %}
|
|
49
|
+
{% set hr = model.hierarchy %}
|
|
40
50
|
# ── Hierarchy: resolver nodos raíz ────────────────────────────────────
|
|
41
51
|
_hierarchy_mode = TreeMode(tree_mode)
|
|
52
|
+
_hierarchy_config = HierarchyConfig(
|
|
53
|
+
{{ model.name }},
|
|
54
|
+
key_columns=({% for c in hr.key_columns %}'{{ c }}', {% endfor %}),
|
|
55
|
+
parent_columns=({% for c in hr.parent_columns %}'{{ c }}', {% endfor %}),
|
|
56
|
+
)
|
|
42
57
|
_hierarchy_root_ids = None
|
|
43
58
|
|
|
44
59
|
if _hierarchy_mode != TreeMode.FLAT:
|
|
45
60
|
if tree_root is not None:
|
|
46
|
-
_hierarchy_root_ids = [tree_root]
|
|
61
|
+
_hierarchy_root_ids = [_hierarchy_config.root_key(tree_root)]
|
|
47
62
|
else:
|
|
48
|
-
#
|
|
63
|
+
# Las raíces son las filas de la consulta ya filtrada y acotada por rls
|
|
49
64
|
{{ def_keyword }} _get_root_ids(_sess):
|
|
50
65
|
_root_result = {{ await_keyword -}} _sess.execute(query)
|
|
51
|
-
return [
|
|
66
|
+
return [
|
|
67
|
+
_hierarchy_config.key(instance)
|
|
68
|
+
for instance in _root_result.scalars().all()
|
|
69
|
+
]
|
|
52
70
|
|
|
53
71
|
if session is not None:
|
|
54
72
|
_hierarchy_root_ids = {{ await_keyword -}} _get_root_ids(session)
|
|
@@ -64,8 +82,7 @@ tree_depth: Optional[int] = None,
|
|
|
64
82
|
|
|
65
83
|
{# ── Execute block using HierarchyResolver ──────────────────────────────── #}
|
|
66
84
|
{% macro generate_hierarchy_execute(model, max_depth) %}
|
|
67
|
-
{% if model.
|
|
68
|
-
{% set sr = model.self_reference %}
|
|
85
|
+
{% if model.has_hierarchy %}
|
|
69
86
|
{% set vector_cols_with_encoder = [] -%}
|
|
70
87
|
{% for vcol in model.columns | selectattr('type', 'equalto', 'vector') | list -%}
|
|
71
88
|
{% if vcol.get('has_encoder') -%}
|
|
@@ -74,7 +91,7 @@ tree_depth: Optional[int] = None,
|
|
|
74
91
|
{% endfor -%}
|
|
75
92
|
# ── Hierarchy resolver instance ───────────────────────────────────────
|
|
76
93
|
_hierarchy_resolver = HierarchyResolver(
|
|
77
|
-
config=
|
|
94
|
+
config=_hierarchy_config,
|
|
78
95
|
dto_factory=lambda instance, includes, max_depth: {{ model.name }}Read.from_instance(
|
|
79
96
|
instance,
|
|
80
97
|
{% if not model.is_view %}
|
|
@@ -82,6 +99,7 @@ tree_depth: Optional[int] = None,
|
|
|
82
99
|
max_depth=max_depth
|
|
83
100
|
{% endif %}
|
|
84
101
|
),
|
|
102
|
+
rls=rls,
|
|
85
103
|
)
|
|
86
104
|
|
|
87
105
|
{{ def_keyword }} execute_query(session: {{ session_type }}) -> List[{{ model.name }}Read]:
|
{tai_sql-0.8.3 → tai_sql-0.8.4}/tai_sql/generators/python_client/templates/macros/read.jinja2
RENAMED
|
@@ -207,8 +207,6 @@
|
|
|
207
207
|
|
|
208
208
|
{{ vector_macros.generate_vector_query(model) }}
|
|
209
209
|
|
|
210
|
-
{{ hierarchy_macros.generate_hierarchy_query(model) }}
|
|
211
|
-
|
|
212
210
|
# Log de parámetros aplicados
|
|
213
211
|
if filters:
|
|
214
212
|
_log_tx(session, f"[{{ schema_name }}] filters={filters}")
|
|
@@ -216,6 +214,8 @@
|
|
|
216
214
|
# Aplicar regla RLS (si existe)
|
|
217
215
|
if rls is not None:
|
|
218
216
|
query = RLSQueryApplicator.apply_rls(query, {{ model.name }}, rls)
|
|
217
|
+
|
|
218
|
+
{{ hierarchy_macros.generate_hierarchy_query(model) }}
|
|
219
219
|
|
|
220
220
|
# Aplicar opciones de carga optimizada
|
|
221
221
|
if includes:
|
|
@@ -269,7 +269,7 @@
|
|
|
269
269
|
{% endif %}
|
|
270
270
|
|
|
271
271
|
# Ejecutar query
|
|
272
|
-
{% if model.
|
|
272
|
+
{% if model.has_hierarchy %}
|
|
273
273
|
{% if vector_cols_with_encoder %}
|
|
274
274
|
{# El modo `flat` de la jerarquía es la consulta vectorial de siempre: se emite con otro
|
|
275
275
|
nombre y el bloque jerárquico la llama, en vez de repetirla a medias. #}
|