tai-sql 0.7.18__tar.gz → 0.7.19__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.7.18 → tai_sql-0.7.19}/PKG-INFO +1 -1
- {tai_sql-0.7.18 → tai_sql-0.7.19}/pyproject.toml +1 -1
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_generate/funcs.py +16 -1
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_schema/schemas/chatbot.py +5 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/triggers/__init__.py +3 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/triggers/analyzer.py +82 -22
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/triggers/renderer.py +67 -10
- tai_sql-0.7.19/tai_sql/generators/python_client/triggers/scopes.py +164 -0
- tai_sql-0.7.19/tai_sql/generators/python_client/triggers/transformer.py +724 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/trigger_api.py +4 -2
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/triggers.py +32 -9
- tai_sql-0.7.18/tai_sql/generators/python_client/triggers/transformer.py +0 -691
- {tai_sql-0.7.18 → tai_sql-0.7.19}/LICENSE +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/README.md +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_backup/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_backup/main.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_deploy/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_deploy/config.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_deploy/local.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_deploy/main.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_deploy/workflow.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_feed/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_feed/main.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_generate/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_generate/main.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_hash_migrate/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_hash_migrate/main.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_info/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_info/main.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_info/status.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_init/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_init/main.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_init/model.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_install/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_install/main.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_ping/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_ping/main.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_pull/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_pull/funcs.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_pull/main.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_restore/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_restore/main.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_rules/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_rules/main.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_schema/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_schema/model.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_schema/new.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_schema/schemas/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_schema/schemas/auth.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_schema/schemas/base.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_schema/schemas/default.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/cmd_schema/set_default.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/prompts.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/utils/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/utils/connectivity.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/utils/create.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/utils/github/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/utils/github/auth.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/utils/github/client.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/commands/utils/github/models.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/errors.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/main.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/preamble.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/services/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/cli/services/backup.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/connection/README.md +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/connection/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/connection/provider.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/connection/recipe.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/connection/session.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/connection/sources.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/crypto.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/drivers/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/drivers/base.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/drivers/bigquery.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/drivers/capabilities.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/drivers/mysql.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/drivers/postgresql.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/drivers/sqlserver.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/errors/README.md +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/errors/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/errors/collector.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/errors/render.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/errors/types.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/feed/README.md +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/feed/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/feed/collect.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/feed/executor.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/feed/feeder.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/feed/plan.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/feed/presenter.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/feed/records.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/feed/transform.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/base.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/README.md +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/assets/README.md +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/assets/cytoscape.min.js +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/builder.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/generator.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/graphviz_render.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/html_render.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/layout.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/model.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/templates/er.html.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/diagram/themes.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/calculated.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/context.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/base.py.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/domain/__init__.py.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/domain/dao.py.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/domain/dtos.py.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/domain/model.py.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/facade.py.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/macros/aggregation.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/macros/dataframe.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/macros/filters.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/macros/hierarchy.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/macros/read.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/macros/triggers.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/macros/upsert.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/macros/vector.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/macros/write.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/session.py.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/shared/__init__.py.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/shared/aggregation.py.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/shared/context.py.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/shared/utils/__init__.py.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/shared/utils/encoders.py.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/shared/utils/funcs.py.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/shared/utils/hierarchy.py.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/shared/utils/rls.py.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/templates/shared/vector.py.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/python_client/triggers/steps.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/rules/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/generators/rules/templates/schema.md.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/README.md +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/blueprint.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/build.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/defaults.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/naming.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/reader.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/templates/schema.py.jinja2 +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/types.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/introspect/writer.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/README.md +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/analysis/README.md +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/analysis/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/analysis/calculated.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/analysis/collector.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/analysis/column_analyzer.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/analysis/constraints.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/analysis/hints.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/analysis/relation_analyzer.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/analysis/schema_analyzer.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/README.md +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/annotations.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/base.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/calculated.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/columns.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/constraints.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/datasource.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/descriptors.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/encoders.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/enumerations.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/relations.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/table.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/declarative/view.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/mapping/README.md +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/mapping/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/mapping/sqlalchemy_mapper.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/model/README.md +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/model/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/model/column.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/model/constraints.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/model/entity.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/model/enumeration.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/model/relation.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/model/schema.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/model/trigger.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/naming.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/ordering.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/registry.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/orm/types.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/project/README.md +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/project/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/project/config.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/project/context.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/project/manager.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/project/paths.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/README.md +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/catalog.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/installer.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/meta.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/service.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/staleness.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/static/comandos.md +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/static/errores.md +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/rules/static/modelo.md +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/schema.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/README.md +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/diff/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/diff/constraints.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/diff/indexes.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/diff/tables.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/diff/views.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/executor.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/ownership.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/plan/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/plan/planner.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/plan/statements.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/presenter.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/report.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/safety.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/state/__init__.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/state/local.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/state/model.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/state/normalize.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/state/remote.py +0 -0
- {tai_sql-0.7.18 → tai_sql-0.7.19}/tai_sql/sync/synchronizer.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import click
|
|
2
2
|
|
|
3
3
|
from tai_sql import pm
|
|
4
|
-
from tai_sql.errors import ErrorCode, GenerationError, render_warning
|
|
4
|
+
from tai_sql.errors import ErrorCode, GenerationError, TaiSqlError, render_warning
|
|
5
5
|
from tai_sql.rules import warn_if_outdated
|
|
6
6
|
|
|
7
7
|
from ...preamble import load_schema
|
|
@@ -49,6 +49,21 @@ def run_generate(schema_name: str = None):
|
|
|
49
49
|
)
|
|
50
50
|
generator.generate()
|
|
51
51
|
|
|
52
|
+
except TaiSqlError as e:
|
|
53
|
+
# El generador ya sabe qué ha fallado y cómo se arregla —un trigger que no se
|
|
54
|
+
# puede transpilar dice cuál y por qué—: su solución vale más que la genérica.
|
|
55
|
+
# Se conserva el tipo y se añade el schema y el generador delante del sitio
|
|
56
|
+
# que traía (el trigger), en vez de pisarlo.
|
|
57
|
+
raise type(e)(
|
|
58
|
+
message=f"{generator_name} falló al generar: {e.message}",
|
|
59
|
+
solution=e.solution,
|
|
60
|
+
location=' · '.join(filter(None, (
|
|
61
|
+
f"Schema '{schema_name}'", generator_name, e.diagnostic.location,
|
|
62
|
+
))),
|
|
63
|
+
code=e.diagnostic.code,
|
|
64
|
+
**e.diagnostic.context,
|
|
65
|
+
) from e
|
|
66
|
+
|
|
52
67
|
except Exception as e:
|
|
53
68
|
raise GenerationError(
|
|
54
69
|
message=f"{generator_name} falló al generar: {e}",
|
|
@@ -62,6 +62,11 @@ class ChatbotSchema(SchemaTemplate):
|
|
|
62
62
|
|
|
63
63
|
visible: col[bool] = column(default=True, index=True, description='Si no, la conversación no se lista')
|
|
64
64
|
last_message_timestamp: col[datetime | None] = column(description='Timestamp del último mensaje del chat')
|
|
65
|
+
# Por alias y no como FK a modelo.id, a propósito: el alias es con lo que el catálogo y
|
|
66
|
+
# el front piden un modelo, y uno desactivado no tiene por qué romper la conversación
|
|
67
|
+
# (cae al de por defecto). Opcional y sin default: un push la añade a una base de
|
|
68
|
+
# datos existente sin tocar sus filas
|
|
69
|
+
modelo: col[str | None] = column(description='Alias del modelo con el que responde esta conversación; sin él, el de por defecto')
|
|
65
70
|
|
|
66
71
|
messages: onetomany[Mensaje] # Relación one-to-many con la tabla Mensaje
|
|
67
72
|
token_usage: onetomany[TokenUsage] # El consumo de la conversación, tenga mensaje o no
|
|
@@ -12,6 +12,9 @@ ninguna capa de despacho — el coste es cero.
|
|
|
12
12
|
(metadato puro) → transformer → AST reescrito
|
|
13
13
|
→ renderer → dict de contexto para Jinja2
|
|
14
14
|
|
|
15
|
+
`scopes` lo usan los dos primeros: dice qué `t` es el parámetro y cuál es otra variable
|
|
16
|
+
con el mismo nombre en un ámbito interno.
|
|
17
|
+
|
|
15
18
|
Este código vivía en `orm/triggers.py` (1.300 de sus 2.092 líneas). Está aquí porque
|
|
16
19
|
conoce los nombres de los artefactos generados (`{Model}SyncDAO`, `{Model}Create`…),
|
|
17
20
|
que son conocimiento del generador, no de la definición del schema.
|
|
@@ -15,9 +15,10 @@ NO le corresponde: validar que el trigger sea legal (eso ocurre en tiempo de imp
|
|
|
15
15
|
schema, en `orm.triggers`), ni conocer la sintaxis del DSL.
|
|
16
16
|
"""
|
|
17
17
|
from __future__ import annotations
|
|
18
|
-
from typing import List, Optional
|
|
18
|
+
from typing import Dict, List, Optional, Set
|
|
19
19
|
import ast
|
|
20
20
|
|
|
21
|
+
from . import scopes
|
|
21
22
|
from .steps import TriggerStep
|
|
22
23
|
|
|
23
24
|
|
|
@@ -31,14 +32,34 @@ class TriggerAnalyzer(ast.NodeVisitor):
|
|
|
31
32
|
- Operaciones de mutación de datos
|
|
32
33
|
"""
|
|
33
34
|
|
|
34
|
-
def __init__(self, source_lines: List[str]):
|
|
35
|
+
def __init__(self, source_lines: List[str], trigger_var: str = 't'):
|
|
35
36
|
"""
|
|
36
37
|
Args:
|
|
37
38
|
source_lines: Líneas del código fuente para obtener el texto original
|
|
39
|
+
trigger_var: Nombre del parámetro TriggerAPI del trigger. Con uno fijo a `'t'`,
|
|
40
|
+
un trigger que lo llamara de otra forma no registraba ninguna operación, y
|
|
41
|
+
el DAO generado no importaba los DAOs de los modelos que toca.
|
|
38
42
|
"""
|
|
39
43
|
self.steps: List[TriggerStep] = []
|
|
40
44
|
self.source_lines = source_lines
|
|
41
|
-
self.trigger_var =
|
|
45
|
+
self.trigger_var = trigger_var
|
|
46
|
+
# Llamadas ya registradas por `visit_Assign`, para que `visit_Call` no las repita
|
|
47
|
+
self._registered: Set[int] = set()
|
|
48
|
+
# Qué `Name` son de verdad el parámetro: los calcula la primera llamada a `visit`
|
|
49
|
+
self._refs: Optional[Dict[ast.Name, Optional[str]]] = None
|
|
50
|
+
|
|
51
|
+
def visit(self, node: ast.AST):
|
|
52
|
+
"""
|
|
53
|
+
Recorre `node`, resolviendo antes los ámbitos si es la primera vez.
|
|
54
|
+
|
|
55
|
+
`node` es la función del trigger (lo que pasa `analyze_steps`) o un bloque de su
|
|
56
|
+
cuerpo. De la función cuentan el cuerpo y la `lambda` de `when=`, cada uno con su
|
|
57
|
+
parámetro; un `t` re-ligado en un ámbito interno (`[x for t in ...]`) no es una
|
|
58
|
+
operación, aunque se llame `t.count(...)`.
|
|
59
|
+
"""
|
|
60
|
+
if self._refs is None:
|
|
61
|
+
self._refs = _trigger_refs(node, self.trigger_var)
|
|
62
|
+
return super().visit(node)
|
|
42
63
|
|
|
43
64
|
def visit_Assign(self, node: ast.Assign) -> None:
|
|
44
65
|
"""Visita asignaciones para detectar mutaciones de datos"""
|
|
@@ -67,10 +88,7 @@ class TriggerAnalyzer(ast.NodeVisitor):
|
|
|
67
88
|
if self._is_crud_method(node.value.func):
|
|
68
89
|
crud_operation = node.value.func.attr
|
|
69
90
|
|
|
70
|
-
|
|
71
|
-
model_name = None
|
|
72
|
-
if len(node.value.args) > 0 and isinstance(node.value.args[0], ast.Name):
|
|
73
|
-
model_name = node.value.args[0].id
|
|
91
|
+
model_name = _model_name(node.value)
|
|
74
92
|
|
|
75
93
|
# Los demás argumentos
|
|
76
94
|
args = [self._ast_to_repr(arg) for arg in node.value.args[1:]]
|
|
@@ -92,13 +110,21 @@ class TriggerAnalyzer(ast.NodeVisitor):
|
|
|
92
110
|
ast_node=node,
|
|
93
111
|
trigger_refs=trigger_refs
|
|
94
112
|
))
|
|
113
|
+
self._registered.add(id(node.value))
|
|
95
114
|
|
|
96
115
|
self.generic_visit(node)
|
|
97
116
|
|
|
98
|
-
def
|
|
99
|
-
"""
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
def visit_Call(self, node: ast.Call) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Visita cualquier llamada a un método del TriggerAPI, esté donde esté.
|
|
120
|
+
|
|
121
|
+
Antes solo se miraban las sentencias sueltas y las asignaciones, así que una
|
|
122
|
+
operación en el iterable de un `for`, en un argumento o en una condición no se
|
|
123
|
+
registraba, y el DAO generado no importaba el DAO del modelo que tocaba. Las de
|
|
124
|
+
una asignación las registra `visit_Assign`, con la variable de destino.
|
|
125
|
+
"""
|
|
126
|
+
if id(node) not in self._registered:
|
|
127
|
+
self._visit_call(node)
|
|
102
128
|
self.generic_visit(node)
|
|
103
129
|
|
|
104
130
|
def _visit_call(self, node: ast.Call) -> None:
|
|
@@ -137,10 +163,7 @@ class TriggerAnalyzer(ast.NodeVisitor):
|
|
|
137
163
|
elif self._is_crud_method(node.func):
|
|
138
164
|
crud_operation = node.func.attr # find, create, count, etc.
|
|
139
165
|
|
|
140
|
-
|
|
141
|
-
model_name = None
|
|
142
|
-
if len(node.args) > 0 and isinstance(node.args[0], ast.Name):
|
|
143
|
-
model_name = node.args[0].id
|
|
166
|
+
model_name = _model_name(node)
|
|
144
167
|
|
|
145
168
|
# Los demás argumentos son parámetros de la operación
|
|
146
169
|
args = [self._ast_to_repr(arg) for arg in node.args[1:]] # Saltar el modelo
|
|
@@ -158,18 +181,24 @@ class TriggerAnalyzer(ast.NodeVisitor):
|
|
|
158
181
|
trigger_refs=trigger_refs
|
|
159
182
|
))
|
|
160
183
|
|
|
184
|
+
def _is_trigger_ref(self, node: ast.AST) -> bool:
|
|
185
|
+
"""Si `node` es el parámetro del trigger, y no otra variable con su nombre."""
|
|
186
|
+
if not isinstance(node, ast.Name):
|
|
187
|
+
return False
|
|
188
|
+
if self._refs is None:
|
|
189
|
+
return node.id == self.trigger_var
|
|
190
|
+
return node in self._refs
|
|
191
|
+
|
|
161
192
|
def _is_trigger_attr(self, node: ast.AST, attr_name: str) -> bool:
|
|
162
193
|
"""Verifica si un nodo es un atributo del trigger (ej: t.data, t.old)"""
|
|
163
194
|
return (isinstance(node, ast.Attribute) and
|
|
164
|
-
|
|
165
|
-
node.value.id == self.trigger_var and
|
|
195
|
+
self._is_trigger_ref(node.value) and
|
|
166
196
|
node.attr == attr_name)
|
|
167
197
|
|
|
168
198
|
def _is_trigger_method(self, node: ast.AST, method_name: str) -> bool:
|
|
169
199
|
"""Verifica si un nodo es una llamada a método del trigger (ej: t.log, t.abort)"""
|
|
170
200
|
return (isinstance(node, ast.Attribute) and
|
|
171
|
-
|
|
172
|
-
node.value.id == self.trigger_var and
|
|
201
|
+
self._is_trigger_ref(node.value) and
|
|
173
202
|
node.attr == method_name)
|
|
174
203
|
|
|
175
204
|
def _is_crud_method(self, node: ast.AST) -> bool:
|
|
@@ -184,8 +213,7 @@ class TriggerAnalyzer(ast.NodeVisitor):
|
|
|
184
213
|
}
|
|
185
214
|
|
|
186
215
|
return (isinstance(node, ast.Attribute) and
|
|
187
|
-
|
|
188
|
-
node.value.id == self.trigger_var and
|
|
216
|
+
self._is_trigger_ref(node.value) and
|
|
189
217
|
node.attr in crud_methods)
|
|
190
218
|
|
|
191
219
|
def _extract_subscript_key(self, node: ast.Subscript) -> Optional[str]:
|
|
@@ -216,10 +244,11 @@ class TriggerAnalyzer(ast.NodeVisitor):
|
|
|
216
244
|
Lista de referencias encontradas (ej: ['t.new', 't.old'])
|
|
217
245
|
"""
|
|
218
246
|
references = []
|
|
247
|
+
is_trigger_ref = self._is_trigger_ref
|
|
219
248
|
|
|
220
249
|
class ReferenceExtractor(ast.NodeVisitor):
|
|
221
250
|
def visit_Attribute(self, node):
|
|
222
|
-
if
|
|
251
|
+
if is_trigger_ref(node.value):
|
|
223
252
|
if node.attr in ['old', 'new']:
|
|
224
253
|
references.append(f't.{node.attr}')
|
|
225
254
|
self.generic_visit(node)
|
|
@@ -234,3 +263,34 @@ class TriggerAnalyzer(ast.NodeVisitor):
|
|
|
234
263
|
# lineno es 1-indexed
|
|
235
264
|
return self.source_lines[node.lineno - 1].strip()
|
|
236
265
|
return None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _model_name(call: ast.Call) -> Optional[str]:
|
|
269
|
+
"""El modelo de `t.<op>(Modelo, ...)`: el primer argumento, o `model=` como en `TriggerAPI`."""
|
|
270
|
+
if call.args:
|
|
271
|
+
return call.args[0].id if isinstance(call.args[0], ast.Name) else None
|
|
272
|
+
for kw in call.keywords:
|
|
273
|
+
if kw.arg == 'model' and isinstance(kw.value, ast.Name):
|
|
274
|
+
return kw.value.id
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _trigger_refs(node: ast.AST, trigger_var: str) -> Dict[ast.Name, Optional[str]]:
|
|
279
|
+
"""
|
|
280
|
+
Las referencias al parámetro dentro de `node`, que es la función del trigger o un
|
|
281
|
+
bloque de su cuerpo.
|
|
282
|
+
|
|
283
|
+
De la función no se mira la firma —ahí `t` es el argumento, no un uso—: su cuerpo
|
|
284
|
+
con el parámetro del trigger, y la `lambda` de `when=` con el suyo, que puede
|
|
285
|
+
llamarse distinto.
|
|
286
|
+
"""
|
|
287
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
288
|
+
return scopes.param_refs([node], trigger_var)
|
|
289
|
+
refs = scopes.param_refs(node.body, trigger_var)
|
|
290
|
+
for decorator in node.decorator_list:
|
|
291
|
+
if not isinstance(decorator, ast.Call):
|
|
292
|
+
continue
|
|
293
|
+
for kw in decorator.keywords:
|
|
294
|
+
if kw.arg == 'when' and isinstance(kw.value, ast.Lambda) and kw.value.args.args:
|
|
295
|
+
refs.update(scopes.param_refs([kw.value.body], kw.value.args.args[0].arg))
|
|
296
|
+
return refs
|
|
@@ -20,6 +20,9 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
|
|
20
20
|
import ast
|
|
21
21
|
import copy
|
|
22
22
|
import re
|
|
23
|
+
import textwrap
|
|
24
|
+
|
|
25
|
+
from tai_sql.errors import GenerationError
|
|
23
26
|
|
|
24
27
|
from .analyzer import TriggerAnalyzer
|
|
25
28
|
from .transformer import TriggerCodeTransformer
|
|
@@ -57,7 +60,7 @@ def analyze_steps(trigger: Trigger) -> list:
|
|
|
57
60
|
if not trigger.ast_tree or not trigger.source_code:
|
|
58
61
|
return []
|
|
59
62
|
|
|
60
|
-
analyzer = TriggerAnalyzer(trigger.source_code.split('\n'))
|
|
63
|
+
analyzer = TriggerAnalyzer(trigger.source_code.split('\n'), _trigger_param(trigger))
|
|
61
64
|
analyzer.visit(trigger.ast_tree)
|
|
62
65
|
return analyzer.steps
|
|
63
66
|
|
|
@@ -71,6 +74,54 @@ def _trigger_param(trigger: Trigger) -> str:
|
|
|
71
74
|
return 't'
|
|
72
75
|
|
|
73
76
|
|
|
77
|
+
def _when_param(trigger: Trigger) -> str:
|
|
78
|
+
"""
|
|
79
|
+
Nombre del parámetro de la `lambda` de `when=`.
|
|
80
|
+
|
|
81
|
+
No tiene por qué coincidir con el de la función: `when=lambda x: x.new.precio > 0`
|
|
82
|
+
sobre un `def f(self, t)` es válido. Con el de la función, el `x` se quedaba sin
|
|
83
|
+
transpilar y la condición fallaba al ejecutar con `NameError`.
|
|
84
|
+
"""
|
|
85
|
+
if trigger.ast_tree:
|
|
86
|
+
for decorator in trigger.ast_tree.decorator_list:
|
|
87
|
+
if not isinstance(decorator, ast.Call):
|
|
88
|
+
continue
|
|
89
|
+
for kw in decorator.keywords:
|
|
90
|
+
if kw.arg == 'when' and isinstance(kw.value, ast.Lambda) and kw.value.args.args:
|
|
91
|
+
return kw.value.args.args[0].arg
|
|
92
|
+
return _trigger_param(trigger)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _check_compiles(trigger: Trigger, code: str, mode: str, what: str) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Compila el código transpilado dentro de una función como la del hook que lo recibe.
|
|
98
|
+
|
|
99
|
+
Es la red de seguridad del transformer: lo que él no rechace y no sea Python válido
|
|
100
|
+
—un `await` donde no cabe, un argumento repetido— sale aquí, con el nombre del
|
|
101
|
+
trigger, y no como un DAO generado que no se puede importar.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
GenerationError: Si el código no compila.
|
|
105
|
+
"""
|
|
106
|
+
header = 'async def _hook():' if mode == 'async' else 'def _hook():'
|
|
107
|
+
try:
|
|
108
|
+
compile(f"{header}\n{textwrap.indent(code, ' ')}\n", f'<trigger {trigger.name}>', 'exec')
|
|
109
|
+
except SyntaxError as e:
|
|
110
|
+
line = (e.text or '').strip()
|
|
111
|
+
raise GenerationError(
|
|
112
|
+
message=(
|
|
113
|
+
f"Trigger '{trigger.name}': su {what} transpilado para el cliente {mode} no "
|
|
114
|
+
f"compila: {e.msg}" + (f" (en `{line}`)" if line else "")
|
|
115
|
+
),
|
|
116
|
+
solution=(
|
|
117
|
+
"Reescribe esa expresión de forma más sencilla —con una variable intermedia "
|
|
118
|
+
"o una comprensión de lista— y, si el trigger es Python válido, repórtalo: "
|
|
119
|
+
"es un fallo de la transpilación de tai-sql."
|
|
120
|
+
),
|
|
121
|
+
location=trigger.name,
|
|
122
|
+
) from e
|
|
123
|
+
|
|
124
|
+
|
|
74
125
|
def render_body(
|
|
75
126
|
trigger: Trigger,
|
|
76
127
|
mode: str = 'sync',
|
|
@@ -115,11 +166,13 @@ def render_body(
|
|
|
115
166
|
_trigger_param(trigger), trigger.event, mode,
|
|
116
167
|
operative_str_fields if operative_str_fields is not None else [],
|
|
117
168
|
tablename,
|
|
169
|
+
trigger_name=trigger.name,
|
|
118
170
|
)
|
|
119
|
-
transformed = transformer.
|
|
120
|
-
ast.fix_missing_locations(transformed)
|
|
171
|
+
transformed = transformer.transpile(module)
|
|
121
172
|
|
|
122
|
-
|
|
173
|
+
code = '\n'.join(ast.unparse(stmt) for stmt in transformed.body)
|
|
174
|
+
_check_compiles(trigger, code, mode, 'cuerpo')
|
|
175
|
+
return code
|
|
123
176
|
|
|
124
177
|
|
|
125
178
|
def render_when(
|
|
@@ -136,20 +189,24 @@ def render_when(
|
|
|
136
189
|
if not trigger.when_source:
|
|
137
190
|
return None
|
|
138
191
|
|
|
139
|
-
param =
|
|
192
|
+
param = _when_param(trigger)
|
|
140
193
|
when = trigger.when_source.strip()
|
|
141
194
|
|
|
142
195
|
try:
|
|
143
196
|
tree = ast.parse(when, mode='eval')
|
|
144
|
-
transformer = TriggerCodeTransformer(param, trigger.event, mode, tablename=tablename)
|
|
145
|
-
transformed = transformer.visit(tree)
|
|
146
|
-
ast.fix_missing_locations(transformed)
|
|
147
|
-
return ast.unparse(transformed.body)
|
|
148
197
|
except SyntaxError:
|
|
149
198
|
# Fallback: la fuente de la lambda no siempre se recupera como expresión
|
|
150
199
|
# parseable (troceo de strings en el decorador). Quitar el prefijo `t.` deja
|
|
151
200
|
# algo utilizable en la mayoría de casos.
|
|
152
|
-
|
|
201
|
+
code = re.sub(rf'\b{re.escape(param)}\.', '', when)
|
|
202
|
+
else:
|
|
203
|
+
transformer = TriggerCodeTransformer(
|
|
204
|
+
param, trigger.event, mode, tablename=tablename, trigger_name=trigger.name,
|
|
205
|
+
)
|
|
206
|
+
code = ast.unparse(transformer.transpile(tree).body)
|
|
207
|
+
|
|
208
|
+
_check_compiles(trigger, f"if {code}:\n pass", mode, 'condición `when=`')
|
|
209
|
+
return code
|
|
153
210
|
|
|
154
211
|
|
|
155
212
|
def render_trigger(
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Ámbitos del cuerpo de un trigger: qué apariciones de un nombre son el parámetro.
|
|
4
|
+
|
|
5
|
+
CAPA: generador (`generators/python_client/triggers/`).
|
|
6
|
+
|
|
7
|
+
El cuerpo de un trigger puede reutilizar el nombre del parámetro (`t`) en un ámbito
|
|
8
|
+
interno —`[t.strip() for t in etiquetas]`, `lambda t: t.activo`, una función anidada
|
|
9
|
+
con un argumento `t`—, y ahí `t` ya no es `TriggerAPI`. Sin distinguirlo, el
|
|
10
|
+
transformer reescribía o rechazaba código que no le corresponde. Este módulo resuelve
|
|
11
|
+
el ámbito una vez y devuelve qué nodos `Name` son de verdad el parámetro.
|
|
12
|
+
|
|
13
|
+
De paso dice, de cada uno, si cae en un sitio donde el código generado no puede
|
|
14
|
+
esperar a una corrutina: dentro de una `lambda`, de una expresión generadora o de una
|
|
15
|
+
función anidada que no es `async`. En el DAO async, una operación ahí lleva un `await`
|
|
16
|
+
que no compila (`lambda`, `def`) o que convierte la expresión generadora en un
|
|
17
|
+
generador asíncrono que nadie sabe recorrer.
|
|
18
|
+
|
|
19
|
+
Las reglas son las de Python: los argumentos por defecto, las anotaciones y los
|
|
20
|
+
decoradores de una función o `lambda` se evalúan fuera; el iterable del **primer**
|
|
21
|
+
`for` de una comprensión, también. Lo demás de una comprensión, el cuerpo de una
|
|
22
|
+
`lambda` y el de una función anidada, dentro.
|
|
23
|
+
|
|
24
|
+
NO le corresponde: reescribir nada (eso es `transformer.py`) ni registrar operaciones
|
|
25
|
+
(eso es `analyzer.py`). Solo responde «¿este `t` es el parámetro?».
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
from typing import Dict, Iterable, Optional, Set
|
|
29
|
+
import ast
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
#: Dónde no se puede esperar a una corrutina, con cómo se nombra en los mensajes.
|
|
33
|
+
LAMBDA = 'una `lambda`'
|
|
34
|
+
GENERATOR = 'una expresión generadora'
|
|
35
|
+
FUNCTION = 'una función anidada que no es `async`'
|
|
36
|
+
|
|
37
|
+
_COMPREHENSIONS = (ast.ListComp, ast.SetComp, ast.DictComp, ast.GeneratorExp)
|
|
38
|
+
_FUNCTIONS = (ast.FunctionDef, ast.AsyncFunctionDef)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def param_refs(roots: Iterable[ast.AST], name: str) -> Dict[ast.Name, Optional[str]]:
|
|
42
|
+
"""
|
|
43
|
+
Las apariciones de `name` que se refieren al parámetro del trigger.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
roots: Nodos que están en el ámbito del propio trigger: las sentencias de su
|
|
47
|
+
cuerpo, un `ast.Module` con ellas o la expresión de su `when=`.
|
|
48
|
+
name: El nombre del parámetro (`t`, o el que use el trigger).
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Cada `ast.Name` que es el parámetro → dónde cae: `None` si se puede esperar
|
|
52
|
+
ahí, o `LAMBDA`, `GENERATOR` o `FUNCTION` si no. Las claves son los propios
|
|
53
|
+
nodos (un nodo AST se compara por identidad): quien reescribe el árbol puede
|
|
54
|
+
seguir preguntando por los que conserva.
|
|
55
|
+
"""
|
|
56
|
+
refs: Dict[ast.Name, Optional[str]] = {}
|
|
57
|
+
|
|
58
|
+
def walk(node: ast.AST, shadowed: bool, deferred: Optional[str]) -> None:
|
|
59
|
+
if isinstance(node, ast.Name):
|
|
60
|
+
if node.id == name and not shadowed:
|
|
61
|
+
refs[node] = deferred
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
if isinstance(node, ast.Lambda):
|
|
65
|
+
_walk_all(_outer_parts(node.args), walk, shadowed, deferred)
|
|
66
|
+
walk(node.body, shadowed or name in _arg_names(node.args), LAMBDA)
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
if isinstance(node, _FUNCTIONS):
|
|
70
|
+
outer = [*node.decorator_list, *_outer_parts(node.args)]
|
|
71
|
+
if node.returns is not None:
|
|
72
|
+
outer.append(node.returns)
|
|
73
|
+
_walk_all(outer, walk, shadowed, deferred)
|
|
74
|
+
inner = shadowed or name in _arg_names(node.args) | local_names(node.body)
|
|
75
|
+
# Dentro de un `async def` se puede esperar, aunque esté en una lambda.
|
|
76
|
+
inner_deferred = None if isinstance(node, ast.AsyncFunctionDef) else FUNCTION
|
|
77
|
+
_walk_all(node.body, walk, inner, inner_deferred)
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
if isinstance(node, _COMPREHENSIONS):
|
|
81
|
+
first, *rest = node.generators
|
|
82
|
+
# El iterable del primer `for` se evalúa en el ámbito de fuera, y ya: en
|
|
83
|
+
# `[x for t in t.find_many(...)]` ese `t` sí es el parámetro.
|
|
84
|
+
walk(first.iter, shadowed, deferred)
|
|
85
|
+
bound = set()
|
|
86
|
+
for generator in node.generators:
|
|
87
|
+
bound |= _stored_names(generator.target)
|
|
88
|
+
inner = shadowed or name in bound
|
|
89
|
+
# Una comprensión de lista, conjunto o dict se evalúa en el sitio, y en un
|
|
90
|
+
# `async def` admite `await`; una expresión generadora, no.
|
|
91
|
+
inner_deferred = GENERATOR if isinstance(node, ast.GeneratorExp) else deferred
|
|
92
|
+
for i, generator in enumerate(node.generators):
|
|
93
|
+
walk(generator.target, inner, inner_deferred)
|
|
94
|
+
if i:
|
|
95
|
+
walk(generator.iter, inner, inner_deferred)
|
|
96
|
+
_walk_all(generator.ifs, walk, inner, inner_deferred)
|
|
97
|
+
parts = [node.key, node.value] if isinstance(node, ast.DictComp) else [node.elt]
|
|
98
|
+
_walk_all(parts, walk, inner, inner_deferred)
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
_walk_all(ast.iter_child_nodes(node), walk, shadowed, deferred)
|
|
102
|
+
|
|
103
|
+
_walk_all(roots, walk, False, None)
|
|
104
|
+
return refs
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def local_names(body: Iterable[ast.stmt]) -> Set[str]:
|
|
108
|
+
"""
|
|
109
|
+
Los nombres que liga un bloque en su propio ámbito, sin entrar en los internos.
|
|
110
|
+
|
|
111
|
+
Es lo que hace local un nombre en una función: un argumento, una asignación, el
|
|
112
|
+
destino de un `for` o de un `with`, un `import`, un `except ... as`, una `def`.
|
|
113
|
+
"""
|
|
114
|
+
names: Set[str] = set()
|
|
115
|
+
declared: Set[str] = set()
|
|
116
|
+
pending = list(body)
|
|
117
|
+
while pending:
|
|
118
|
+
node = pending.pop()
|
|
119
|
+
if isinstance(node, (*_FUNCTIONS, ast.ClassDef)):
|
|
120
|
+
names.add(node.name)
|
|
121
|
+
continue
|
|
122
|
+
if isinstance(node, (ast.Lambda, *_COMPREHENSIONS)):
|
|
123
|
+
continue
|
|
124
|
+
if isinstance(node, (ast.Global, ast.Nonlocal)):
|
|
125
|
+
declared.update(node.names)
|
|
126
|
+
continue
|
|
127
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, (ast.Store, ast.Del)):
|
|
128
|
+
names.add(node.id)
|
|
129
|
+
elif isinstance(node, (ast.Import, ast.ImportFrom)):
|
|
130
|
+
names.update((alias.asname or alias.name).split('.')[0] for alias in node.names)
|
|
131
|
+
elif isinstance(node, ast.ExceptHandler) and node.name:
|
|
132
|
+
names.add(node.name)
|
|
133
|
+
pending.extend(ast.iter_child_nodes(node))
|
|
134
|
+
return names - declared
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _walk_all(nodes, walk, shadowed: bool, deferred: Optional[str]) -> None:
|
|
138
|
+
for node in nodes:
|
|
139
|
+
if node is not None:
|
|
140
|
+
walk(node, shadowed, deferred)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _outer_parts(args: ast.arguments) -> list:
|
|
144
|
+
"""Lo de una firma que se evalúa fuera de la función: defaults y anotaciones."""
|
|
145
|
+
parts = [*args.defaults, *args.kw_defaults]
|
|
146
|
+
for arg in (*args.posonlyargs, *args.args, args.vararg, *args.kwonlyargs, args.kwarg):
|
|
147
|
+
if arg is not None and arg.annotation is not None:
|
|
148
|
+
parts.append(arg.annotation)
|
|
149
|
+
return parts
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _arg_names(args: ast.arguments) -> Set[str]:
|
|
153
|
+
return {
|
|
154
|
+
arg.arg
|
|
155
|
+
for arg in (*args.posonlyargs, *args.args, args.vararg, *args.kwonlyargs, args.kwarg)
|
|
156
|
+
if arg is not None
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _stored_names(target: ast.AST) -> Set[str]:
|
|
161
|
+
return {
|
|
162
|
+
node.id for node in ast.walk(target)
|
|
163
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store)
|
|
164
|
+
}
|