synalog 2.0.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {synalog-2.0.0 → synalog-2.2.0}/Cargo.lock +1 -1
- {synalog-2.0.0 → synalog-2.2.0}/Cargo.toml +1 -1
- {synalog-2.0.0 → synalog-2.2.0}/PKG-INFO +38 -38
- {synalog-2.0.0 → synalog-2.2.0}/README.md +37 -37
- {synalog-2.0.0 → synalog-2.2.0}/docs/assertions.md +6 -5
- synalog-2.2.0/docs/assets/synalog-dark.svg +11 -0
- synalog-2.2.0/docs/assets/synalog-light.svg +11 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/cli.md +29 -15
- {synalog-2.0.0 → synalog-2.2.0}/docs/development.md +1 -1
- {synalog-2.0.0 → synalog-2.2.0}/docs/differences-datalog.md +8 -8
- {synalog-2.0.0 → synalog-2.2.0}/docs/differences.md +2 -2
- {synalog-2.0.0 → synalog-2.2.0}/docs/engines.md +1 -1
- synalog-2.2.0/docs/examples/functions.log +103 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/recursion.log +20 -20
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/run.py +2 -2
- synalog-2.0.0/docs/examples/shop/concepts/Customer.l → synalog-2.2.0/docs/examples/shop/concepts/customer.l +1 -1
- synalog-2.0.0/docs/examples/shop/concepts/OrderStatus.l → synalog-2.2.0/docs/examples/shop/concepts/order_status.l +1 -1
- synalog-2.0.0/docs/examples/shop/rules/CustomerRevenue.l → synalog-2.2.0/docs/examples/shop/rules/customer_revenue.l +2 -2
- synalog-2.0.0/docs/examples/shop/rules/OrdersByStatus.l → synalog-2.2.0/docs/examples/shop/rules/orders_by_status.l +2 -2
- synalog-2.0.0/docs/examples/shop/rules/TopCustomers.l → synalog-2.2.0/docs/examples/shop/rules/top_customers.l +1 -1
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/shop.log +6 -6
- synalog-2.0.0/docs/examples/smoke/concepts/Device.l → synalog-2.2.0/docs/examples/smoke/concepts/device.l +1 -1
- synalog-2.0.0/docs/examples/smoke/concepts/TestStatus.l → synalog-2.2.0/docs/examples/smoke/concepts/test_status.l +1 -1
- synalog-2.0.0/docs/examples/smoke/rules/FailuresByDevice.l → synalog-2.2.0/docs/examples/smoke/rules/failures_by_device.l +2 -2
- synalog-2.0.0/docs/examples/smoke/rules/RunsPerDay.l → synalog-2.2.0/docs/examples/smoke/rules/runs_per_day.l +1 -1
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/smoke.log +5 -5
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/syntax.log +7 -7
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/temporal.log +3 -3
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/temporal_graph.log +11 -11
- {synalog-2.0.0 → synalog-2.2.0}/docs/getting-started.md +16 -16
- {synalog-2.0.0 → synalog-2.2.0}/docs/javascripts/playground.js +2 -2
- synalog-2.2.0/docs/javascripts/synalog-highlight.js +302 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/knowledge-graphs.md +24 -24
- {synalog-2.0.0 → synalog-2.2.0}/docs/language/aggregation.md +7 -7
- {synalog-2.0.0 → synalog-2.2.0}/docs/language/directives.md +8 -8
- {synalog-2.0.0 → synalog-2.2.0}/docs/language/functions.md +5 -5
- {synalog-2.0.0 → synalog-2.2.0}/docs/language/functors.md +4 -4
- {synalog-2.0.0 → synalog-2.2.0}/docs/language/index.md +35 -35
- {synalog-2.0.0 → synalog-2.2.0}/docs/language/recursion.md +8 -6
- {synalog-2.0.0 → synalog-2.2.0}/docs/language/syntax.md +15 -15
- {synalog-2.0.0 → synalog-2.2.0}/docs/language/temporal.md +11 -11
- {synalog-2.0.0 → synalog-2.2.0}/docs/python-api.md +39 -8
- {synalog-2.0.0 → synalog-2.2.0}/docs/stylesheets/extra.css +49 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/verification.md +5 -4
- synalog-2.2.0/img/synalog-dark.svg +11 -0
- synalog-2.2.0/img/synalog-light.svg +11 -0
- {synalog-2.0.0 → synalog-2.2.0}/pyproject.toml +1 -1
- {synalog-2.0.0 → synalog-2.2.0}/python/synalog/__init__.py +4 -0
- {synalog-2.0.0 → synalog-2.2.0}/python/synalog/checking.py +37 -36
- {synalog-2.0.0 → synalog-2.2.0}/python/synalog/cli.py +85 -116
- synalog-2.2.0/python/synalog/config.py +54 -0
- {synalog-2.0.0 → synalog-2.2.0}/python/synalog/execution.py +11 -30
- {synalog-2.0.0 → synalog-2.2.0}/python/synalog/introspect.py +46 -41
- {synalog-2.0.0 → synalog-2.2.0}/python/synalog/project.py +32 -49
- {synalog-2.0.0 → synalog-2.2.0}/python/synalog/runners.py +94 -91
- {synalog-2.0.0 → synalog-2.2.0}/skills/synalog/SKILL.md +43 -42
- synalog-2.0.0/skills/synalog/examples/company/concepts/Department.l → synalog-2.2.0/skills/synalog/examples/company/concepts/department.l +1 -1
- synalog-2.0.0/skills/synalog/examples/company/concepts/Person.l → synalog-2.2.0/skills/synalog/examples/company/concepts/person.l +1 -1
- synalog-2.0.0/skills/synalog/examples/company/concepts/WorksIn.l → synalog-2.2.0/skills/synalog/examples/company/concepts/works_in.l +3 -3
- synalog-2.0.0/skills/synalog/examples/company/rules/TeamMate.l → synalog-2.2.0/skills/synalog/examples/company/rules/team_mate.l +3 -3
- synalog-2.0.0/skills/synalog/examples/org/concepts/AllManagers.l → synalog-2.2.0/skills/synalog/examples/org/concepts/all_managers.l +1 -1
- synalog-2.0.0/skills/synalog/examples/sales/concepts/Region.l → synalog-2.2.0/skills/synalog/examples/sales/concepts/region.l +1 -1
- synalog-2.0.0/skills/synalog/examples/sales/rules/MonthlySales.l → synalog-2.2.0/skills/synalog/examples/sales/rules/monthly_sales.l +1 -1
- synalog-2.0.0/skills/synalog/examples/sales/rules/TopRegion.l → synalog-2.2.0/skills/synalog/examples/sales/rules/top_region.l +1 -1
- synalog-2.0.0/skills/synalog/examples/sales/rules/TotalByRegion.l → synalog-2.2.0/skills/synalog/examples/sales/rules/total_by_region.l +2 -2
- {synalog-2.0.0 → synalog-2.2.0}/src/assertion/mod.rs +1 -1
- {synalog-2.0.0 → synalog-2.2.0}/src/assertion/parse.rs +10 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/assertion/translate.rs +22 -18
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/dialects.rs +8 -4
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/expr_translate_test.rs +12 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/functors.rs +231 -2
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/functors_test.rs +12 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/universe.rs +144 -8
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/universe_test.rs +138 -12
- {synalog-2.0.0 → synalog-2.2.0}/src/errors.rs +7 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/python.rs +95 -12
- {synalog-2.0.0 → synalog-2.2.0}/src/verifier/assertions.rs +5 -0
- synalog-2.2.0/src/verifier/dependencies.rs +63 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/verifier/directives.rs +3 -2
- {synalog-2.0.0 → synalog-2.2.0}/src/verifier/functors.rs +1 -1
- {synalog-2.0.0 → synalog-2.2.0}/src/verifier/mod.rs +32 -1
- {synalog-2.0.0 → synalog-2.2.0}/src/verifier/undefined.rs +120 -4
- {synalog-2.0.0 → synalog-2.2.0}/src/wasm.rs +13 -2
- {synalog-2.0.0 → synalog-2.2.0}/uv.lock +1 -1
- {synalog-2.0.0 → synalog-2.2.0}/zensical.toml +4 -2
- synalog-2.0.0/docs/assets/synalog-dark.svg +0 -11
- synalog-2.0.0/docs/assets/synalog-light.svg +0 -11
- synalog-2.0.0/docs/examples/axioms.l +0 -122
- synalog-2.0.0/docs/examples/axioms.log +0 -13203
- synalog-2.0.0/docs/examples/axioms.ttl +0 -68
- synalog-2.0.0/docs/examples/functions.log +0 -103
- synalog-2.0.0/img/synalog-dark.svg +0 -11
- synalog-2.0.0/img/synalog-light.svg +0 -11
- synalog-2.0.0/python/synalog/config.py +0 -113
- {synalog-2.0.0 → synalog-2.2.0}/.gitignore +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/LICENSE +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/NOTICE +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/assets/synalog_favicon.png +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/benchmark/results.json +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/benchmark/speedup_by_engine.png +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/benchmark/speedup_distribution.png +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/benchmark/summary.md +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/benchmark/summary.png +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/benchmark/time_comparison.png +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/benchmark/verify_by_engine.png +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/benchmark.md +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/aggregation.l +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/aggregation.log +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/directives.l +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/directives.log +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/functions.l +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/functors.l +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/functors.log +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/getting_started.l +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/getting_started.log +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/knowledge_graphs.l +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/knowledge_graphs.log +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/recursion.l +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/shop/data/orders.csv +0 -0
- /synalog-2.0.0/docs/examples/shop/synalog.toml → /synalog-2.2.0/docs/examples/shop/layer.toml +0 -0
- /synalog-2.0.0/docs/examples/shop/tables/Orders.l → /synalog-2.2.0/docs/examples/shop/tables/orders.l +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/smoke/data/smoke_tests.csv +0 -0
- /synalog-2.0.0/docs/examples/smoke/synalog.toml → /synalog-2.2.0/docs/examples/smoke/layer.toml +0 -0
- /synalog-2.0.0/docs/examples/smoke/tables/SmokeTests.l → /synalog-2.2.0/docs/examples/smoke/tables/smoke_tests.l +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/syntax.l +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/temporal.l +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/temporal_graph.l +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/verification.l +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/examples/verification.log +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/index.md +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/javascripts/summarize.js +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/playground/vendor/codemirror.js +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/playground.md +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/docs/why.md +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/python/synalog/__main__.py +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/skills/synalog/examples/company/data/departments.csv +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/skills/synalog/examples/company/data/employees.csv +0 -0
- /synalog-2.0.0/skills/synalog/examples/company/synalog.toml → /synalog-2.2.0/skills/synalog/examples/company/layer.toml +0 -0
- /synalog-2.0.0/skills/synalog/examples/company/tables/Departments.l → /synalog-2.2.0/skills/synalog/examples/company/tables/departments.l +0 -0
- /synalog-2.0.0/skills/synalog/examples/company/tables/Employees.l → /synalog-2.2.0/skills/synalog/examples/company/tables/employees.l +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/skills/synalog/examples/org/data/employees.csv +0 -0
- /synalog-2.0.0/skills/synalog/examples/org/synalog.toml → /synalog-2.2.0/skills/synalog/examples/org/layer.toml +0 -0
- /synalog-2.0.0/skills/synalog/examples/org/tables/Employees.l → /synalog-2.2.0/skills/synalog/examples/org/tables/employees.l +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/skills/synalog/examples/sales/data/sales.csv +0 -0
- /synalog-2.0.0/skills/synalog/examples/sales/synalog.toml → /synalog-2.2.0/skills/synalog/examples/sales/layer.toml +0 -0
- /synalog-2.0.0/skills/synalog/examples/sales/tables/Sales.l → /synalog-2.2.0/skills/synalog/examples/sales/tables/sales.l +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/assertion/report.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/annotations.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/annotations_test.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/concertina.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/dialects_test.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/expr_translate.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/mod.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/program.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/program_test.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/rule_translate.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/rule_translate_test.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/type_inference/builder.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/type_inference/built_in.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/type_inference/edge.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/type_inference/expression.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/type_inference/graph.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/type_inference/inference.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/type_inference/intersection.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/type_inference/mod.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/compiler/type_inference/types.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/lib.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/parser/json.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/parser/json_test.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/parser/mod.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/parser/names.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/parser/parse.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/parser/parse_test.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/parser/rewrite.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/parser/rewrite_test.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/parser/span.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/parser/span_test.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/parser/traverse.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/parser/traverse_test.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/verifier/arity.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/verifier/contradiction.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/verifier/front_matter.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/verifier/orderby.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/verifier/positional.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/verifier/recursion.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/verifier/reserved.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/verifier/safety.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/verifier/sqlexpr.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/verifier/stratification.rs +0 -0
- {synalog-2.0.0 → synalog-2.2.0}/src/verifier/vars.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: synalog
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Classifier: Programming Language :: Rust
|
|
5
5
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -162,13 +162,13 @@ $ synalog program.l run EngineeringTeam
|
|
|
162
162
|
- `--limit` / `--offset` paginate the result.
|
|
163
163
|
- `--csv` (with `run`) prints results as CSV instead of the rendered table.
|
|
164
164
|
- `--search REGEX` (with `print`/`run`) keeps only rows where some column matches the regular expression `REGEX`, e.g. `synalog program.l run Customers --search "(?i)acme"`. In the interactive session the same is `.search Customers (?i)acme`.
|
|
165
|
-
- `run` executes on `duckdb` (the default), `sqlite` (stdlib) or `psql` (
|
|
165
|
+
- `run` executes on `duckdb` (the default), `sqlite` (stdlib) or `psql` (in a [project](#projects-layertoml) connected to it) out of the box: synalog depends on duckdb and on psycopg with its bundled libpq, on macOS and Linux alike. For other engines, install their driver (`pip install 'synalog[trino]'`, ...) or use `print` and run the SQL with your own client.
|
|
166
166
|
- `import path.to.file.Pred;` statements resolve `path/to/file.l` against the program file's directory, then the current directory; pass `--import-root DIR` (repeatable) to search elsewhere.
|
|
167
167
|
- `--load TABLE=PATH` (repeatable) loads a csv/tsv/json/jsonl/parquet file as a table before running, e.g. `synalog senior.l run Senior --load employees=employees.csv`.
|
|
168
168
|
|
|
169
|
-
### Projects: `
|
|
169
|
+
### Projects: `layer.toml`
|
|
170
170
|
|
|
171
|
-
A folder with a `
|
|
171
|
+
A folder with a `layer.toml` is a project. Its `[connection]` says which database its programs run on, as plain fields — commit it:
|
|
172
172
|
|
|
173
173
|
```toml
|
|
174
174
|
[connection]
|
|
@@ -180,7 +180,7 @@ user = "analyst"
|
|
|
180
180
|
schema = "public"
|
|
181
181
|
```
|
|
182
182
|
|
|
183
|
-
Secrets never go in the file (synalog refuses it): each comes from the environment as `SYNALOG_<ENGINE>_<FIELD>` — `SYNALOG_PSQL_PASSWORD`, `SYNALOG_DATABRICKS_ACCESS_TOKEN`, or `GOOGLE_APPLICATION_CREDENTIALS` for BigQuery — usually from the project's `.env`, kept out of git. Inside the project (from the program's folder or the current directory, and their parents), `run`, `print` and `verify` use that engine and connection, imports resolve from the project's folder, and `synalog introspect` writes the project's `tables/`, one file per table of the database; `--engine
|
|
183
|
+
Secrets never go in the file (synalog refuses it): each comes from the environment as `SYNALOG_<ENGINE>_<FIELD>` — `SYNALOG_PSQL_PASSWORD`, `SYNALOG_DATABRICKS_ACCESS_TOKEN`, or `GOOGLE_APPLICATION_CREDENTIALS` for BigQuery — usually from the project's `.env`, kept out of git. Inside the project (from the program's folder or the current directory, and their parents), `run`, `print` and `verify` use that engine and connection, imports resolve from the project's folder, and `synalog introspect` writes the project's `tables/`, one file per table of the database. The project's file is the only place a connection comes from; `--engine` and an `@Engine` annotation choose the dialect, and a remote engine runs only through the project's connection to it. `synalog connect <engine> key=value ...` writes it — the non-secret fields to `layer.toml`, the secret ones to `.env` — `synalog connect` shows it (secrets hidden), `synalog connect clear` removes it. The fields of every engine are in `synalog.project.ENGINES`.
|
|
184
184
|
|
|
185
185
|
Running `synalog` with no arguments starts an interactive session, in the spirit of `python` (the options above, e.g. `--engine` or `--load`, apply to it too):
|
|
186
186
|
|
|
@@ -262,7 +262,7 @@ for name, sql in sqls.items():
|
|
|
262
262
|
print(name, sql)
|
|
263
263
|
```
|
|
264
264
|
|
|
265
|
-
### `execute(source, predicate, engine=None,
|
|
265
|
+
### `execute(source, predicate, engine=None, project=None, import_root=None, limit=None, offset=None, pattern=None, assertion=None, loads=()) -> tuple[list[str], list[tuple]]`
|
|
266
266
|
|
|
267
267
|
Run a predicate on its database (the program's or project's engine and connection) and return `(columns, rows)`. Each recursion stops as soon as a step changes nothing, so it costs the steps its data needs, and `@Recursive(P, -1)` (until nothing changes) runs on every engine. `plan()` returns the same steps for a host that runs them itself.
|
|
268
268
|
|
|
@@ -270,9 +270,9 @@ Run a predicate on its database (the program's or project's engine and connectio
|
|
|
270
270
|
columns, rows = synalog.execute(source, "TopCustomers", limit=20)
|
|
271
271
|
```
|
|
272
272
|
|
|
273
|
-
### `check(source, engine=None, import_root=None, assertions=True,
|
|
273
|
+
### `check(source, engine=None, import_root=None, assertions=True, project=None) -> tuple[list[str], list[str]]`
|
|
274
274
|
|
|
275
|
-
Run structural validation. Returns the error messages and the warning messages, as two lists. The program is valid when there is no error; warnings do not make it invalid. Inside a project whose `
|
|
275
|
+
Run structural validation. Returns the error messages and the warning messages, as two lists. The program is valid when there is no error; warnings do not make it invalid. Inside a project whose `layer.toml` has a `[connection]` — the current directory's, or the folder `project` names — it also runs the program's `@Assert` statements against the database and reports each violated one as an error; `assertions=False` keeps it offline.
|
|
276
276
|
|
|
277
277
|
```python
|
|
278
278
|
errors, warnings = synalog.check(source)
|
|
@@ -285,22 +285,22 @@ All of these functions accept an optional `engine` keyword that overrides the pr
|
|
|
285
285
|
|
|
286
286
|
## Language overview
|
|
287
287
|
|
|
288
|
-
A Synalog program is a **project**: a folder with a `
|
|
288
|
+
A Synalog program is a **project**: a folder with a `layer.toml`, holding one predicate per file in three folders, the layout of [semantic layers](https://github.com/SynaLinks/semantic-layers):
|
|
289
289
|
|
|
290
290
|
```
|
|
291
291
|
shop/
|
|
292
|
-
├── tables/
|
|
293
|
-
├── concepts/
|
|
294
|
-
├── rules/
|
|
295
|
-
├── rules/
|
|
296
|
-
└──
|
|
292
|
+
├── tables/orders.l # the data: one file per database table
|
|
293
|
+
├── concepts/customer.l # entities and relationships extracted from tables
|
|
294
|
+
├── rules/customer_revenue.l # insights derived from concepts
|
|
295
|
+
├── rules/top_customers.l
|
|
296
|
+
└── layer.toml # the project's name, description and database
|
|
297
297
|
```
|
|
298
298
|
|
|
299
299
|
Each file is named after its predicate and opens with front matter (`name`, the predicate; `description`, what its rows are), then imports what it builds on by folder, file and predicate, then orders its predicate with `@OrderBy`. A database table is referenced by its lowercase database name in `tables/`, and everything else builds on the PascalCase predicate that maps it:
|
|
300
300
|
|
|
301
|
-
`tables/
|
|
301
|
+
`tables/orders.l`
|
|
302
302
|
|
|
303
|
-
```
|
|
303
|
+
```synalog
|
|
304
304
|
---
|
|
305
305
|
name: Orders
|
|
306
306
|
description: One row per order, with its customer, amount and status.
|
|
@@ -310,41 +310,41 @@ Orders(order_id:, customer_id:, amount:, status:) :-
|
|
|
310
310
|
orders(order_id:, customer_id:, amount:, status:);
|
|
311
311
|
```
|
|
312
312
|
|
|
313
|
-
`concepts/
|
|
313
|
+
`concepts/customer.l`
|
|
314
314
|
|
|
315
|
-
```
|
|
315
|
+
```synalog
|
|
316
316
|
---
|
|
317
317
|
name: Customer
|
|
318
318
|
description: Every customer who placed at least one order.
|
|
319
319
|
---
|
|
320
|
-
import tables.
|
|
320
|
+
import tables.orders.Orders;
|
|
321
321
|
|
|
322
322
|
@OrderBy(Customer, "customer_id");
|
|
323
323
|
Customer(customer_id:) distinct :- Orders(customer_id:);
|
|
324
324
|
```
|
|
325
325
|
|
|
326
|
-
`rules/
|
|
326
|
+
`rules/customer_revenue.l`
|
|
327
327
|
|
|
328
|
-
```
|
|
328
|
+
```synalog
|
|
329
329
|
---
|
|
330
330
|
name: CustomerRevenue
|
|
331
331
|
description: Total amount ordered by each customer, all statuses included.
|
|
332
332
|
---
|
|
333
|
-
import concepts.
|
|
334
|
-
import tables.
|
|
333
|
+
import concepts.customer.Customer;
|
|
334
|
+
import tables.orders.Orders;
|
|
335
335
|
|
|
336
336
|
@OrderBy(CustomerRevenue, "customer_id");
|
|
337
337
|
CustomerRevenue(customer_id:, total? += amount) distinct :-
|
|
338
338
|
Customer(customer_id:), Orders(customer_id:, amount:);
|
|
339
339
|
```
|
|
340
340
|
|
|
341
|
-
Imports resolve from the project's folder, so every file runs on its own: `synalog rules/
|
|
341
|
+
Imports resolve from the project's folder, so every file runs on its own: `synalog rules/customer_revenue.l run CustomerRevenue`. The snippets below show the language itself, one rule at a time.
|
|
342
342
|
|
|
343
343
|
### Named arguments
|
|
344
344
|
|
|
345
345
|
Synalog uses **named arguments only** (no positional arguments). The left side of `:` is the column name, the right side is the variable:
|
|
346
346
|
|
|
347
|
-
```
|
|
347
|
+
```synalog
|
|
348
348
|
# column "amount" bound to variable "total"
|
|
349
349
|
Orders(amount: total)
|
|
350
350
|
|
|
@@ -356,7 +356,7 @@ Orders(amount:)
|
|
|
356
356
|
|
|
357
357
|
Variables are defined with `==`. Arithmetic, string and comparison operators are supported:
|
|
358
358
|
|
|
359
|
-
```
|
|
359
|
+
```synalog
|
|
360
360
|
OrderWithTax(order_id:, total:) :-
|
|
361
361
|
Orders(order_id:, amount:),
|
|
362
362
|
total == amount * 1.10;
|
|
@@ -368,7 +368,7 @@ OrderWithTax(order_id:, total:) :-
|
|
|
368
368
|
|
|
369
369
|
Aggregation uses the `distinct` keyword and special operators in the rule head:
|
|
370
370
|
|
|
371
|
-
```
|
|
371
|
+
```synalog
|
|
372
372
|
# Sum
|
|
373
373
|
Revenue(total? += amount) distinct :- Orders(amount:);
|
|
374
374
|
|
|
@@ -392,7 +392,7 @@ TopSeller(name? ArgMax= name -> revenue) distinct :- Sales(name:, revenue:);
|
|
|
392
392
|
|
|
393
393
|
### Logical operators
|
|
394
394
|
|
|
395
|
-
```
|
|
395
|
+
```synalog
|
|
396
396
|
# Conjunction (AND), comma
|
|
397
397
|
Result(x:, y:) :- TableA(x:), TableB(x:, y:);
|
|
398
398
|
|
|
@@ -405,14 +405,14 @@ Inactive(user_id:) :- Users(user_id:), ~Logins(user_id:);
|
|
|
405
405
|
|
|
406
406
|
Multiple rule definitions for the same predicate combine results (union):
|
|
407
407
|
|
|
408
|
-
```
|
|
408
|
+
```synalog
|
|
409
409
|
HighValue(user_id:) :- Orders(user_id:, amount:), amount > 10000;
|
|
410
410
|
HighValue(user_id:) :- Referrals(user_id:, tier: "vip");
|
|
411
411
|
```
|
|
412
412
|
|
|
413
413
|
### Conditionals
|
|
414
414
|
|
|
415
|
-
```
|
|
415
|
+
```synalog
|
|
416
416
|
OrderSize(order_id:, size:) :-
|
|
417
417
|
Orders(order_id:, amount:),
|
|
418
418
|
size == (if amount > 1000 then "large"
|
|
@@ -424,7 +424,7 @@ OrderSize(order_id:, size:) :-
|
|
|
424
424
|
|
|
425
425
|
Directives control predicate behavior and **must be placed before** the rule definition:
|
|
426
426
|
|
|
427
|
-
```
|
|
427
|
+
```synalog
|
|
428
428
|
@OrderBy(TopCustomers, "total", "DESC");
|
|
429
429
|
@Limit(TopCustomers, 10);
|
|
430
430
|
TopCustomers(customer_id:, total? += amount) distinct :- Orders(customer_id:, amount:);
|
|
@@ -442,7 +442,7 @@ TopCustomers(customer_id:, total? += amount) distinct :- Orders(customer_id:, am
|
|
|
442
442
|
|
|
443
443
|
Functors let you reuse predicate logic by parameterizing input predicates:
|
|
444
444
|
|
|
445
|
-
```
|
|
445
|
+
```synalog
|
|
446
446
|
# Define a reusable pattern
|
|
447
447
|
@OrderBy(SegmentRevenue, "segment_id");
|
|
448
448
|
SegmentRevenue(segment_id:, total? += amount) distinct :-
|
|
@@ -458,7 +458,7 @@ SMBRevenue := SegmentRevenue(Segment: SMBCustomers);
|
|
|
458
458
|
|
|
459
459
|
Recursive predicates compute transitive closures, for example finding all managers above an employee:
|
|
460
460
|
|
|
461
|
-
```
|
|
461
|
+
```synalog
|
|
462
462
|
@Recursive(AllManagers, 20);
|
|
463
463
|
|
|
464
464
|
# Base case: direct manager
|
|
@@ -476,7 +476,7 @@ Useful for: referral chains, org charts, product taxonomies, bill of materials.
|
|
|
476
476
|
|
|
477
477
|
Find shortest paths in weighted graphs by enumerating route costs recursively, then keeping the minimum per destination with a `Min=` aggregation:
|
|
478
478
|
|
|
479
|
-
```
|
|
479
|
+
```synalog
|
|
480
480
|
# Enumerate route costs from the origin, hop by hop.
|
|
481
481
|
@Recursive(RouteCost, 10);
|
|
482
482
|
RouteCost(destination:, cost:) :-
|
|
@@ -495,7 +495,7 @@ ShippingCost(destination:, total? Min= cost) distinct :- RouteCost(destination:,
|
|
|
495
495
|
|
|
496
496
|
When working with timestamps or dates, always convert to string first:
|
|
497
497
|
|
|
498
|
-
```
|
|
498
|
+
```synalog
|
|
499
499
|
@OrderBy(MonthlyOrders, "month");
|
|
500
500
|
MonthlyOrders(month:, count? += 1) distinct :-
|
|
501
501
|
Orders(created_at:),
|
|
@@ -509,7 +509,7 @@ RecentOrders(order_id:) :-
|
|
|
509
509
|
|
|
510
510
|
`Today(date:)` (today's date as `"YYYY-MM-DD"`) and `Now(timestamp:)` (the current instant as the engine's native timestamp) are built-in concepts. They are inlined per dialect by the compiler, with no runtime table needed, so they work on every engine. `Now` is the most precise value; derive coarser parts (date, time, hour) from it through the `ToString` → `Substr` pipeline. Join against `Today` whenever a rule needs "today":
|
|
511
511
|
|
|
512
|
-
```
|
|
512
|
+
```synalog
|
|
513
513
|
@OrderBy(ThisMonthOrders, "order_id");
|
|
514
514
|
ThisMonthOrders(order_id:, created_at:) :-
|
|
515
515
|
Orders(order_id:, created_at:),
|
|
@@ -523,7 +523,7 @@ They are reserved names: you cannot redefine, extend, or update them.
|
|
|
523
523
|
|
|
524
524
|
Entities and relationships become node and edge concepts, and rules traverse them. Edges join **through** the node concepts rather than the raw table, so a filter on a node applies to every edge built on it:
|
|
525
525
|
|
|
526
|
-
```
|
|
526
|
+
```synalog
|
|
527
527
|
@OrderBy(Person, "person_id");
|
|
528
528
|
Person(person_id:, name:, profile_url:) distinct :- Employees(person_id:, name:, profile_url:);
|
|
529
529
|
|
|
@@ -536,7 +536,7 @@ WorksIn(person_id:, department_id:) distinct :-
|
|
|
536
536
|
|
|
537
537
|
Relationships with a lifetime carry it as a half-open interval `[valid_from, valid_to)`, using `"9999-12-31"` as the open end so ISO strings compare correctly, and `Today` supplies the clock for point-in-time questions:
|
|
538
538
|
|
|
539
|
-
```
|
|
539
|
+
```synalog
|
|
540
540
|
@OrderBy(CurrentEmployment, "person_id");
|
|
541
541
|
CurrentEmployment(person_id:, company_id:, role:) distinct :-
|
|
542
542
|
EmployedAt(person_id:, company_id:, role:, valid_from:, valid_to:),
|
|
@@ -120,13 +120,13 @@ $ synalog program.l run EngineeringTeam
|
|
|
120
120
|
- `--limit` / `--offset` paginate the result.
|
|
121
121
|
- `--csv` (with `run`) prints results as CSV instead of the rendered table.
|
|
122
122
|
- `--search REGEX` (with `print`/`run`) keeps only rows where some column matches the regular expression `REGEX`, e.g. `synalog program.l run Customers --search "(?i)acme"`. In the interactive session the same is `.search Customers (?i)acme`.
|
|
123
|
-
- `run` executes on `duckdb` (the default), `sqlite` (stdlib) or `psql` (
|
|
123
|
+
- `run` executes on `duckdb` (the default), `sqlite` (stdlib) or `psql` (in a [project](#projects-layertoml) connected to it) out of the box: synalog depends on duckdb and on psycopg with its bundled libpq, on macOS and Linux alike. For other engines, install their driver (`pip install 'synalog[trino]'`, ...) or use `print` and run the SQL with your own client.
|
|
124
124
|
- `import path.to.file.Pred;` statements resolve `path/to/file.l` against the program file's directory, then the current directory; pass `--import-root DIR` (repeatable) to search elsewhere.
|
|
125
125
|
- `--load TABLE=PATH` (repeatable) loads a csv/tsv/json/jsonl/parquet file as a table before running, e.g. `synalog senior.l run Senior --load employees=employees.csv`.
|
|
126
126
|
|
|
127
|
-
### Projects: `
|
|
127
|
+
### Projects: `layer.toml`
|
|
128
128
|
|
|
129
|
-
A folder with a `
|
|
129
|
+
A folder with a `layer.toml` is a project. Its `[connection]` says which database its programs run on, as plain fields — commit it:
|
|
130
130
|
|
|
131
131
|
```toml
|
|
132
132
|
[connection]
|
|
@@ -138,7 +138,7 @@ user = "analyst"
|
|
|
138
138
|
schema = "public"
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
-
Secrets never go in the file (synalog refuses it): each comes from the environment as `SYNALOG_<ENGINE>_<FIELD>` — `SYNALOG_PSQL_PASSWORD`, `SYNALOG_DATABRICKS_ACCESS_TOKEN`, or `GOOGLE_APPLICATION_CREDENTIALS` for BigQuery — usually from the project's `.env`, kept out of git. Inside the project (from the program's folder or the current directory, and their parents), `run`, `print` and `verify` use that engine and connection, imports resolve from the project's folder, and `synalog introspect` writes the project's `tables/`, one file per table of the database; `--engine
|
|
141
|
+
Secrets never go in the file (synalog refuses it): each comes from the environment as `SYNALOG_<ENGINE>_<FIELD>` — `SYNALOG_PSQL_PASSWORD`, `SYNALOG_DATABRICKS_ACCESS_TOKEN`, or `GOOGLE_APPLICATION_CREDENTIALS` for BigQuery — usually from the project's `.env`, kept out of git. Inside the project (from the program's folder or the current directory, and their parents), `run`, `print` and `verify` use that engine and connection, imports resolve from the project's folder, and `synalog introspect` writes the project's `tables/`, one file per table of the database. The project's file is the only place a connection comes from; `--engine` and an `@Engine` annotation choose the dialect, and a remote engine runs only through the project's connection to it. `synalog connect <engine> key=value ...` writes it — the non-secret fields to `layer.toml`, the secret ones to `.env` — `synalog connect` shows it (secrets hidden), `synalog connect clear` removes it. The fields of every engine are in `synalog.project.ENGINES`.
|
|
142
142
|
|
|
143
143
|
Running `synalog` with no arguments starts an interactive session, in the spirit of `python` (the options above, e.g. `--engine` or `--load`, apply to it too):
|
|
144
144
|
|
|
@@ -220,7 +220,7 @@ for name, sql in sqls.items():
|
|
|
220
220
|
print(name, sql)
|
|
221
221
|
```
|
|
222
222
|
|
|
223
|
-
### `execute(source, predicate, engine=None,
|
|
223
|
+
### `execute(source, predicate, engine=None, project=None, import_root=None, limit=None, offset=None, pattern=None, assertion=None, loads=()) -> tuple[list[str], list[tuple]]`
|
|
224
224
|
|
|
225
225
|
Run a predicate on its database (the program's or project's engine and connection) and return `(columns, rows)`. Each recursion stops as soon as a step changes nothing, so it costs the steps its data needs, and `@Recursive(P, -1)` (until nothing changes) runs on every engine. `plan()` returns the same steps for a host that runs them itself.
|
|
226
226
|
|
|
@@ -228,9 +228,9 @@ Run a predicate on its database (the program's or project's engine and connectio
|
|
|
228
228
|
columns, rows = synalog.execute(source, "TopCustomers", limit=20)
|
|
229
229
|
```
|
|
230
230
|
|
|
231
|
-
### `check(source, engine=None, import_root=None, assertions=True,
|
|
231
|
+
### `check(source, engine=None, import_root=None, assertions=True, project=None) -> tuple[list[str], list[str]]`
|
|
232
232
|
|
|
233
|
-
Run structural validation. Returns the error messages and the warning messages, as two lists. The program is valid when there is no error; warnings do not make it invalid. Inside a project whose `
|
|
233
|
+
Run structural validation. Returns the error messages and the warning messages, as two lists. The program is valid when there is no error; warnings do not make it invalid. Inside a project whose `layer.toml` has a `[connection]` — the current directory's, or the folder `project` names — it also runs the program's `@Assert` statements against the database and reports each violated one as an error; `assertions=False` keeps it offline.
|
|
234
234
|
|
|
235
235
|
```python
|
|
236
236
|
errors, warnings = synalog.check(source)
|
|
@@ -243,22 +243,22 @@ All of these functions accept an optional `engine` keyword that overrides the pr
|
|
|
243
243
|
|
|
244
244
|
## Language overview
|
|
245
245
|
|
|
246
|
-
A Synalog program is a **project**: a folder with a `
|
|
246
|
+
A Synalog program is a **project**: a folder with a `layer.toml`, holding one predicate per file in three folders, the layout of [semantic layers](https://github.com/SynaLinks/semantic-layers):
|
|
247
247
|
|
|
248
248
|
```
|
|
249
249
|
shop/
|
|
250
|
-
├── tables/
|
|
251
|
-
├── concepts/
|
|
252
|
-
├── rules/
|
|
253
|
-
├── rules/
|
|
254
|
-
└──
|
|
250
|
+
├── tables/orders.l # the data: one file per database table
|
|
251
|
+
├── concepts/customer.l # entities and relationships extracted from tables
|
|
252
|
+
├── rules/customer_revenue.l # insights derived from concepts
|
|
253
|
+
├── rules/top_customers.l
|
|
254
|
+
└── layer.toml # the project's name, description and database
|
|
255
255
|
```
|
|
256
256
|
|
|
257
257
|
Each file is named after its predicate and opens with front matter (`name`, the predicate; `description`, what its rows are), then imports what it builds on by folder, file and predicate, then orders its predicate with `@OrderBy`. A database table is referenced by its lowercase database name in `tables/`, and everything else builds on the PascalCase predicate that maps it:
|
|
258
258
|
|
|
259
|
-
`tables/
|
|
259
|
+
`tables/orders.l`
|
|
260
260
|
|
|
261
|
-
```
|
|
261
|
+
```synalog
|
|
262
262
|
---
|
|
263
263
|
name: Orders
|
|
264
264
|
description: One row per order, with its customer, amount and status.
|
|
@@ -268,41 +268,41 @@ Orders(order_id:, customer_id:, amount:, status:) :-
|
|
|
268
268
|
orders(order_id:, customer_id:, amount:, status:);
|
|
269
269
|
```
|
|
270
270
|
|
|
271
|
-
`concepts/
|
|
271
|
+
`concepts/customer.l`
|
|
272
272
|
|
|
273
|
-
```
|
|
273
|
+
```synalog
|
|
274
274
|
---
|
|
275
275
|
name: Customer
|
|
276
276
|
description: Every customer who placed at least one order.
|
|
277
277
|
---
|
|
278
|
-
import tables.
|
|
278
|
+
import tables.orders.Orders;
|
|
279
279
|
|
|
280
280
|
@OrderBy(Customer, "customer_id");
|
|
281
281
|
Customer(customer_id:) distinct :- Orders(customer_id:);
|
|
282
282
|
```
|
|
283
283
|
|
|
284
|
-
`rules/
|
|
284
|
+
`rules/customer_revenue.l`
|
|
285
285
|
|
|
286
|
-
```
|
|
286
|
+
```synalog
|
|
287
287
|
---
|
|
288
288
|
name: CustomerRevenue
|
|
289
289
|
description: Total amount ordered by each customer, all statuses included.
|
|
290
290
|
---
|
|
291
|
-
import concepts.
|
|
292
|
-
import tables.
|
|
291
|
+
import concepts.customer.Customer;
|
|
292
|
+
import tables.orders.Orders;
|
|
293
293
|
|
|
294
294
|
@OrderBy(CustomerRevenue, "customer_id");
|
|
295
295
|
CustomerRevenue(customer_id:, total? += amount) distinct :-
|
|
296
296
|
Customer(customer_id:), Orders(customer_id:, amount:);
|
|
297
297
|
```
|
|
298
298
|
|
|
299
|
-
Imports resolve from the project's folder, so every file runs on its own: `synalog rules/
|
|
299
|
+
Imports resolve from the project's folder, so every file runs on its own: `synalog rules/customer_revenue.l run CustomerRevenue`. The snippets below show the language itself, one rule at a time.
|
|
300
300
|
|
|
301
301
|
### Named arguments
|
|
302
302
|
|
|
303
303
|
Synalog uses **named arguments only** (no positional arguments). The left side of `:` is the column name, the right side is the variable:
|
|
304
304
|
|
|
305
|
-
```
|
|
305
|
+
```synalog
|
|
306
306
|
# column "amount" bound to variable "total"
|
|
307
307
|
Orders(amount: total)
|
|
308
308
|
|
|
@@ -314,7 +314,7 @@ Orders(amount:)
|
|
|
314
314
|
|
|
315
315
|
Variables are defined with `==`. Arithmetic, string and comparison operators are supported:
|
|
316
316
|
|
|
317
|
-
```
|
|
317
|
+
```synalog
|
|
318
318
|
OrderWithTax(order_id:, total:) :-
|
|
319
319
|
Orders(order_id:, amount:),
|
|
320
320
|
total == amount * 1.10;
|
|
@@ -326,7 +326,7 @@ OrderWithTax(order_id:, total:) :-
|
|
|
326
326
|
|
|
327
327
|
Aggregation uses the `distinct` keyword and special operators in the rule head:
|
|
328
328
|
|
|
329
|
-
```
|
|
329
|
+
```synalog
|
|
330
330
|
# Sum
|
|
331
331
|
Revenue(total? += amount) distinct :- Orders(amount:);
|
|
332
332
|
|
|
@@ -350,7 +350,7 @@ TopSeller(name? ArgMax= name -> revenue) distinct :- Sales(name:, revenue:);
|
|
|
350
350
|
|
|
351
351
|
### Logical operators
|
|
352
352
|
|
|
353
|
-
```
|
|
353
|
+
```synalog
|
|
354
354
|
# Conjunction (AND), comma
|
|
355
355
|
Result(x:, y:) :- TableA(x:), TableB(x:, y:);
|
|
356
356
|
|
|
@@ -363,14 +363,14 @@ Inactive(user_id:) :- Users(user_id:), ~Logins(user_id:);
|
|
|
363
363
|
|
|
364
364
|
Multiple rule definitions for the same predicate combine results (union):
|
|
365
365
|
|
|
366
|
-
```
|
|
366
|
+
```synalog
|
|
367
367
|
HighValue(user_id:) :- Orders(user_id:, amount:), amount > 10000;
|
|
368
368
|
HighValue(user_id:) :- Referrals(user_id:, tier: "vip");
|
|
369
369
|
```
|
|
370
370
|
|
|
371
371
|
### Conditionals
|
|
372
372
|
|
|
373
|
-
```
|
|
373
|
+
```synalog
|
|
374
374
|
OrderSize(order_id:, size:) :-
|
|
375
375
|
Orders(order_id:, amount:),
|
|
376
376
|
size == (if amount > 1000 then "large"
|
|
@@ -382,7 +382,7 @@ OrderSize(order_id:, size:) :-
|
|
|
382
382
|
|
|
383
383
|
Directives control predicate behavior and **must be placed before** the rule definition:
|
|
384
384
|
|
|
385
|
-
```
|
|
385
|
+
```synalog
|
|
386
386
|
@OrderBy(TopCustomers, "total", "DESC");
|
|
387
387
|
@Limit(TopCustomers, 10);
|
|
388
388
|
TopCustomers(customer_id:, total? += amount) distinct :- Orders(customer_id:, amount:);
|
|
@@ -400,7 +400,7 @@ TopCustomers(customer_id:, total? += amount) distinct :- Orders(customer_id:, am
|
|
|
400
400
|
|
|
401
401
|
Functors let you reuse predicate logic by parameterizing input predicates:
|
|
402
402
|
|
|
403
|
-
```
|
|
403
|
+
```synalog
|
|
404
404
|
# Define a reusable pattern
|
|
405
405
|
@OrderBy(SegmentRevenue, "segment_id");
|
|
406
406
|
SegmentRevenue(segment_id:, total? += amount) distinct :-
|
|
@@ -416,7 +416,7 @@ SMBRevenue := SegmentRevenue(Segment: SMBCustomers);
|
|
|
416
416
|
|
|
417
417
|
Recursive predicates compute transitive closures, for example finding all managers above an employee:
|
|
418
418
|
|
|
419
|
-
```
|
|
419
|
+
```synalog
|
|
420
420
|
@Recursive(AllManagers, 20);
|
|
421
421
|
|
|
422
422
|
# Base case: direct manager
|
|
@@ -434,7 +434,7 @@ Useful for: referral chains, org charts, product taxonomies, bill of materials.
|
|
|
434
434
|
|
|
435
435
|
Find shortest paths in weighted graphs by enumerating route costs recursively, then keeping the minimum per destination with a `Min=` aggregation:
|
|
436
436
|
|
|
437
|
-
```
|
|
437
|
+
```synalog
|
|
438
438
|
# Enumerate route costs from the origin, hop by hop.
|
|
439
439
|
@Recursive(RouteCost, 10);
|
|
440
440
|
RouteCost(destination:, cost:) :-
|
|
@@ -453,7 +453,7 @@ ShippingCost(destination:, total? Min= cost) distinct :- RouteCost(destination:,
|
|
|
453
453
|
|
|
454
454
|
When working with timestamps or dates, always convert to string first:
|
|
455
455
|
|
|
456
|
-
```
|
|
456
|
+
```synalog
|
|
457
457
|
@OrderBy(MonthlyOrders, "month");
|
|
458
458
|
MonthlyOrders(month:, count? += 1) distinct :-
|
|
459
459
|
Orders(created_at:),
|
|
@@ -467,7 +467,7 @@ RecentOrders(order_id:) :-
|
|
|
467
467
|
|
|
468
468
|
`Today(date:)` (today's date as `"YYYY-MM-DD"`) and `Now(timestamp:)` (the current instant as the engine's native timestamp) are built-in concepts. They are inlined per dialect by the compiler, with no runtime table needed, so they work on every engine. `Now` is the most precise value; derive coarser parts (date, time, hour) from it through the `ToString` → `Substr` pipeline. Join against `Today` whenever a rule needs "today":
|
|
469
469
|
|
|
470
|
-
```
|
|
470
|
+
```synalog
|
|
471
471
|
@OrderBy(ThisMonthOrders, "order_id");
|
|
472
472
|
ThisMonthOrders(order_id:, created_at:) :-
|
|
473
473
|
Orders(order_id:, created_at:),
|
|
@@ -481,7 +481,7 @@ They are reserved names: you cannot redefine, extend, or update them.
|
|
|
481
481
|
|
|
482
482
|
Entities and relationships become node and edge concepts, and rules traverse them. Edges join **through** the node concepts rather than the raw table, so a filter on a node applies to every edge built on it:
|
|
483
483
|
|
|
484
|
-
```
|
|
484
|
+
```synalog
|
|
485
485
|
@OrderBy(Person, "person_id");
|
|
486
486
|
Person(person_id:, name:, profile_url:) distinct :- Employees(person_id:, name:, profile_url:);
|
|
487
487
|
|
|
@@ -494,7 +494,7 @@ WorksIn(person_id:, department_id:) distinct :-
|
|
|
494
494
|
|
|
495
495
|
Relationships with a lifetime carry it as a half-open interval `[valid_from, valid_to)`, using `"9999-12-31"` as the open end so ISO strings compare correctly, and `Today` supplies the clock for point-in-time questions:
|
|
496
496
|
|
|
497
|
-
```
|
|
497
|
+
```synalog
|
|
498
498
|
@OrderBy(CurrentEmployment, "person_id");
|
|
499
499
|
CurrentEmployment(person_id:, company_id:, role:) distinct :-
|
|
500
500
|
EmployedAt(person_id:, company_id:, role:, valid_from:, valid_to:),
|
|
@@ -9,7 +9,7 @@ Assertions are written in first-order logic, not in Synalog. A rule and its asse
|
|
|
9
9
|
|
|
10
10
|
## A first assertion
|
|
11
11
|
|
|
12
|
-
```
|
|
12
|
+
```synalog
|
|
13
13
|
Parent(x:, y:) :- parents(parent: x, child: y);
|
|
14
14
|
|
|
15
15
|
@Assert(Ancestor,
|
|
@@ -48,7 +48,7 @@ Carol is Alice's grandchild, Erin is Carol's, but Erin is Alice's great-grandchi
|
|
|
48
48
|
|
|
49
49
|
`@Assert` takes the predicate the properties are about, then one named argument per property: the name identifies the property in reports, the string is the statement.
|
|
50
50
|
|
|
51
|
-
```
|
|
51
|
+
```synalog
|
|
52
52
|
@Assert(Revenue,
|
|
53
53
|
one_row_per_customer: "∀ c r s, Revenue c r → Revenue c s → r = s",
|
|
54
54
|
positive: "∀ c, Revenue c > 0");
|
|
@@ -57,6 +57,7 @@ Carol is Alice's grandchild, Erin is Carol's, but Erin is Alice's great-grandchi
|
|
|
57
57
|
- A predicate may have several `@Assert` annotations; a name may appear only once per predicate.
|
|
58
58
|
- A statement that spans several lines goes in a triple-quoted string (`"""..."""`).
|
|
59
59
|
- An assertion can be written before its predicate exists: it waits, as `pending`, until the predicate is defined. Write the contract first.
|
|
60
|
+
- Once its predicate exists, an assertion naming a predicate that nothing defines or imports is a warning: a misspelled name, or a missing import, would leave it pending, never checked.
|
|
60
61
|
- Assertions do not change the generated SQL.
|
|
61
62
|
|
|
62
63
|
## The statement language
|
|
@@ -74,7 +75,7 @@ Statements are written as [Lean](https://lean-lang.org/) propositions, with the
|
|
|
74
75
|
| `+`, `-`, `*`, `/` | | arithmetic |
|
|
75
76
|
| `∑ x, t` | `sum x, t` | sum of `t` over `x` |
|
|
76
77
|
|
|
77
|
-
```
|
|
78
|
+
```synalog
|
|
78
79
|
@Assert(Revenue, consistent: """forall c r s,
|
|
79
80
|
Revenue c r -> Revenue c s -> r = s""");
|
|
80
81
|
```
|
|
@@ -92,7 +93,7 @@ A name bound by a quantifier is a variable; an unbound lowercase name is univers
|
|
|
92
93
|
|
|
93
94
|
The properties worth stating are the ones a rule can silently get wrong. All of these run as written against the tables `orders(order_id, customer_id, amount, status)` and `customers(customer_id, country)`:
|
|
94
95
|
|
|
95
|
-
```
|
|
96
|
+
```synalog
|
|
96
97
|
Order(order_id:, customer_id:, amount:, status:) :- orders(order_id:, customer_id:, amount:, status:);
|
|
97
98
|
Customer(customer_id:, country:) :- customers(customer_id:, country:);
|
|
98
99
|
|
|
@@ -143,7 +144,7 @@ An assertion is checked by looking for its counterexamples: Synalog compiles tha
|
|
|
143
144
|
| `synalog program.l verify [Predicate ...]` | Runs every assertion, or those of the given predicates, and prints the counterexamples of each violated one (5 by default, `--limit` to change). Exits 1 if one is violated. |
|
|
144
145
|
| `synalog program.l run Predicate` | Checks the program's assertions before it prints anything, and exits 1 with a few counterexamples if one is violated. |
|
|
145
146
|
| `synalog program.l print Predicate` | Never touches the database: assertions are not run. |
|
|
146
|
-
| [`check()`](python-api.md#check) | Runs the assertions when a database is known: inside a project whose `
|
|
147
|
+
| [`check()`](python-api.md#check) | Runs the assertions when a database is known: inside a project whose `layer.toml` has a `[connection]` (the current directory's, or the folder `project` names). Each violated assertion is an error. `assertions=False` keeps it offline. |
|
|
147
148
|
|
|
148
149
|
```text
|
|
149
150
|
$ synalog family.l run Grandparent --load parents=parents.csv
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 100 100" role="img" aria-label="Synalog">
|
|
2
|
+
<title>Synalog</title>
|
|
3
|
+
<!-- A Hilbert curve of order 3: one line made of smaller copies of itself. -->
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="synalog" gradientUnits="userSpaceOnUse" x1="14.86" y1="14.86" x2="85.14" y2="85.14">
|
|
6
|
+
<stop offset="0" stop-color="#f0e3fb"/>
|
|
7
|
+
<stop offset="1" stop-color="#a271d6"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
</defs>
|
|
10
|
+
<path d="M14.86 14.86 L14.86 24.90 L24.90 24.90 L24.90 14.86 L34.94 14.86 L44.98 14.86 L44.98 24.90 L34.94 24.90 L34.94 34.94 L44.98 34.94 L44.98 44.98 L34.94 44.98 L24.90 44.98 L24.90 34.94 L14.86 34.94 L14.86 44.98 L14.86 55.02 L24.90 55.02 L24.90 65.06 L14.86 65.06 L14.86 75.10 L14.86 85.14 L24.90 85.14 L24.90 75.10 L34.94 75.10 L34.94 85.14 L44.98 85.14 L44.98 75.10 L44.98 65.06 L34.94 65.06 L34.94 55.02 L44.98 55.02 L55.02 55.02 L65.06 55.02 L65.06 65.06 L55.02 65.06 L55.02 75.10 L55.02 85.14 L65.06 85.14 L65.06 75.10 L75.10 75.10 L75.10 85.14 L85.14 85.14 L85.14 75.10 L85.14 65.06 L75.10 65.06 L75.10 55.02 L85.14 55.02 L85.14 44.98 L85.14 34.94 L75.10 34.94 L75.10 44.98 L65.06 44.98 L55.02 44.98 L55.02 34.94 L65.06 34.94 L65.06 24.90 L55.02 24.90 L55.02 14.86 L65.06 14.86 L75.10 14.86 L75.10 24.90 L85.14 24.90 L85.14 14.86" fill="none" stroke="url(#synalog)" stroke-width="5.71" stroke-linecap="round" stroke-linejoin="round"/>
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 100 100" role="img" aria-label="Synalog">
|
|
2
|
+
<title>Synalog</title>
|
|
3
|
+
<!-- A Hilbert curve of order 3: one line made of smaller copies of itself. -->
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="synalog" gradientUnits="userSpaceOnUse" x1="14.86" y1="14.86" x2="85.14" y2="85.14">
|
|
6
|
+
<stop offset="0" stop-color="#4e2275"/>
|
|
7
|
+
<stop offset="1" stop-color="#b07ddb"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
</defs>
|
|
10
|
+
<path d="M14.86 14.86 L14.86 24.90 L24.90 24.90 L24.90 14.86 L34.94 14.86 L44.98 14.86 L44.98 24.90 L34.94 24.90 L34.94 34.94 L44.98 34.94 L44.98 44.98 L34.94 44.98 L24.90 44.98 L24.90 34.94 L14.86 34.94 L14.86 44.98 L14.86 55.02 L24.90 55.02 L24.90 65.06 L14.86 65.06 L14.86 75.10 L14.86 85.14 L24.90 85.14 L24.90 75.10 L34.94 75.10 L34.94 85.14 L44.98 85.14 L44.98 75.10 L44.98 65.06 L34.94 65.06 L34.94 55.02 L44.98 55.02 L55.02 55.02 L65.06 55.02 L65.06 65.06 L55.02 65.06 L55.02 75.10 L55.02 85.14 L65.06 85.14 L65.06 75.10 L75.10 75.10 L75.10 85.14 L85.14 85.14 L85.14 75.10 L85.14 65.06 L75.10 65.06 L75.10 55.02 L85.14 55.02 L85.14 44.98 L85.14 34.94 L75.10 34.94 L75.10 44.98 L65.06 44.98 L55.02 44.98 L55.02 34.94 L65.06 34.94 L65.06 24.90 L55.02 24.90 L55.02 14.86 L65.06 14.86 L75.10 14.86 L75.10 24.90 L85.14 24.90 L85.14 14.86" fill="none" stroke="url(#synalog)" stroke-width="5.71" stroke-linecap="round" stroke-linejoin="round"/>
|
|
11
|
+
</svg>
|