Repository: PRQL/prql Branch: main Commit: e100d3d5fb86 Files: 892 Total size: 3.1 MB Directory structure: gitextract_epfrnbyg/ ├── .cargo/ │ └── config.toml ├── .config/ │ ├── insta.yaml │ ├── lychee.toml │ ├── nextest.toml │ ├── vscode-recommended/ │ │ ├── launch.json │ │ ├── settings.json │ │ └── tasks.json │ └── wt.toml ├── .devcontainer/ │ ├── base-image/ │ │ └── Dockerfile │ └── devcontainer.json ├── .gitattributes ├── .github/ │ ├── .codecov.yaml │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yaml │ │ ├── config.yaml │ │ └── something_else.yaml │ ├── actionlint.yaml │ ├── actions/ │ │ ├── build-prqlc/ │ │ │ └── action.yaml │ │ ├── build-prqlc-c/ │ │ │ └── action.yaml │ │ ├── build-python/ │ │ │ └── action.yaml │ │ └── time-compilation/ │ │ └── action.yaml │ ├── dependabot.yaml │ ├── nightly-failure.md │ └── workflows/ │ ├── README.md │ ├── build-devcontainer.yaml │ ├── build-web.yaml │ ├── claude.yaml │ ├── lint-megalinter.yaml │ ├── nightly.yaml │ ├── publish-web.yaml │ ├── pull-request-target.yaml │ ├── release.yaml │ ├── scripts/ │ │ ├── set_version.sh │ │ └── util_free_space.sh │ ├── test-dotnet.yaml │ ├── test-elixir.yaml │ ├── test-java.yaml │ ├── test-js.yaml │ ├── test-php.yaml │ ├── test-prqlc-c.yaml │ ├── test-python.yaml │ ├── test-rust.yaml │ └── tests.yaml ├── .gitignore ├── .markdownlint-cli2.yaml ├── .mega-linter.yaml ├── .pre-commit-config.yaml ├── .prettierignore ├── .prettierrc.yaml ├── .sqlfluff ├── .typos.toml ├── .vscode/ │ └── extensions.json ├── CHANGELOG.md ├── CLAUDE.md ├── Cargo.toml ├── LICENSE ├── README.md ├── Taskfile.yaml ├── bacon.toml ├── flake.nix ├── grammars/ │ ├── CotEditor/ │ │ ├── PRQL.yaml │ │ └── README.md │ ├── GtkSourceView/ │ │ ├── README.md │ │ └── prql.lang │ ├── KSyntaxHighlighting/ │ │ ├── README.md │ │ └── prql.xml │ ├── README.md │ ├── emacs/ │ │ ├── README.md │ │ └── prql-mode.el │ ├── nano/ │ │ ├── README.md │ │ └── prql.nanorc │ ├── prql-lezer/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ ├── rollup.config.js │ │ ├── src/ │ │ │ ├── highlight.js │ │ │ └── prql.grammar │ │ └── test/ │ │ ├── arithmetics.txt │ │ ├── arrays.txt │ │ ├── datetime.txt │ │ ├── full_queries.txt │ │ ├── identifiers.txt │ │ ├── misc.txt │ │ ├── numbers.txt │ │ ├── operators.txt │ │ ├── strings.txt │ │ ├── test-prql.js │ │ └── tuples.txt │ ├── raku/ │ │ ├── META6.json │ │ ├── README.md │ │ ├── lib/ │ │ │ └── prql.rakumod │ │ └── t/ │ │ ├── arithmetics.rakutest │ │ ├── arrays.rakutest │ │ ├── datetime.rakutest │ │ ├── full_queries.rakutest │ │ ├── identifiers.rakutest │ │ ├── misc.rakutest │ │ ├── numbers.rakutest │ │ ├── operators.rakutest │ │ ├── strings.rakutest │ │ └── tuples.rakutest │ └── vim/ │ └── README.md ├── prqlc/ │ ├── README.md │ ├── Taskfile.yaml │ ├── bindings/ │ │ ├── dotnet/ │ │ │ ├── .gitignore │ │ │ ├── PrqlCompiler/ │ │ │ │ ├── Message.cs │ │ │ │ ├── MessageKind.cs │ │ │ │ ├── NativePrqlCompilerOptions.cs │ │ │ │ ├── NativeResult.cs │ │ │ │ ├── PrqlCompiler.cs │ │ │ │ ├── PrqlCompiler.csproj │ │ │ │ ├── PrqlCompilerOptions.cs │ │ │ │ ├── Result.cs │ │ │ │ ├── SourceLocation.cs │ │ │ │ └── Span.cs │ │ │ ├── PrqlCompiler.Tests/ │ │ │ │ ├── CompilerTest.cs │ │ │ │ ├── PrqlCompiler.Tests.csproj │ │ │ │ └── Usings.cs │ │ │ ├── README.md │ │ │ └── prql-net.sln │ │ ├── elixir/ │ │ │ ├── .formatter.exs │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── lib/ │ │ │ │ ├── prql/ │ │ │ │ │ ├── errors.ex │ │ │ │ │ └── native.ex │ │ │ │ └── prql.ex │ │ │ ├── mix.exs │ │ │ ├── native/ │ │ │ │ └── prql/ │ │ │ │ ├── .cargo/ │ │ │ │ │ └── config.toml │ │ │ │ ├── Cargo.toml │ │ │ │ ├── README.md │ │ │ │ └── src/ │ │ │ │ └── lib.rs │ │ │ └── test/ │ │ │ ├── prql_test.exs │ │ │ └── test_helper.exs │ │ ├── java/ │ │ │ ├── .gitignore │ │ │ ├── Cargo.toml │ │ │ ├── DEVELOPMENT.md │ │ │ ├── README.md │ │ │ ├── cross.sh │ │ │ ├── java/ │ │ │ │ ├── .mvn/ │ │ │ │ │ └── wrapper/ │ │ │ │ │ ├── maven-wrapper.jar │ │ │ │ │ └── maven-wrapper.properties │ │ │ │ ├── build.sh │ │ │ │ ├── mvnw │ │ │ │ ├── mvnw.cmd │ │ │ │ ├── pom.xml │ │ │ │ └── src/ │ │ │ │ ├── main/ │ │ │ │ │ ├── java/ │ │ │ │ │ │ └── org/ │ │ │ │ │ │ └── prql/ │ │ │ │ │ │ └── prql4j/ │ │ │ │ │ │ ├── Environment.java │ │ │ │ │ │ ├── NativeLibraryLoader.java │ │ │ │ │ │ └── PrqlCompiler.java │ │ │ │ │ └── resources/ │ │ │ │ │ └── .gitkeep │ │ │ │ └── test/ │ │ │ │ ├── java/ │ │ │ │ │ └── org/ │ │ │ │ │ └── prql/ │ │ │ │ │ └── prql4j/ │ │ │ │ │ └── PrqlCompilerTest.java │ │ │ │ └── resources/ │ │ │ │ └── .gitkeep │ │ │ └── src/ │ │ │ └── lib.rs │ │ ├── js/ │ │ │ ├── Cargo.toml │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── lib.rs │ │ │ └── tests/ │ │ │ └── test_all.mjs │ │ ├── php/ │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ ├── README.md │ │ │ ├── composer.json │ │ │ ├── phpstan.neon │ │ │ ├── src/ │ │ │ │ ├── Compiler.php │ │ │ │ ├── Message.php │ │ │ │ ├── MessageKind.php │ │ │ │ ├── Options.php │ │ │ │ ├── Result.php │ │ │ │ ├── SourceLocation.php │ │ │ │ └── Span.php │ │ │ └── tests/ │ │ │ └── CompilerTest.php │ │ ├── prqlc-c/ │ │ │ ├── Cargo.toml │ │ │ ├── README.md │ │ │ ├── cbindgen.toml │ │ │ ├── examples/ │ │ │ │ ├── minimal-c/ │ │ │ │ │ ├── Makefile │ │ │ │ │ ├── README.md │ │ │ │ │ └── main.c │ │ │ │ ├── minimal-cpp/ │ │ │ │ │ ├── Makefile │ │ │ │ │ ├── README.md │ │ │ │ │ └── main.cpp │ │ │ │ └── minimal-zig/ │ │ │ │ ├── .gitignore │ │ │ │ ├── README.md │ │ │ │ ├── Taskfile.yaml │ │ │ │ ├── build.zig │ │ │ │ └── src/ │ │ │ │ └── main.zig │ │ │ ├── prqlc.h │ │ │ ├── prqlc.hpp │ │ │ └── src/ │ │ │ └── lib.rs │ │ └── prqlc-python/ │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── Taskfile.yaml │ │ ├── build.rs │ │ ├── noxfile.py │ │ ├── pyproject.toml │ │ ├── python/ │ │ │ ├── prqlc/ │ │ │ │ ├── __init__.py │ │ │ │ ├── __init__.pyi │ │ │ │ ├── debug.pyi │ │ │ │ └── py.typed │ │ │ └── tests/ │ │ │ └── test_all.py │ │ └── src/ │ │ └── lib.rs │ ├── packages/ │ │ └── snap/ │ │ └── snapcraft.yaml │ ├── prqlc/ │ │ ├── ARCHITECTURE.md │ │ ├── Cargo.toml │ │ ├── README.md │ │ ├── benches/ │ │ │ ├── bench.rs │ │ │ └── bench_impl.rs │ │ ├── build.rs │ │ ├── examples/ │ │ │ └── compile-files/ │ │ │ ├── Cargo.toml │ │ │ ├── README.md │ │ │ ├── build.rs │ │ │ ├── queries/ │ │ │ │ ├── arrays.prql │ │ │ │ ├── query1.prql │ │ │ │ └── variables.prql │ │ │ └── src/ │ │ │ └── main.rs │ │ ├── src/ │ │ │ ├── cli/ │ │ │ │ ├── docs_generator.rs │ │ │ │ ├── highlight.rs │ │ │ │ ├── jinja.rs │ │ │ │ ├── lsp.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── snapshots/ │ │ │ │ │ ├── prqlc__cli__test__shell_completion-2.snap │ │ │ │ │ ├── prqlc__cli__test__shell_completion-3.snap │ │ │ │ │ ├── prqlc__cli__test__shell_completion-4.snap │ │ │ │ │ └── prqlc__cli__test__shell_completion.snap │ │ │ │ ├── test.rs │ │ │ │ └── watch.rs │ │ │ ├── codegen/ │ │ │ │ ├── ast.rs │ │ │ │ ├── mod.rs │ │ │ │ └── types.rs │ │ │ ├── debug/ │ │ │ │ ├── log.rs │ │ │ │ ├── messages.rs │ │ │ │ ├── mod.rs │ │ │ │ └── render_html.rs │ │ │ ├── error_message.rs │ │ │ ├── ir/ │ │ │ │ ├── decl.rs │ │ │ │ ├── generic.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── pl/ │ │ │ │ │ ├── expr.rs │ │ │ │ │ ├── extra.rs │ │ │ │ │ ├── fold.rs │ │ │ │ │ ├── lineage.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── stmt.rs │ │ │ │ │ └── utils.rs │ │ │ │ └── rq/ │ │ │ │ ├── expr.rs │ │ │ │ ├── fold.rs │ │ │ │ ├── ids.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── transform.rs │ │ │ │ └── utils.rs │ │ │ ├── lib.rs │ │ │ ├── main.rs │ │ │ ├── parser.rs │ │ │ ├── semantic/ │ │ │ │ ├── ast_expand.rs │ │ │ │ ├── lowering.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── module.rs │ │ │ │ ├── reporting.rs │ │ │ │ ├── resolver/ │ │ │ │ │ ├── expr.rs │ │ │ │ │ ├── flatten.rs │ │ │ │ │ ├── functions.rs │ │ │ │ │ ├── inference.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── names.rs │ │ │ │ │ ├── snapshots/ │ │ │ │ │ │ ├── prqlc__semantic__resolver__test__append_union_different_tables.snap │ │ │ │ │ │ ├── prqlc__semantic__resolver__test__frames_and_names-2.snap │ │ │ │ │ │ ├── prqlc__semantic__resolver__test__frames_and_names-3.snap │ │ │ │ │ │ ├── prqlc__semantic__resolver__test__frames_and_names.snap │ │ │ │ │ │ ├── prqlc__semantic__resolver__test__functions_1.snap │ │ │ │ │ │ ├── prqlc__semantic__resolver__test__functions_nested.snap │ │ │ │ │ │ ├── prqlc__semantic__resolver__test__functions_pipeline-2.snap │ │ │ │ │ │ ├── prqlc__semantic__resolver__test__functions_pipeline.snap │ │ │ │ │ │ ├── prqlc__semantic__resolver__test__named_args.snap │ │ │ │ │ │ ├── prqlc__semantic__resolver__test__variables_1.snap │ │ │ │ │ │ └── prqlc__semantic__resolver__transforms__tests__aggregate_positional_arg-2.snap │ │ │ │ │ ├── static_eval.rs │ │ │ │ │ ├── stmt.rs │ │ │ │ │ ├── transforms.rs │ │ │ │ │ └── types.rs │ │ │ │ └── std.prql │ │ │ ├── sql/ │ │ │ │ ├── dialect.rs │ │ │ │ ├── gen_expr.rs │ │ │ │ ├── gen_projection.rs │ │ │ │ ├── gen_query.rs │ │ │ │ ├── keywords.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── operators.rs │ │ │ │ ├── pq/ │ │ │ │ │ ├── anchor.rs │ │ │ │ │ ├── ast.rs │ │ │ │ │ ├── context.rs │ │ │ │ │ ├── gen_query.rs │ │ │ │ │ ├── mod.rs │ │ │ │ │ ├── positional_mapping.rs │ │ │ │ │ ├── postprocess.rs │ │ │ │ │ └── preprocess.rs │ │ │ │ └── std.sql.prql │ │ │ └── utils/ │ │ │ ├── id_gen.rs │ │ │ ├── mod.rs │ │ │ └── toposort.rs │ │ └── tests/ │ │ ├── CLAUDE.md │ │ └── integration/ │ │ ├── bad_error_messages.rs │ │ ├── data/ │ │ │ └── chinook/ │ │ │ ├── albums.csv │ │ │ ├── artists.csv │ │ │ ├── customers.csv │ │ │ ├── employees.csv │ │ │ ├── genres.csv │ │ │ ├── invoice_items.csv │ │ │ ├── invoices.csv │ │ │ ├── media_types.csv │ │ │ ├── media_types.json │ │ │ ├── playlist_track.csv │ │ │ ├── playlists.csv │ │ │ ├── schema.sql │ │ │ └── tracks.csv │ │ ├── dbs/ │ │ │ ├── README.md │ │ │ ├── docker-compose.yaml │ │ │ ├── dockerfiles/ │ │ │ │ └── glaredb.Dockerfile │ │ │ ├── mod.rs │ │ │ ├── protocol.rs │ │ │ └── runner.rs │ │ ├── error_messages.rs │ │ ├── main.rs │ │ ├── project/ │ │ │ ├── Project.prql │ │ │ └── artists.prql │ │ ├── queries/ │ │ │ ├── aggregation.prql │ │ │ ├── append_select.prql │ │ │ ├── append_select_compute.prql │ │ │ ├── append_select_multiple_with_null.prql │ │ │ ├── append_select_nulls.prql │ │ │ ├── append_select_simple.prql │ │ │ ├── arithmetic.prql │ │ │ ├── cast.prql │ │ │ ├── constants_only.prql │ │ │ ├── date_to_text.prql │ │ │ ├── distinct.prql │ │ │ ├── distinct_on.prql │ │ │ ├── genre_counts.prql │ │ │ ├── group_all.prql │ │ │ ├── group_sort.prql │ │ │ ├── group_sort_derive_select_join.prql │ │ │ ├── group_sort_filter_derive_select_join.prql │ │ │ ├── group_sort_limit_take.prql │ │ │ ├── invoice_totals.prql │ │ │ ├── loop_01.prql │ │ │ ├── math_module.prql │ │ │ ├── pipelines.prql │ │ │ ├── read_csv.prql │ │ │ ├── set_ops_remove.prql │ │ │ ├── sort.prql │ │ │ ├── sort_2.prql │ │ │ ├── sort_3.prql │ │ │ ├── switch.prql │ │ │ ├── take.prql │ │ │ ├── text_module.prql │ │ │ └── window.prql │ │ ├── queries.rs │ │ ├── snapshots/ │ │ │ ├── integration__queries__compile__aggregation.snap │ │ │ ├── integration__queries__compile__append_select.snap │ │ │ ├── integration__queries__compile__append_select_compute.snap │ │ │ ├── integration__queries__compile__append_select_multiple_with_null.snap │ │ │ ├── integration__queries__compile__append_select_nulls.snap │ │ │ ├── integration__queries__compile__append_select_simple.snap │ │ │ ├── integration__queries__compile__arithmetic.snap │ │ │ ├── integration__queries__compile__cast.snap │ │ │ ├── integration__queries__compile__constants_only.snap │ │ │ ├── integration__queries__compile__distinct.snap │ │ │ ├── integration__queries__compile__distinct_on.snap │ │ │ ├── integration__queries__compile__genre_counts.snap │ │ │ ├── integration__queries__compile__group_all.snap │ │ │ ├── integration__queries__compile__group_sort.snap │ │ │ ├── integration__queries__compile__group_sort_derive_select_join.snap │ │ │ ├── integration__queries__compile__group_sort_filter_derive_select_join.snap │ │ │ ├── integration__queries__compile__group_sort_limit_take.snap │ │ │ ├── integration__queries__compile__invoice_totals.snap │ │ │ ├── integration__queries__compile__loop_01.snap │ │ │ ├── integration__queries__compile__math_module.snap │ │ │ ├── integration__queries__compile__pipelines.snap │ │ │ ├── integration__queries__compile__read_csv.snap │ │ │ ├── integration__queries__compile__set_ops_remove.snap │ │ │ ├── integration__queries__compile__sort.snap │ │ │ ├── integration__queries__compile__sort_2.snap │ │ │ ├── integration__queries__compile__sort_3.snap │ │ │ ├── integration__queries__compile__switch.snap │ │ │ ├── integration__queries__compile__take.snap │ │ │ ├── integration__queries__compile__text_module.snap │ │ │ ├── integration__queries__compile__window.snap │ │ │ ├── integration__queries__compileall__aggregation.snap │ │ │ ├── integration__queries__compileall__append_select.snap │ │ │ ├── integration__queries__compileall__append_select_compute.snap │ │ │ ├── integration__queries__compileall__append_select_multiple_with_null.snap │ │ │ ├── integration__queries__compileall__append_select_nulls.snap │ │ │ ├── integration__queries__compileall__append_select_simple.snap │ │ │ ├── integration__queries__compileall__arithmetic.snap │ │ │ ├── integration__queries__compileall__cast.snap │ │ │ ├── integration__queries__compileall__constants_only.snap │ │ │ ├── integration__queries__compileall__distinct.snap │ │ │ ├── integration__queries__compileall__distinct_on.snap │ │ │ ├── integration__queries__compileall__genre_counts.snap │ │ │ ├── integration__queries__compileall__group_all.snap │ │ │ ├── integration__queries__compileall__group_sort.snap │ │ │ ├── integration__queries__compileall__group_sort_derive_select_join.snap │ │ │ ├── integration__queries__compileall__group_sort_filter_derive_select_join.snap │ │ │ ├── integration__queries__compileall__group_sort_limit_take.snap │ │ │ ├── integration__queries__compileall__invoice_totals.snap │ │ │ ├── integration__queries__compileall__loop_01.snap │ │ │ ├── integration__queries__compileall__math_module.snap │ │ │ ├── integration__queries__compileall__pipelines.snap │ │ │ ├── integration__queries__compileall__read_csv.snap │ │ │ ├── integration__queries__compileall__set_ops_remove.snap │ │ │ ├── integration__queries__compileall__sort.snap │ │ │ ├── integration__queries__compileall__sort_2.snap │ │ │ ├── integration__queries__compileall__sort_3.snap │ │ │ ├── integration__queries__compileall__switch.snap │ │ │ ├── integration__queries__compileall__take.snap │ │ │ ├── integration__queries__compileall__text_module.snap │ │ │ ├── integration__queries__compileall__window.snap │ │ │ ├── integration__queries__debug_lineage__aggregation.snap │ │ │ ├── integration__queries__debug_lineage__append_select.snap │ │ │ ├── integration__queries__debug_lineage__append_select_compute.snap │ │ │ ├── integration__queries__debug_lineage__append_select_multiple_with_null.snap │ │ │ ├── integration__queries__debug_lineage__append_select_nulls.snap │ │ │ ├── integration__queries__debug_lineage__append_select_simple.snap │ │ │ ├── integration__queries__debug_lineage__arithmetic.snap │ │ │ ├── integration__queries__debug_lineage__cast.snap │ │ │ ├── integration__queries__debug_lineage__constants_only.snap │ │ │ ├── integration__queries__debug_lineage__date_to_text.snap │ │ │ ├── integration__queries__debug_lineage__distinct.snap │ │ │ ├── integration__queries__debug_lineage__distinct_on.snap │ │ │ ├── integration__queries__debug_lineage__genre_counts.snap │ │ │ ├── integration__queries__debug_lineage__group_all.snap │ │ │ ├── integration__queries__debug_lineage__group_sort.snap │ │ │ ├── integration__queries__debug_lineage__group_sort_derive_select_join.snap │ │ │ ├── integration__queries__debug_lineage__group_sort_filter_derive_select_join.snap │ │ │ ├── integration__queries__debug_lineage__group_sort_limit_take.snap │ │ │ ├── integration__queries__debug_lineage__invoice_totals.snap │ │ │ ├── integration__queries__debug_lineage__loop_01.snap │ │ │ ├── integration__queries__debug_lineage__math_module.snap │ │ │ ├── integration__queries__debug_lineage__pipelines.snap │ │ │ ├── integration__queries__debug_lineage__read_csv.snap │ │ │ ├── integration__queries__debug_lineage__set_ops_remove.snap │ │ │ ├── integration__queries__debug_lineage__sort.snap │ │ │ ├── integration__queries__debug_lineage__sort_2.snap │ │ │ ├── integration__queries__debug_lineage__sort_3.snap │ │ │ ├── integration__queries__debug_lineage__switch.snap │ │ │ ├── integration__queries__debug_lineage__take.snap │ │ │ ├── integration__queries__debug_lineage__text_module.snap │ │ │ ├── integration__queries__debug_lineage__window.snap │ │ │ ├── integration__queries__fmt__aggregation.snap │ │ │ ├── integration__queries__fmt__append_select.snap │ │ │ ├── integration__queries__fmt__append_select_compute.snap │ │ │ ├── integration__queries__fmt__append_select_multiple_with_null.snap │ │ │ ├── integration__queries__fmt__append_select_nulls.snap │ │ │ ├── integration__queries__fmt__append_select_simple.snap │ │ │ ├── integration__queries__fmt__arithmetic.snap │ │ │ ├── integration__queries__fmt__cast.snap │ │ │ ├── integration__queries__fmt__constants_only.snap │ │ │ ├── integration__queries__fmt__date_to_text.snap │ │ │ ├── integration__queries__fmt__distinct.snap │ │ │ ├── integration__queries__fmt__distinct_on.snap │ │ │ ├── integration__queries__fmt__genre_counts.snap │ │ │ ├── integration__queries__fmt__group_all.snap │ │ │ ├── integration__queries__fmt__group_sort.snap │ │ │ ├── integration__queries__fmt__group_sort_derive_select_join.snap │ │ │ ├── integration__queries__fmt__group_sort_filter_derive_select_join.snap │ │ │ ├── integration__queries__fmt__group_sort_limit_take.snap │ │ │ ├── integration__queries__fmt__invoice_totals.snap │ │ │ ├── integration__queries__fmt__loop_01.snap │ │ │ ├── integration__queries__fmt__math_module.snap │ │ │ ├── integration__queries__fmt__pipelines.snap │ │ │ ├── integration__queries__fmt__read_csv.snap │ │ │ ├── integration__queries__fmt__set_ops_remove.snap │ │ │ ├── integration__queries__fmt__sort.snap │ │ │ ├── integration__queries__fmt__sort_2.snap │ │ │ ├── integration__queries__fmt__sort_3.snap │ │ │ ├── integration__queries__fmt__switch.snap │ │ │ ├── integration__queries__fmt__take.snap │ │ │ ├── integration__queries__fmt__text_module.snap │ │ │ ├── integration__queries__fmt__window.snap │ │ │ ├── integration__queries__lex__aggregation.snap │ │ │ ├── integration__queries__lex__append_select.snap │ │ │ ├── integration__queries__lex__append_select_compute.snap │ │ │ ├── integration__queries__lex__append_select_multiple_with_null.snap │ │ │ ├── integration__queries__lex__append_select_nulls.snap │ │ │ ├── integration__queries__lex__append_select_simple.snap │ │ │ ├── integration__queries__lex__arithmetic.snap │ │ │ ├── integration__queries__lex__cast.snap │ │ │ ├── integration__queries__lex__constants_only.snap │ │ │ ├── integration__queries__lex__date_to_text.snap │ │ │ ├── integration__queries__lex__distinct.snap │ │ │ ├── integration__queries__lex__distinct_on.snap │ │ │ ├── integration__queries__lex__genre_counts.snap │ │ │ ├── integration__queries__lex__group_all.snap │ │ │ ├── integration__queries__lex__group_sort.snap │ │ │ ├── integration__queries__lex__group_sort_derive_select_join.snap │ │ │ ├── integration__queries__lex__group_sort_filter_derive_select_join.snap │ │ │ ├── integration__queries__lex__group_sort_limit_take.snap │ │ │ ├── integration__queries__lex__invoice_totals.snap │ │ │ ├── integration__queries__lex__loop_01.snap │ │ │ ├── integration__queries__lex__math_module.snap │ │ │ ├── integration__queries__lex__pipelines.snap │ │ │ ├── integration__queries__lex__read_csv.snap │ │ │ ├── integration__queries__lex__set_ops_remove.snap │ │ │ ├── integration__queries__lex__sort.snap │ │ │ ├── integration__queries__lex__sort_2.snap │ │ │ ├── integration__queries__lex__sort_3.snap │ │ │ ├── integration__queries__lex__switch.snap │ │ │ ├── integration__queries__lex__take.snap │ │ │ ├── integration__queries__lex__text_module.snap │ │ │ ├── integration__queries__lex__window.snap │ │ │ ├── integration__queries__results__aggregation.snap │ │ │ ├── integration__queries__results__append_select.snap │ │ │ ├── integration__queries__results__append_select_compute.snap │ │ │ ├── integration__queries__results__append_select_multiple_with_null.snap │ │ │ ├── integration__queries__results__append_select_nulls.snap │ │ │ ├── integration__queries__results__append_select_simple.snap │ │ │ ├── integration__queries__results__arithmetic.snap │ │ │ ├── integration__queries__results__cast.snap │ │ │ ├── integration__queries__results__constants_only.snap │ │ │ ├── integration__queries__results__date_to_text.snap │ │ │ ├── integration__queries__results__distinct.snap │ │ │ ├── integration__queries__results__distinct_on.snap │ │ │ ├── integration__queries__results__genre_counts.snap │ │ │ ├── integration__queries__results__group_all.snap │ │ │ ├── integration__queries__results__group_sort.snap │ │ │ ├── integration__queries__results__group_sort_derive_select_join.snap │ │ │ ├── integration__queries__results__group_sort_filter_derive_select_join.snap │ │ │ ├── integration__queries__results__group_sort_limit_take.snap │ │ │ ├── integration__queries__results__invoice_totals.snap │ │ │ ├── integration__queries__results__loop_01.snap │ │ │ ├── integration__queries__results__math_module.snap │ │ │ ├── integration__queries__results__pipelines.snap │ │ │ ├── integration__queries__results__read_csv.snap │ │ │ ├── integration__queries__results__set_ops_remove.snap │ │ │ ├── integration__queries__results__sort.snap │ │ │ ├── integration__queries__results__sort_2.snap │ │ │ ├── integration__queries__results__sort_3.snap │ │ │ ├── integration__queries__results__switch.snap │ │ │ ├── integration__queries__results__take.snap │ │ │ ├── integration__queries__results__text_module.snap │ │ │ └── integration__queries__results__window.snap │ │ └── sql.rs │ ├── prqlc-macros/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── lib.rs │ └── prqlc-parser/ │ ├── Cargo.toml │ └── src/ │ ├── error.rs │ ├── generic.rs │ ├── lexer/ │ │ ├── lr.rs │ │ ├── mod.rs │ │ └── test.rs │ ├── lib.rs │ ├── parser/ │ │ ├── expr.rs │ │ ├── interpolation.rs │ │ ├── mod.rs │ │ ├── perror.rs │ │ ├── pr/ │ │ │ ├── expr.rs │ │ │ ├── ident.rs │ │ │ ├── mod.rs │ │ │ ├── ops.rs │ │ │ ├── stmt.rs │ │ │ └── types.rs │ │ ├── stmt.rs │ │ ├── test.rs │ │ └── types.rs │ ├── snapshots/ │ │ └── prqlc_parser__test__pipeline_parse_tree.snap │ ├── span.rs │ └── test.rs ├── rust-toolchain.toml └── web/ ├── .gitignore ├── Taskfile.yaml ├── book/ │ ├── Cargo.toml │ ├── README.md │ ├── book.toml │ ├── comparison-table.css │ ├── highlight-prql.js │ ├── src/ │ │ ├── README.md │ │ ├── SUMMARY.md │ │ ├── lib.rs │ │ ├── main.rs │ │ ├── project/ │ │ │ ├── bindings/ │ │ │ │ ├── README.md │ │ │ │ ├── dotnet.md │ │ │ │ ├── elixir.md │ │ │ │ ├── java.md │ │ │ │ ├── javascript.md │ │ │ │ ├── php.md │ │ │ │ ├── python.md │ │ │ │ ├── r.md │ │ │ │ └── rust.md │ │ │ ├── changelog.md │ │ │ ├── contributing/ │ │ │ │ ├── README.md │ │ │ │ ├── development.md │ │ │ │ ├── fortnightly-dev-call.ics │ │ │ │ └── language-design.md │ │ │ ├── integrations/ │ │ │ │ ├── README.md │ │ │ │ ├── clickhouse.md │ │ │ │ ├── databend.md │ │ │ │ ├── duckdb.md │ │ │ │ ├── jupyter.md │ │ │ │ ├── postgresql.md │ │ │ │ ├── prefect.md │ │ │ │ ├── prqlc-cli.md │ │ │ │ ├── qstudio.md │ │ │ │ ├── rill.md │ │ │ │ ├── syntax-highlighting.md │ │ │ │ └── vscode.md │ │ │ └── target.md │ │ ├── reference/ │ │ │ ├── data/ │ │ │ │ ├── README.md │ │ │ │ ├── from.md │ │ │ │ ├── read-files.md │ │ │ │ └── relation-literals.md │ │ │ ├── declarations/ │ │ │ │ ├── README.md │ │ │ │ ├── functions.md │ │ │ │ └── variables.md │ │ │ ├── spec/ │ │ │ │ ├── README.md │ │ │ │ ├── modules.md │ │ │ │ ├── name-resolution.md │ │ │ │ ├── null.md │ │ │ │ └── type-system.md │ │ │ ├── stdlib/ │ │ │ │ ├── README.md │ │ │ │ ├── date.md │ │ │ │ ├── distinct.md │ │ │ │ ├── math.md │ │ │ │ ├── text.md │ │ │ │ └── transforms/ │ │ │ │ ├── README.md │ │ │ │ ├── aggregate.md │ │ │ │ ├── append.md │ │ │ │ ├── derive.md │ │ │ │ ├── filter.md │ │ │ │ ├── group.md │ │ │ │ ├── join.md │ │ │ │ ├── loop.md │ │ │ │ ├── select.md │ │ │ │ ├── sort.md │ │ │ │ ├── take.md │ │ │ │ └── window.md │ │ │ └── syntax/ │ │ │ ├── README.md │ │ │ ├── arrays.md │ │ │ ├── case.md │ │ │ ├── comments.md │ │ │ ├── f-strings.md │ │ │ ├── function-calls.md │ │ │ ├── keywords.md │ │ │ ├── literals.md │ │ │ ├── operators.md │ │ │ ├── parameters.md │ │ │ ├── pipes.md │ │ │ ├── r-strings.md │ │ │ ├── ranges.md │ │ │ ├── s-strings.md │ │ │ ├── strings.md │ │ │ └── tuples.md │ │ └── tutorial/ │ │ ├── aggregation.md │ │ ├── annotated_example.md │ │ ├── filtering.md │ │ └── relations.md │ ├── tests/ │ │ └── documentation/ │ │ ├── README.md │ │ ├── book.rs │ │ ├── main.rs │ │ ├── readme.rs │ │ ├── snapshots/ │ │ │ ├── documentation__book__README__prql-language-book__0.snap │ │ │ ├── documentation__book__README__prql-language-book__1.snap │ │ │ ├── documentation__book__project__target__examples__0.snap │ │ │ ├── documentation__book__project__target__examples__1.snap │ │ │ ├── documentation__book__project__target__version__0.snap │ │ │ ├── documentation__book__project__target__version__1.snap │ │ │ ├── documentation__book__reference__data__from__0.snap │ │ │ ├── documentation__book__reference__data__from__1.snap │ │ │ ├── documentation__book__reference__data__from__2.snap │ │ │ ├── documentation__book__reference__data__from__3.snap │ │ │ ├── documentation__book__reference__data__read-files__reading-files__0.snap │ │ │ ├── documentation__book__reference__data__read-files__reading-files__1.snap │ │ │ ├── documentation__book__reference__data__relation-literals__array-literals__0.snap │ │ │ ├── documentation__book__reference__data__relation-literals__array-literals__1.snap │ │ │ ├── documentation__book__reference__data__relation-literals__array-literals__2.snap │ │ │ ├── documentation__book__reference__data__relation-literals__array-literals__3.snap │ │ │ ├── documentation__book__reference__data__relation-literals__array-literals__4.snap │ │ │ ├── documentation__book__reference__declarations__functions__0.snap │ │ │ ├── documentation__book__reference__declarations__functions__1.snap │ │ │ ├── documentation__book__reference__declarations__functions__late-binding__0.snap │ │ │ ├── documentation__book__reference__declarations__functions__other-examples__0.snap │ │ │ ├── documentation__book__reference__declarations__functions__partial-application__0.snap │ │ │ ├── documentation__book__reference__declarations__functions__partial-application__1.snap │ │ │ ├── documentation__book__reference__declarations__functions__partial-application__2.snap │ │ │ ├── documentation__book__reference__declarations__functions__piping-values-into-functions__0.snap │ │ │ ├── documentation__book__reference__declarations__functions__piping-values-into-functions__1.snap │ │ │ ├── documentation__book__reference__declarations__functions__piping-values-into-functions__2.snap │ │ │ ├── documentation__book__reference__declarations__variables__variables--__0.snap │ │ │ ├── documentation__book__reference__declarations__variables__variables--__1.snap │ │ │ ├── documentation__book__reference__declarations__variables__variables--__2.snap │ │ │ ├── documentation__book__reference__spec__name-resolution__translating-to-sql__0.snap │ │ │ ├── documentation__book__reference__spec__name-resolution__translating-to-sql__1.snap │ │ │ ├── documentation__book__reference__spec__null__null-handling__0.snap │ │ │ ├── documentation__book__reference__stdlib__README__standard-library__0.snap │ │ │ ├── documentation__book__reference__stdlib__README__standard-library__1.snap │ │ │ ├── documentation__book__reference__stdlib__README__standard-library__2.snap │ │ │ ├── documentation__book__reference__stdlib__date__date-functions__0.snap │ │ │ ├── documentation__book__reference__stdlib__date__date-functions__1.snap │ │ │ ├── documentation__book__reference__stdlib__date__date-functions__2.snap │ │ │ ├── documentation__book__reference__stdlib__distinct__how-do-i-remove-duplicates__0.snap │ │ │ ├── documentation__book__reference__stdlib__distinct__how-do-i-remove-duplicates__1.snap │ │ │ ├── documentation__book__reference__stdlib__distinct__remove-duplicates-from-each-group__0.snap │ │ │ ├── documentation__book__reference__stdlib__distinct__remove-duplicates-from-each-group__1.snap │ │ │ ├── documentation__book__reference__stdlib__math__example__0.snap │ │ │ ├── documentation__book__reference__stdlib__text__example__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__aggregate__aggregate-is-required__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__aggregate__examples__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__aggregate__examples__1.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__append__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__append__intersection__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__append__remove__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__derive__examples__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__derive__examples__1.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__filter__examples__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__filter__examples__1.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__filter__examples__2.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__filter__examples__3.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__group__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__group__1.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__group__2.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__join__examples__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__join__examples__1.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__join__examples__2.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__join__examples__3.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__join__examples__4.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__loop__examples__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__select__examples__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__select__examples__1.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__select__examples__2.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__select__examples__3.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__select__excluding-columns__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__select__excluding-columns__1.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__select__excluding-columns__2.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__select__excluding-columns__3.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__sort__examples__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__sort__examples__1.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__sort__examples__2.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__sort__examples__3.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__sort__ordering-guarantees__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__take__examples__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__take__examples__1.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__window__example__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__window__example__1.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__window__example__2.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__window__window-functions-as-first-class-citizens__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__window__windowing-by-default__0.snap │ │ │ ├── documentation__book__reference__stdlib__transforms__window__windowing-by-default__1.snap │ │ │ ├── documentation__book__reference__syntax__case__0.snap │ │ │ ├── documentation__book__reference__syntax__case__1.snap │ │ │ ├── documentation__book__reference__syntax__comments__0.snap │ │ │ ├── documentation__book__reference__syntax__f-strings__0.snap │ │ │ ├── documentation__book__reference__syntax__f-strings__1.snap │ │ │ ├── documentation__book__reference__syntax__f-strings__2.snap │ │ │ ├── documentation__book__reference__syntax__keywords__0.snap │ │ │ ├── documentation__book__reference__syntax__keywords__identifiers--keywords__0.snap │ │ │ ├── documentation__book__reference__syntax__keywords__identifiers--keywords__1.snap │ │ │ ├── documentation__book__reference__syntax__keywords__identifiers--keywords__2.snap │ │ │ ├── documentation__book__reference__syntax__keywords__identifiers--keywords__3.snap │ │ │ ├── documentation__book__reference__syntax__keywords__quoting__0.snap │ │ │ ├── documentation__book__reference__syntax__keywords__quoting__1.snap │ │ │ ├── documentation__book__reference__syntax__keywords__quoting__2.snap │ │ │ ├── documentation__book__reference__syntax__keywords__schemas--database-names__0.snap │ │ │ ├── documentation__book__reference__syntax__literals__dates__0.snap │ │ │ ├── documentation__book__reference__syntax__literals__durations__0.snap │ │ │ ├── documentation__book__reference__syntax__literals__numbers__0.snap │ │ │ ├── documentation__book__reference__syntax__literals__times__0.snap │ │ │ ├── documentation__book__reference__syntax__literals__timestamps__0.snap │ │ │ ├── documentation__book__reference__syntax__operators__0.snap │ │ │ ├── documentation__book__reference__syntax__operators__coalesce__0.snap │ │ │ ├── documentation__book__reference__syntax__operators__division-and-integer-division__0.snap │ │ │ ├── documentation__book__reference__syntax__operators__parentheses__0.snap │ │ │ ├── documentation__book__reference__syntax__operators__parentheses__1.snap │ │ │ ├── documentation__book__reference__syntax__operators__parentheses__2.snap │ │ │ ├── documentation__book__reference__syntax__operators__regex-expressions__0.snap │ │ │ ├── documentation__book__reference__syntax__operators__regex-expressions__1.snap │ │ │ ├── documentation__book__reference__syntax__operators__regex-expressions__2.snap │ │ │ ├── documentation__book__reference__syntax__operators__regex-expressions__3.snap │ │ │ ├── documentation__book__reference__syntax__operators__regex-expressions__4.snap │ │ │ ├── documentation__book__reference__syntax__operators__regex-expressions__5.snap │ │ │ ├── documentation__book__reference__syntax__operators__wrapping-lines__0.snap │ │ │ ├── documentation__book__reference__syntax__operators__wrapping-lines__1.snap │ │ │ ├── documentation__book__reference__syntax__parameters__0.snap │ │ │ ├── documentation__book__reference__syntax__pipes__0.snap │ │ │ ├── documentation__book__reference__syntax__pipes__1.snap │ │ │ ├── documentation__book__reference__syntax__pipes__ceci-nest-pas-une-pipe__0.snap │ │ │ ├── documentation__book__reference__syntax__pipes__ceci-nest-pas-une-pipe__1.snap │ │ │ ├── documentation__book__reference__syntax__pipes__inner-transforms__0.snap │ │ │ ├── documentation__book__reference__syntax__r-strings__0.snap │ │ │ ├── documentation__book__reference__syntax__ranges__0.snap │ │ │ ├── documentation__book__reference__syntax__ranges__1.snap │ │ │ ├── documentation__book__reference__syntax__s-strings__0.snap │ │ │ ├── documentation__book__reference__syntax__s-strings__1.snap │ │ │ ├── documentation__book__reference__syntax__s-strings__2.snap │ │ │ ├── documentation__book__reference__syntax__s-strings__3.snap │ │ │ ├── documentation__book__reference__syntax__s-strings__braces__0.snap │ │ │ ├── documentation__book__reference__syntax__s-strings__precedence-within-s-strings__0.snap │ │ │ ├── documentation__book__reference__syntax__s-strings__precedence-within-s-strings__1.snap │ │ │ ├── documentation__book__reference__syntax__strings__0.snap │ │ │ ├── documentation__book__reference__syntax__strings__quoting-and-escape-characters__0.snap │ │ │ ├── documentation__book__reference__syntax__strings__quoting-and-escape-characters__1.snap │ │ │ ├── documentation__book__reference__syntax__tuples__0.snap │ │ │ └── documentation__book__reference__syntax__tuples__1.snap │ │ └── website.rs │ └── theme/ │ ├── head.hbs │ └── highlight.js ├── playground/ │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── generateBook.cjs │ ├── index.html │ ├── package.json │ ├── public/ │ │ ├── manifest.json │ │ └── robots.txt │ ├── src/ │ │ ├── app/ │ │ │ ├── App.css │ │ │ └── App.jsx │ │ ├── examples.js │ │ ├── highlight.css │ │ ├── index.css │ │ ├── main.jsx │ │ ├── output/ │ │ │ ├── Output.css │ │ │ └── Output.jsx │ │ ├── reportWebVitals.js │ │ ├── setupTests.js │ │ ├── sidebar/ │ │ │ ├── Sidebar.css │ │ │ └── Sidebar.jsx │ │ └── workbench/ │ │ ├── Workbench.css │ │ ├── Workbench.jsx │ │ ├── duckdb.js │ │ ├── monaco-theme.json │ │ └── prql-syntax.js │ └── vite.config.js ├── prql-codemirror-demo/ │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── codemirror.ts │ │ ├── lang-prql/ │ │ │ ├── complete.ts │ │ │ └── prql.ts │ │ ├── main.ts │ │ ├── style.css │ │ └── vite-env.d.ts │ └── tsconfig.json └── website/ ├── .gitignore ├── README.md ├── archetypes/ │ └── default.md ├── config.yaml ├── content/ │ ├── _index.md │ ├── demos/ │ │ └── ace-demo.html │ ├── faq.md │ ├── playground.html │ ├── posts/ │ │ ├── 2022-05-19-examples.md │ │ ├── 2023-01-07-functional-relations.md │ │ ├── 2023-01-27-prql-query.md │ │ ├── 2023-01-28-format-pretty-reports/ │ │ │ └── _index.md │ │ ├── 2023-02-02-one-year/ │ │ │ └── _index.md │ │ └── 2023-03-14-pi-day.md │ └── roadmap.md ├── data/ │ ├── examples/ │ │ ├── basic.yaml │ │ ├── dialects.yaml │ │ ├── expressions.yaml │ │ ├── f-strings.yaml │ │ ├── friendly-syntax.yaml │ │ ├── functions.yaml │ │ ├── hero.yaml │ │ ├── joins.yaml │ │ ├── null-handling.yaml │ │ ├── orthogonal.yaml │ │ ├── s-strings.yaml │ │ ├── top-n.yaml │ │ └── windows.yaml │ └── testimonials.yaml ├── static/ │ └── CNAME └── themes/ └── prql-theme/ ├── archetypes/ │ └── default.md ├── layouts/ │ ├── 404.html │ ├── _default/ │ │ ├── _markup/ │ │ │ └── render-link.html │ │ ├── article.html │ │ ├── baseof.html │ │ ├── big_iframe.html │ │ ├── home.html │ │ ├── list.html │ │ └── single.html │ ├── partials/ │ │ ├── footer.html │ │ ├── head.html │ │ ├── header.html │ │ ├── section-cards.html │ │ └── section-testimonials.html │ └── shortcodes/ │ ├── cite.html │ ├── columns.html │ ├── faq.html │ └── link.html ├── static/ │ ├── main.js │ ├── plugins/ │ │ └── highlight/ │ │ ├── highlight.css │ │ └── prql.js │ └── style.css └── theme.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cargo/config.toml ================================================ [target.wasm32-unknown-unknown] runner = 'wasm-bindgen-test-runner' [target.x86_64-pc-windows-msvc] # https://github.com/rust-lang/rust/issues/141626#issuecomment-2919988483 linker = "rust-lld" [resolver] incompatible-rust-versions = "fallback" ================================================ FILE: .config/insta.yaml ================================================ behavior: # Disabling because of issues with running on Windows # force_update: true review: # The default (true) has a small performance hit and never needed for us warn_undiscovered: false include_ignored: false include_hidden: false ================================================ FILE: .config/lychee.toml ================================================ # Lychee link checker configuration # See: https://github.com/lycheeverse/lychee # Retry configuration for network errors max_retries = 5 retry_wait_time = 30 # seconds between retries # Timeout configuration timeout = 20 # seconds # Maximum redirects to follow max_redirects = 5 # Accept these status codes as valid accept = [200, 206, 429] # Exclude patterns - URLs to ignore exclude = [ # Sites that block GitHub Actions or have reliability issues "^https://twitter\\.com", "^https://www\\.cs\\.ox\\.ac\\.uk", "^https://www\\.php\\.net", "^https://github\\.com/.*container", "^https://benn\\.substack\\.com", "^https://stackoverflow\\.com", "^https://www\\.npmjs\\.com", "^https://invent\\.kde\\.org", "^https://news\\.ycombinator\\.com", "^https://repology\\.org", ] # Use cache to reduce rate limiting issues cache = true max_cache_age = "1d" ================================================ FILE: .config/nextest.toml ================================================ [profile.default] fail-fast = false failure-output = "final" slow-timeout = { period = "500ms" } [[profile.default.overrides]] # compileall does many passes over the same query, so it takes a bit longer filter = 'package(prqlc) & test(queries::)' slow-timeout = { period = "2s" } [[profile.default.overrides]] # compileall does many passes over the same query, so it takes a bit longer filter = 'package(prqlc) & test(queries::compileall::)' slow-timeout = { period = "10s" } [[profile.default.overrides]] filter = 'package(prqlc) & test(queries::results::)' slow-timeout = { period = "10s" } test-group = 'test-dbs' [test-groups.test-dbs] # test-dbs runs database setup when the connection is established, and because # nextest runs test in separate processes, this happens on every test. To # prevent multiple setups running at once, we set max-threads to 1. Ideally, we # could run tests in parallel and they would use a locking mechanism to see if # the database has already been setup. For now, we can use cargo test instead. max-threads = 1 [[profile.default.overrides]] # cli tests take a bit longer filter = 'test(cli)' slow-timeout = { period = "2s" } [[profile.default.overrides]] filter = 'package(mdbook-prql)' # These are testing dozens of queries, so we give them more time. slow-timeout = { period = "20s" } test-group = 'docs-mdbook' [test-groups.docs-mdbook] max-threads = 'num-cpus' ================================================ FILE: .config/vscode-recommended/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "lldb", "request": "launch", "name": "prqlc _a.prql", "program": "${workspaceFolder}/target/debug/prqlc", "args": ["debug", "semantics", "_a.prql"], "env": { "RUST_LOG": "debug", "RUST_BACKTRACE": "1" }, "preLaunchTask": "prqlc-build", "cwd": "${workspaceFolder}" } ] } ================================================ FILE: .config/vscode-recommended/settings.json ================================================ { "files.exclude": { "**/.git": true, "**/.svn": true, "**/.hg": true, "**/CVS": true, "**/.DS_Store": true, "**/Thumbs.db": true, "web/book/book": true, "web/build": true, ".direnv": true, ".pytest_cache": true, ".mypy_cache": true } } ================================================ FILE: .config/vscode-recommended/tasks.json ================================================ { "version": "2.0.0", "tasks": [ { "label": "prqlc-build", "type": "cargo", "command": "build", "args": ["--package=prqlc"], "presentation": { "reveal": "silent", "clear": true }, "problemMatcher": ["$rustc"] } ] } ================================================ FILE: .config/wt.toml ================================================ # Worktrunk configuration for PRQL # # Available template variables: # {{ repo }} - Repository name # {{ branch }} - Branch name (slashes replaced with dashes) # {{ worktree }} - Path to the new worktree # {{ repo_root }} - Path to the main repository root # Post-start commands run in parallel after switching to a worktree [post-start] # Seed compiled dependencies from main worktree using CoW (copy-on-write) # Copies essential files, skipping incremental build objects # Result: warm-start builds instead of cold compiles # macOS: uses cp -c (clonefile on APFS) deps = """ [ -d {{ repo_path }}/target/debug/deps ] && [ ! -e target ] && mkdir -p target/debug/deps && cp -c {{ repo_path }}/target/debug/deps/*.rlib {{ repo_path }}/target/debug/deps/*.rmeta target/debug/deps/ && cp -cR {{ repo_path }}/target/debug/.fingerprint {{ repo_path }}/target/debug/build target/debug/ """ ================================================ FILE: .devcontainer/base-image/Dockerfile ================================================ # syntax=docker/dockerfile:1.4 FROM mcr.microsoft.com/devcontainers/rust:2-1-bookworm # ========= Install cargo-tools for non-root user (vscode) ========= USER vscode ARG cargo_crates RUN <<"EOF" curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash cargo binstall -y --locked ${cargo_crates} EOF USER root ================================================ FILE: .devcontainer/devcontainer.json ================================================ // Dev Container for Rust, website, prqlc-js and prqlc-python { "image": "ghcr.io/prql/prql-devcontainer-base:latest", "features": { "ghcr.io/devcontainers/features/hugo:1": {}, "ghcr.io/devcontainers/features/python:1": {}, "ghcr.io/devcontainers/features/node:1": {}, "ghcr.io/eitsupi/devcontainer-features/go-task:1": {}, "ghcr.io/eitsupi/devcontainer-features/jq-likes:2": { "yqVersion": "latest" }, "ghcr.io/eitsupi/devcontainer-features/duckdb-cli:1": {}, "ghcr.io/rocker-org/devcontainer-features/apt-packages:1": { "packages": "cmake,sqlite3" } }, "customizations": { "vscode": { "extensions": [ // Keep in sync with Taskfile.yaml "prql-lang.prql-vscode", "rust-lang.rust-analyzer", "mitsuhiko.insta", "esbenp.prettier-vscode", "budparr.language-hugo-vscode" ] } }, "mounts": [ { "source": "devcontainer-cargo-cache-${devcontainerId}", "target": "/usr/local/cargo/registry", "type": "volume" }, { "source": "devcontainer-cargo-target-${devcontainerId}", "target": "${containerWorkspaceFolder}/target", "type": "volume" } ], "postCreateCommand": { "set-ownership": "sudo chown vscode target /usr/local/cargo/registry/", "install-python-deps": "task install-maturin", // Disabling because of the issues in #3709 // "install-python-deps": "task install-maturin && task install-pre-commit && pre-commit install-hooks", "install-npm-dependencies": "task install-npm-dependencies" } } ================================================ FILE: .gitattributes ================================================ # Currently required for: # - Elixir tests at # https://github.com/PRQL/prql/blob/5eb4063dbd36bdac07529797dd2cec8c55127263/.github/workflows/test-elixir.yaml#L31 # - Previously for Readme tests, those those now should support either lf or # crlf, at https://github.com/PRQL/prql/blob/6d4662b4e6e0f07dc3cd8eb5c19cc78fc199d0b2/web/book/tests/documentation/readme.rs#L9 * text=auto eol=lf # Prevent files from cluttering `git grep` results *.min.js -diff *.min.js.map -diff *.min.css -diff *.min.css.map -diff package-lock.json -diff # mdbook expects the file to be named highlight.js so we cannot rename it to .min.js highlight.js -diff ================================================ FILE: .github/.codecov.yaml ================================================ comment: false ignore: - "**/tests/**" coverage: status: project: default: removed_code_behavior: adjust_base # This disables report a success/failure. That's not helpful on `main` # and we get the success/failure from the patch status on PRs. informational: true patch: default: only_pulls: true ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Code of Conduct We prefer keeping our rules as short as possible and filling the gaps with the mortar of human interaction: empathy. All we ask of members of this project is this: - Please treat each other with respect and understanding. - Please respect our wish to not serve as a stage for disputes about fairness or personal differences. If you can agree to these conditions, your contributions are welcome. If you can not, please don’t spoil it for the rest of us. To report a violation of these rules, please email [prql@prql-lang.org](mailto:prql@prql-lang.org) or contact one of the [project maintainers](https://github.com/orgs/PRQL/people). --- (based on ) ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing Check out our [Contributing Docs](https://prql-lang.org/book/project/contributing/) ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yaml ================================================ name: 🐛 Bug report description: File a bug report to help us improve labels: [bug] body: - type: textarea id: what-happened attributes: label: What happened? description: | Thanks for reporting a bug! Feel free to add any initial context here. - type: textarea id: prql-input attributes: label: PRQL input description: | A minimal, self-contained example that demonstrates the issue. This will be automatically formatted into code, so no need for markdown backticks. render: elm validations: required: true - type: textarea id: output attributes: label: SQL output description: | The SQL that PRQL currently compiles to. Feel free to use the [playground](https://prql-lang.org/playground/) to generate the SQL. This will be automatically formatted into code, so no need for markdown backticks. render: SQL validations: required: true - type: textarea id: expected-output attributes: label: Expected SQL output description: Optional; no need to write out if it's obvious from the context render: SQL - type: checkboxes id: mvce-checkboxes attributes: label: MVCE confirmation description: | Please confirm that the bug report is minimal and doesn't exist already: - **Minimal example** — the example is as focused as reasonably possible to demonstrate the underlying issue in PRQL. For example, it's not possible to exclude any line and still observe the bug. - **New issue** — a search of GitHub Issues suggests this is not a duplicate. options: - label: Minimal example - label: New issue - type: textarea id: extra attributes: label: Anything else? ================================================ FILE: .github/ISSUE_TEMPLATE/config.yaml ================================================ blank_issues_enabled: true ================================================ FILE: .github/ISSUE_TEMPLATE/something_else.yaml ================================================ name: Something else description: Anything that's not a bug report body: - type: textarea id: what-happened attributes: label: What's up? ================================================ FILE: .github/actionlint.yaml ================================================ # Custom runner labels not yet recognized by actionlint self-hosted-runner: labels: - macos-15-intel ================================================ FILE: .github/actions/build-prqlc/action.yaml ================================================ name: build-prqlc description: > Build prqlc Note that much of this is copy/pasted into build-prqlc-c, so changes here should generally be copied into that file. inputs: target: description: Build target required: true profile: description: Build profile option; `dev` or `release`. required: true features: description: Features to enable default: "" outputs: artifact-name: description: The name of the artifact value: ${{ steps.echo-artifact-name.outputs.artifact-name }} runs: using: composite steps: - run: rustup target add ${{ inputs.target }} shell: bash - run: ./.github/workflows/scripts/set_version.sh shell: bash - name: Compute Cargo.lock hash shell: bash run: | if command -v sha256sum &> /dev/null; then echo "cargo_lock_hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_ENV" else echo "cargo_lock_hash=$(shasum -a 256 Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_ENV" fi - uses: Swatinem/rust-cache@v2 with: prefix-key: ${{ env.version }}-${{ env.cargo_lock_hash }} # Share cache with `test-rust`, except for `musl` targets. save-if: ${{ (github.ref == 'refs/heads/main') && contains(inputs.target, 'musl') }} shared-key: rust-${{ inputs.target }} - if: runner.os == 'Linux' shell: bash run: | sudo apt-get update sudo apt-get install -y musl-tools - if: runner.os == 'Windows' && inputs.profile == 'release' shell: bash run: echo 'RUSTFLAGS=-Ctarget-feature=+crt-static' >>"$GITHUB_ENV" - if: inputs.target == 'aarch64-unknown-linux-musl' shell: bash run: | sudo apt-get update sudo apt-get install -y gcc-aarch64-linux-gnu echo 'CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-gnu-gcc' >>"$GITHUB_ENV" echo 'CC=aarch64-linux-gnu-gcc' >>"$GITHUB_ENV" - name: cargo build uses: clechasseur/rs-cargo@v4 with: command: build # We previously had `--package=prqlc` for all, but this caches much # worse than just building the whole workspace. # https://github.com/PRQL/prql/issues/3098, so we just build the whole # workspace — except for the `musl` target, which can't build the whole # workspace. (This is overly complicated and would be great to simplify, # even at the cost of slighly less efficiency.) args: --profile=${{ inputs.profile }} --locked --target=${{ inputs.target }} --no-default-features --features=${{ inputs.features }} ${{ contains(inputs.target, 'musl') && '--package=prqlc' || '--all-targets' }} - name: Create artifact for Linux and macOS shell: bash if: runner.os != 'Windows' run: | export ARTIFACT_NAME="prqlc-${{ github.ref_type == 'tag' && github.ref_name || 0 }}-${{ matrix.target }}.tar.gz" echo "ARTIFACT_NAME=${ARTIFACT_NAME}" >>"$GITHUB_ENV" TEMP_DIR=$(mktemp -d) cp prqlc/prqlc/README.md LICENSE "${TEMP_DIR}/" cp -r target/${{ matrix.target }}/${{ inputs.profile == 'release' && 'release' || 'debug' }}/prqlc "${TEMP_DIR}/" tar czf "${ARTIFACT_NAME}" -C "$TEMP_DIR" . - name: Create artifact for Windows shell: bash if: runner.os == 'Windows' run: | export ARTIFACT_NAME="prqlc-${{ github.ref_type == 'tag' && github.ref_name || 0 }}-${{ matrix.target }}.zip" echo "ARTIFACT_NAME=${ARTIFACT_NAME}" >>"$GITHUB_ENV" cd target/${{ matrix.target }}/${{ inputs.profile == 'release' && 'release' || 'debug' }} cp ../../../prqlc/prqlc/README.md . 7z a "../../../${ARTIFACT_NAME}" prqlc.exe ../../../LICENSE README.md - name: Upload prqlc uses: actions/upload-artifact@v5 with: name: prqlc-${{ inputs.target }}-${{ inputs.profile }} path: ${{ env.ARTIFACT_NAME }} - id: echo-artifact-name shell: bash run: echo "artifact-name=${{ env.ARTIFACT_NAME }}" >>"$GITHUB_OUTPUT" ================================================ FILE: .github/actions/build-prqlc-c/action.yaml ================================================ name: build-prqlc-c description: > A version of `build-prqlc` for the C bindings. Note that this is quite open to change, including names and which files we package. Contributions and/or suggestions are welcome. inputs: target: description: Build target required: true profile: description: Build profile option; `dev` or `release`. required: true features: description: Features to enable default: "" outputs: artifact-name: description: The name of the artifact value: ${{ steps.echo-artifact-name.outputs.artifact-name }} runs: using: composite steps: - run: rustup target add ${{ inputs.target }} shell: bash - run: ./.github/workflows/scripts/set_version.sh shell: bash - name: Compute Cargo.lock hash shell: bash run: | if command -v sha256sum &> /dev/null; then echo "cargo_lock_hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_ENV" else echo "cargo_lock_hash=$(shasum -a 256 Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_ENV" fi - uses: Swatinem/rust-cache@v2 with: prefix-key: ${{ env.version }}-${{ env.cargo_lock_hash }} # Share cache with `test-rust`, except for `musl` targets. save-if: ${{ (github.ref == 'refs/heads/main') && contains(inputs.target, 'musl') }} shared-key: rust-${{ inputs.target }} - if: runner.os == 'Linux' shell: bash run: | sudo apt-get update sudo apt-get install -y musl-tools - if: runner.os == 'Windows' && inputs.profile == 'release' shell: bash run: echo 'RUSTFLAGS=-Ctarget-feature=+crt-static' >>"$GITHUB_ENV" - if: inputs.target == 'aarch64-unknown-linux-musl' shell: bash run: | sudo apt-get update sudo apt-get install -y gcc-aarch64-linux-gnu echo 'CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-gnu-gcc' >>"$GITHUB_ENV" echo 'CC=aarch64-linux-gnu-gcc' >>"$GITHUB_ENV" - name: cargo build uses: clechasseur/rs-cargo@v4 with: command: build args: --profile=${{ inputs.profile }} --locked --target=${{ inputs.target }} --no-default-features --features=${{ inputs.features }} ${{ contains(inputs.target, 'musl') && '--package=prqlc-c' || '--all-targets' }} - name: Create artifact for Linux and macOS shell: bash if: runner.os != 'Windows' run: | export ARTIFACT_NAME="prqlc_c-${{ github.ref_type == 'tag' && github.ref_name || 0 }}-${{ matrix.target }}.tar.gz" echo "ARTIFACT_NAME=${ARTIFACT_NAME}" >>"$GITHUB_ENV" cd target/${{ matrix.target }}/${{ inputs.profile == 'release' && 'release' || 'debug' }} ls -al tar czf "../../../${ARTIFACT_NAME}" *prqlc_c* - name: Create artifact for Windows shell: bash if: runner.os == 'Windows' run: | export ARTIFACT_NAME="prqlc_c-${{ github.ref_type == 'tag' && github.ref_name || 0 }}-${{ matrix.target }}.zip" echo "ARTIFACT_NAME=${ARTIFACT_NAME}" >>"$GITHUB_ENV" cd target/${{ matrix.target }}/${{ inputs.profile == 'release' && 'release' || 'debug' }} ls -al 7z a "../../../${ARTIFACT_NAME}" *prqlc_c* - name: Upload prqlc-c uses: actions/upload-artifact@v5 with: name: prqlc_c-${{ inputs.target }}-${{ inputs.profile }} path: ${{ env.ARTIFACT_NAME }} - id: echo-artifact-name shell: bash run: echo "artifact-name=${{ env.ARTIFACT_NAME }}" >>"$GITHUB_OUTPUT" ================================================ FILE: .github/actions/build-python/action.yaml ================================================ name: build-wheel description: "Use maturin to build python dists." inputs: target: description: Maturin build target, or 'source' for source distribution. Currently only used for Linux, otherwise defaults to the platform. required: true profile: description: Build profile option; `dev` or `release`. required: true package: description: Package name required: true runs: using: composite steps: - id: package-path run: echo "package_path=$(cargo metadata --no-deps --format-version 1 | jq -r --arg package_name ${{ inputs.package }} '.packages[] | select(.name == $package_name) | .manifest_path')" >>"$GITHUB_OUTPUT" shell: bash # There's no benefit from caching here, because the maturin action uses a container. - uses: PyO3/maturin-action@v1 if: inputs.target == 'source' with: command: sdist args: -o target/python -m ${{steps.package-path.outputs.package_path}} - uses: PyO3/maturin-action@v1 if: runner.os == 'Linux' && inputs.target != 'source' with: target: ${{ inputs.target }} manylinux: auto command: build args: --profile=${{ inputs.profile }} -o target/python -m ${{steps.package-path.outputs.package_path}} - uses: PyO3/maturin-action@v1 if: runner.os == 'Windows' && inputs.target != 'source' with: command: build args: --profile=${{ inputs.profile }} -o target/python -m ${{steps.package-path.outputs.package_path}} - uses: PyO3/maturin-action@v1 if: runner.os == 'macOS' && inputs.target != 'source' with: command: build # We override the target with `universal2-apple-darwin`. Probably we # should use the target from the input or at least ensure it matches. args: --profile=${{ inputs.profile }} -o target/python --target universal2-apple-darwin -m ${{steps.package-path.outputs.package_path}} - name: Upload wheels uses: actions/upload-artifact@v5 with: name: # We could avoid the OS here if we handled the target more explicitly. ${{ inputs.package }}-${{ runner.os }}-${{ inputs.target }}-${{ inputs.profile }} path: target/python ================================================ FILE: .github/actions/time-compilation/action.yaml ================================================ name: Time Compilation description: Time the cargo compilation, outputting an HTML file. inputs: use_cache: required: true description: Whether to use the cache of dependencies runs: using: composite steps: - run: ./.github/workflows/scripts/set_version.sh shell: bash - name: 💰 Cache id: cache uses: Swatinem/rust-cache@v2 with: prefix-key: ${{ env.version }}-${{ hashFiles('./Cargo.lock') }} save-if: ${{ github.ref == 'refs/heads/main' }} # 'true' seems to require quotes (and using a bare `inputs.use_cache` # doesn't work); I'm really not sure why. There are some issues on the # interwebs around this, but I couldn't find one that explained it. if: inputs.use_cache == 'true' - name: Remove cached results shell: bash run: rm -rf target/cargo-timings - name: 🏭 Compile uses: clechasseur/rs-cargo@v4 with: command: build args: --timings --all-targets - uses: actions/upload-artifact@v5 with: name: cargo-timing-${{ inputs.use_cache == 'true' && 'cache' || 'no_cache' }}.html path: target/cargo-timings/cargo-timing-*.html if-no-files-found: error # Only upload if a) we got a cache hit or b) we didn't want to use the cache anyway if: steps.cache.outputs.cache-hit == 'true' || inputs.use_cache == 'false' ================================================ FILE: .github/dependabot.yaml ================================================ version: 2 updates: - package-ecosystem: cargo directory: "/" schedule: # Running daily means the GHA cache will be cycling much faster, and we're # not in a great rush. So trying weekly, and we can iterate. interval: weekly commit-message: prefix: "chore: " # Bump all patch versions of rust dependencies as a single PR groups: patch: update-types: - patch # We exclude labels throughout because of https://github.com/dependabot/dependabot-core/issues/7645#issuecomment-1986212847 labels: [] - package-ecosystem: "npm" directories: - "/prqlc/bindings/js" - "/web/playground" schedule: interval: daily ignore: - dependency-name: "*" update-types: - version-update:semver-patch commit-message: prefix: "chore: " labels: [] - package-ecosystem: docker directory: .devcontainer/base-image schedule: interval: daily commit-message: prefix: "chore: " labels: [] - package-ecosystem: "github-actions" directories: - "/" - ".github/actions/build-python" - ".github/actions/build-prqlc" - ".github/actions/build-prqlc-c" - ".github/actions/time-compilation" commit-message: prefix: "chore: " schedule: interval: daily labels: [] - package-ecosystem: uv directory: "prqlc/bindings/prqlc-python" schedule: interval: daily commit-message: prefix: "chore: " ignore: - dependency-name: "*" update-types: - version-update:semver-patch labels: [] - package-ecosystem: "devcontainers" directory: "/" schedule: interval: daily labels: [] - package-ecosystem: "mix" directory: "prqlc/bindings/elixir" schedule: interval: daily commit-message: prefix: "chore: " labels: [] ================================================ FILE: .github/nightly-failure.md ================================================ --- title: Nightly tests failed labels: github_actions --- Nightly tests [failed on {{ date | date('YYYY-MM-DD') }}]({{ env.LINK }}) ================================================ FILE: .github/workflows/README.md ================================================ # GitHub Workflows See [our development docs](https://prql-lang.org/book/project/contributing/development.html) for docs & discussion. ================================================ FILE: .github/workflows/build-devcontainer.yaml ================================================ name: build-devcontainer # Multi-platform build for devcontainer base image on: workflow_call: inputs: push: type: boolean default: false workflow_dispatch: inputs: push: type: boolean default: false env: IMAGE_NAME: ghcr.io/prql/prql-devcontainer-base jobs: build: strategy: fail-fast: false matrix: include: - platform: linux/amd64 platform_name: amd64 runner: ubuntu-24.04 - platform: linux/arm64 platform_name: arm64 runner: ubuntu-24.04-arm runs-on: ${{ matrix.runner }} timeout-minutes: 240 steps: - uses: actions/checkout@v5 - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - uses: docker/setup-buildx-action@v3 - name: Prep build args run: echo "cargo_crates=$(yq -r '.vars.cargo_crates' Taskfile.yaml)" >> "$GITHUB_ENV" - name: Build and push by digest id: build uses: docker/build-push-action@v6 timeout-minutes: 240 with: context: .devcontainer/base-image build-args: cargo_crates=${{ env.cargo_crates }} platforms: ${{ matrix.platform }} outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=${{ inputs.push }} cache-from: type=gha,scope=${{ matrix.platform }} cache-to: type=gha,mode=max,scope=${{ matrix.platform }} # Smoke test: build local image and verify tools work - name: Build local image for smoke test uses: docker/build-push-action@v6 with: context: .devcontainer/base-image build-args: cargo_crates=${{ env.cargo_crates }} load: true tags: devcontainer-test:latest cache-from: type=gha,scope=${{ matrix.platform }} - name: Smoke test - run tests and build book run: | docker run --rm -v "${{ github.workspace }}:/workspace" -w /workspace \ devcontainer-test:latest bash -c " set -euxo pipefail cargo test mdbook build web/book/ " - name: Export digest if: inputs.push run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" touch "/tmp/digests/${digest#sha256:}" - name: Upload digest if: inputs.push uses: actions/upload-artifact@v5 with: name: digests-${{ matrix.platform_name }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 merge: runs-on: ubuntu-24.04 timeout-minutes: 30 if: inputs.push needs: build steps: - name: Download digests uses: actions/download-artifact@v6 with: path: /tmp/digests pattern: digests-* merge-multiple: true - name: Validate digests run: | digest_count=$(find /tmp/digests -type f | wc -l) if [ "$digest_count" -ne 2 ]; then echo "Error: Expected 2 digests (amd64 + arm64), found $digest_count" ls -la /tmp/digests exit 1 fi - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - uses: docker/setup-buildx-action@v3 - uses: docker/metadata-action@v5 id: meta with: images: ${{ env.IMAGE_NAME }} tags: type=raw,latest - name: Create manifest list and push working-directory: /tmp/digests run: | tags=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") digests=$(printf '${{ env.IMAGE_NAME }}@sha256:%s ' *) # shellcheck disable=SC2086 docker buildx imagetools create $tags $digests - name: Verify multi-platform manifest run: | docker buildx imagetools inspect ${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} platforms=$(docker buildx imagetools inspect ${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} --raw | \ jq -r '.manifests[] | select(.platform.os != "unknown") | .platform | "\(.os)/\(.architecture)"' | sort) expected="linux/amd64 linux/arm64" if [ "$platforms" != "$expected" ]; then echo "Error: Expected platforms not found" echo "Expected: $expected" echo "Found: $platforms" exit 1 fi echo "✓ Multi-platform manifest verified: amd64 + arm64" ================================================ FILE: .github/workflows/build-web.yaml ================================================ name: build-web on: workflow_call: workflow_dispatch: # We need consistent env vars across all workflows for the cache to work env: CARGO_TERM_COLOR: always CLICOLOR_FORCE: 1 RUSTFLAGS: "-C debuginfo=0" RUSTDOCFLAGS: "-Dwarnings" jobs: build-web: runs-on: ubuntu-24.04 steps: - name: 📂 Checkout code uses: actions/checkout@v5 # Website requires Hugo - name: Setup Hugo uses: peaceiris/actions-hugo@v3.0.0 - uses: baptiste0928/cargo-install@v3 with: crate: mdbook - uses: baptiste0928/cargo-install@v3 with: crate: mdbook-footnote - uses: baptiste0928/cargo-install@v3 with: crate: wasm-pack - name: Setup Node uses: actions/setup-node@v6 with: node-version: "20.x" cache: "npm" cache-dependency-path: "**/package-lock.json" - run: ./.github/workflows/scripts/set_version.sh - name: 💰 Cache uses: Swatinem/rust-cache@v2 with: prefix-key: ${{ env.version }}-${{ hashFiles('./Cargo.lock') }} shared-key: web save-if: ${{ github.ref == 'refs/heads/web' || github.ref == 'refs/heads/main' }} - uses: go-task/setup-task@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - name: 🕷️ Build web run: task web:build - uses: actions/upload-pages-artifact@v4.0.0 with: path: web/website/public/ build-codemirror-demo: runs-on: ubuntu-24.04 steps: - name: 📂 Checkout code uses: actions/checkout@v5 - name: 🧅 Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Install CodeMirror demo dependencies working-directory: web/prql-codemirror-demo/ run: bun install - name: Install Lezer dependencies working-directory: grammars/prql-lezer/ run: bun install - name: Build Lezer grammar working-directory: grammars/prql-lezer/ run: bun run build - name: Copy Lezer grammar into demo working-directory: web/prql-codemirror-demo/ run: | mkdir src/lang-prql/prql-lezer cp ../../grammars/prql-lezer/dist/* src/lang-prql/prql-lezer/ - name: Build CodeMirror demo working-directory: web/prql-codemirror-demo/ run: bun run build ================================================ FILE: .github/workflows/claude.yaml ================================================ name: Claude Code on: issue_comment: types: [created] pull_request_review_comment: types: [created] # We need consistent env vars across all workflows for the cache to work env: CARGO_TERM_COLOR: always CLICOLOR_FORCE: 1 RUSTFLAGS: "-C debuginfo=0" RUSTDOCFLAGS: "-Dwarnings" jobs: claude: # Only run when comment contains @claude if: | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) runs-on: ubuntu-latest permissions: contents: write # Allow pushing commits pull-requests: write # Allow commenting and updating PRs issues: write # Allow commenting on issues id-token: write actions: read # Required for Claude to read CI results on PRs steps: - name: 📂 Checkout code uses: actions/checkout@v5 with: # Fix: Checkout PR branch for issue_comment events instead of default branch ref: ${{ github.event.issue.pull_request && format('refs/pull/{0}/head', github.event.issue.number) || github.ref }} fetch-depth: 0 # Get more history for better context fetch-tags: true - name: 🔧 Configure git for Claude run: | git config --global user.name "Claude Code" git config --global user.email "claude@anthropic.com" # Install essential tools for the PRQL project - name: 🔧 Setup Task uses: go-task/setup-task@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} # Rust is pre-installed on ubuntu-latest, but we ensure consistent toolchain - uses: baptiste0928/cargo-install@v3 with: crate: cargo-insta - uses: baptiste0928/cargo-install@v3 with: crate: cargo-nextest - run: ./.github/workflows/scripts/set_version.sh shell: bash - name: 💰 Cache uses: Swatinem/rust-cache@v2 with: prefix-key: ${{ hashFiles('./Cargo.lock') }} shared-key: rust-x86_64-unknown-linux-gnu save-if: false - name: 🤖 Run Claude Code id: claude uses: anthropics/claude-code-action@v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} show_full_output: true # This is an optional setting that allows Claude to read CI results on PRs additional_permissions: | actions: read # Model, tools, and CI behavior. System prompt must be single-line # with \n escapes (YAML multiline breaks argument parsing). claude_args: | --model opus --allowedTools Bash,Edit,Read,Write,Glob,Grep,WebSearch,WebFetch --system-prompt "You are helping with the PRQL project in a GitHub Actions environment.\n\nFollow the project guidelines in CLAUDE.md.\nAfter making changes, ensure tests pass with 'task test-all' and lints with 'task lint'.\n\nFor CI failure diagnosis:\n- Use 'gh run list' and 'gh run view' to check CI status\n- Look for specific error messages in failing jobs\n- Check the test output for detailed failure information\n- When fixing issues, verify changes with the appropriate test commands\n- Once local test commands work, push to the PR\n- Then: iterate between monitoring CI, fixing any remaining issues, monitoring CI\n- Don't return until we're confident that we've done everything we can, including monitoring CI to completion and fixing any failures" ================================================ FILE: .github/workflows/lint-megalinter.yaml ================================================ # MegaLinter GitHub Action configuration file # More info at https://megalinter.io name: lint-megalinter on: workflow_call: workflow_dispatch: # Comment env block if you do not want to apply fixes env: # Apply linter fixes configuration # # When active, APPLY_FIXES must also be defined as environment variable # (in github/workflows/mega-linter.yml or other CI tool) # PRQL-change — edited out # APPLY_FIXES: all # Decide which event triggers application of fixes in a commit or a PR # (pull_request, push, all) APPLY_FIXES_EVENT: pull_request # If APPLY_FIXES is used, defines if the fixes are directly committed (commit) # or posted in a PR (pull_request) APPLY_FIXES_MODE: commit concurrency: group: ${{ github.ref }}-${{ github.workflow }} cancel-in-progress: true jobs: megalinter: name: MegaLinter runs-on: ubuntu-24.04 # Give the default GITHUB_TOKEN write permission to commit and push, comment # issues & post new PR; remove the ones you do not need permissions: contents: write issues: write pull-requests: write steps: # Git Checkout - name: Checkout Code uses: actions/checkout@v5 with: token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} # If you use VALIDATE_ALL_CODEBASE = true, you can remove this line to # improve performance fetch-depth: 0 # MegaLinter - name: MegaLinter # You can override MegaLinter flavor used to have faster performances # More info at https://megalinter.io/flavors/ uses: oxsecurity/megalinter@v9.1.0 id: ml # All available variables are described in documentation # https://megalinter.io/configuration/ env: VALIDATE_ALL_CODEBASE: true GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # ADD YOUR CUSTOM ENV VARIABLES HERE OR DEFINE THEM IN A FILE # .mega-linter.yml AT THE ROOT OF YOUR REPOSITORY # Uncomment to disable copy-paste and spell checks # DISABLE: COPYPASTE,SPELL # Upload MegaLinter artifacts - name: Archive production artifacts uses: actions/upload-artifact@v5 if: success() || failure() with: name: MegaLinter reports path: | megalinter-reports mega-linter.log # Set APPLY_FIXES_IF var for use in future steps - name: Set APPLY_FIXES_IF var run: | printf 'APPLY_FIXES_IF=%s\n' "${{ steps.ml.outputs.has_updated_sources == 1 && ( env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name ) && ( github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository ) }}" >> "${GITHUB_ENV}" # Set APPLY_FIXES_IF_* vars for use in future steps - name: Set APPLY_FIXES_IF_* vars run: | printf 'APPLY_FIXES_IF_PR=%s\n' "${{ env.APPLY_FIXES_IF == 'true' && env.APPLY_FIXES_MODE == 'pull_request' }}" >> "${GITHUB_ENV}" printf 'APPLY_FIXES_IF_COMMIT=%s\n' "${{ env.APPLY_FIXES_IF == 'true' && env.APPLY_FIXES_MODE == 'commit' && (!contains(fromJSON('["refs/heads/main", "refs/heads/master"]'), github.ref)) }}" >> "${GITHUB_ENV}" # Create pull request if applicable # (for now works only on PR from same repository, not from forks) - name: Create Pull Request with applied fixes uses: peter-evans/create-pull-request@v7 id: cpr if: env.APPLY_FIXES_IF_PR == 'true' with: token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} commit-message: "[MegaLinter] Apply linters automatic fixes" title: "[MegaLinter] Apply linters automatic fixes" labels: bot - name: Create PR output if: env.APPLY_FIXES_IF_PR == 'true' run: | echo "PR Number - ${{ steps.cpr.outputs.pull-request-number }}" echo "PR URL - ${{ steps.cpr.outputs.pull-request-url }}" # Push new commit if applicable # (for now works only on PR from same repository, not from forks) - name: Prepare commit if: env.APPLY_FIXES_IF_COMMIT == 'true' run: sudo chown -Rc $UID .git/ - name: Commit and push applied linter fixes uses: stefanzweifel/git-auto-commit-action@v7 if: env.APPLY_FIXES_IF_COMMIT == 'true' with: branch: >- ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }} commit_message: "[MegaLinter] Apply linters fixes" commit_user_name: megalinter-bot commit_user_email: nicolas.vuillamy@ox.security ================================================ FILE: .github/workflows/nightly.yaml ================================================ # A workflow containing jobs we run only on nightly. This is called by # `tests.yaml` on a schedule and on request with a `pr-nightly` label. The # workflow encapsulates lots of jobs rather than listing them all in `tests.yaml` # and conditioning each on `nightly` (wouldn't be terrible but less modular,). name: nightly on: workflow_call: workflow_dispatch: # We need consistent env vars across all workflows for the cache to work env: CARGO_TERM_COLOR: always CLICOLOR_FORCE: 1 RUSTFLAGS: "-C debuginfo=0" RUSTDOCFLAGS: "-Dwarnings" jobs: cargo-audit: runs-on: ubuntu-24.04 # We can't read PRQL repo security events on forks, which causes this to # incorrectly fail ( # https://github.com/PRQL/prql/actions/runs/5718693342/job/15495030808?pr=3195#step:3:28 # ). So we disable. If we wanted to run checks on PRs, we could move this to # `pull-request-target`. # # Would be better if we could only run when we know we have permissions. But # this will do... # 2024-12-09 — this is failing on normal PRs despite clearing the advisories # in GitHub, and the action is archived. We have the GitHub security audits, # `prqlc` doesn't cross any trust boundaries, so disabling for the moment. # We can re-enable if a supported action is available. if: ${{ github.repository_owner == 'prql' && !github.event.pull_request.head.repo.fork && 'run' == 'no' }} permissions: actions: read contents: read security-events: write issues: write checks: write steps: - uses: actions/checkout@v5 - uses: rustsec/audit-check@v2.0.0 with: token: ${{ secrets.GITHUB_TOKEN }} cargo-bench: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v5 - uses: clechasseur/rs-cargo@v4 with: command: bench # GH Actions is fairly noisy, so the precise details don't matter that # much. We do want to check the benchmarks run, and possibly we can # use this to identify and big changes in performance. args: -- --warm-up-time=0.3 --measurement-time=1 time-compilation: runs-on: ubuntu-24.04 strategy: matrix: use_cache: [true, false] steps: - name: 📂 Checkout code uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: python-version: "3.11" - uses: ./.github/actions/time-compilation with: use_cache: ${{ matrix.use_cache }} # We need consistent env vars across all workflows for the cache to work env: CARGO_TERM_COLOR: always CLICOLOR_FORCE: 1 RUSTFLAGS: "-C debuginfo=0" RUSTDOCFLAGS: "-Dwarnings" check-unused-dependencies: runs-on: ubuntu-24.04 steps: - name: 📂 Checkout code uses: actions/checkout@v5 - run: rustup override set nightly-2025-11-10 - uses: baptiste0928/cargo-install@v3 with: crate: cargo-udeps # Once with all targets, once without, to find anything that should be in # `dev` but is more general. - uses: clechasseur/rs-cargo@v4 with: command: udeps args: --all-targets - uses: clechasseur/rs-cargo@v4 with: command: udeps # We now use the devcontainer. TODO: is it possible to have a similar test for # that? Or that would require VSCode to install the dependencies? # test-docker: # # We only test the build in `test-all`; this also runs tests. # runs-on: ubuntu-24.04 # steps: # - name: 📂 Checkout code # uses: actions/checkout@v5 # - uses: docker/setup-buildx-action@v2 # - name: Build # uses: docker/build-push-action@v4 # with: # tags: prql:latest # # Use the GHA cache # load: true # cache-from: type=gha # cache-to: type=gha,mode=max # # https://aschmelyun.com/blog/using-docker-run-inside-of-github-actions/ # - name: Test # uses: addnab/docker-run-action@v3 # with: # image: prql:latest # options: -v ${{ github.workspace }}/:/src # run: task test-rust code-ql: # Currently almost the default code-ql config runs-on: ubuntu-24.04 permissions: actions: read contents: read security-events: write strategy: fail-fast: false matrix: language: ["javascript", "python"] # We could add java, but it require a custom build step and we have very little java... # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support steps: - name: Checkout repository uses: actions/checkout@v5 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild uses: github/codeql-action/autobuild@v4 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # If the Autobuild fails above, remove it and uncomment the following three lines. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # - run: | # echo "Run, Build Application using script" # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 nightly-release: # Test release workflow uses: ./.github/workflows/release.yaml ================================================ FILE: .github/workflows/publish-web.yaml ================================================ name: publish-web on: push: branches: - web # Even though releases push to `web` branch, that doesn't cause this workflow # to run, because GHA can't start workflows itself. So we also run on # releases. release: types: [released] # Called by pull-request when specifically requested workflow_call: workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true # We need consistent env vars across all workflows for the cache to work env: CARGO_TERM_COLOR: always CLICOLOR_FORCE: 1 RUSTFLAGS: "-C debuginfo=0" RUSTDOCFLAGS: "-Dwarnings" jobs: build-web: uses: ./.github/workflows/build-web.yaml deploy-web: needs: build-web runs-on: ubuntu-24.04 # Don't attempt to publish if on a fork or on a PR running on upstream. if: ${{ github.repository_owner == 'prql' && !github.event.pull_request.head.repo.fork }} permissions: pages: write id-token: write environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Setup Pages id: pages uses: actions/configure-pages@v5 - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4.0.5 ================================================ FILE: .github/workflows/pull-request-target.yaml ================================================ name: pull-request-target on: pull_request_target: types: [opened, edited, synchronize, labeled, closed] branches: - "*" concurrency: # Generally we use `github.ref`, but in pull_request_target, that's always `main`. group: ${{ github.workflow }}-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: main: name: Validate PR title runs-on: ubuntu-24.04 steps: - uses: amannn/action-semantic-pull-request@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: requireScope: false # The standard ones plus # - "internal" for code quality / ergonomics improvements # - "devops" for developer ergonomics # - "web" for playground / website (but not docs) # - "refine" for tiny changes types: | feat fix docs style refactor perf test build ci chore revert internal devops web refine backport: # Backport to `web` branch on `pr-backport-web` name: Backport to `web` branch runs-on: ubuntu-24.04 # Confirm that it's merged and has a label to ensure nothing is backported without oversight if: | github.event.pull_request.merged && ( github.event.action == 'closed' || ( github.event.action == 'labeled' && contains(github.event.label.name, 'pr-backport-web') ) ) steps: - uses: tibdex/backport@v2 with: # This is a personal access token from the @prql-bot github_token: ${{ secrets.PRQL_BOT_GITHUB_TOKEN }} # Docs are at https://github.com/tibdex/backport/blob/main/action.yml # We only use `web` atm label_pattern: "^pr-backport-(?([^ ]+))$" title_template: "chore: Backport #<%= number%> to `web`" automerge: runs-on: ubuntu-24.04 permissions: pull-requests: write contents: write # Check it's not coming from a fork — both to save running and # in case someone spoofed an `actor` name. if: ${{ !github.event.pull_request.head.repo.fork }} steps: # Get number of commits in the PR - id: commit-count run: > echo "commit-count=$(curl -s -H 'Authorization: token ${{ github.token }}' https://api.github.com/repos/prql/prql/pulls/${{ github.event.pull_request.number }}/commits | jq 'length')" >>"$GITHUB_OUTPUT" - if: # - It's dependabot # - or there's only one commit — so nothing has made further changes and # - or it's prql-bot # - or it's a pre-commit-ci update github.actor == 'dependabot[bot]' || (steps.commit-count.outputs.commit-count == '1' && (github.actor == 'prql-bot' || github.actor == 'pre-commit-ci[bot]')) run: gh pr merge --auto --squash --delete-branch ${{ github.event.pull_request.html_url }} env: GITHUB_TOKEN: ${{ secrets.PRQL_BOT_GITHUB_TOKEN }} ================================================ FILE: .github/workflows/release.yaml ================================================ # This workflow runs on tags / releases. It also runs on nightly builds without # publishing anything, in order to test as much of the build works as possible. # # We indicate whether it should publish, vs. just build, by checking whether # `github.event_name == 'release'` . (An alternative would be to have an input # which is passed in by the calling workflow.) name: release on: release: types: [released] workflow_call: workflow_dispatch: # We need consistent env vars across all workflows for the cache to work env: CARGO_TERM_COLOR: always CLICOLOR_FORCE: 1 RUSTFLAGS: "-C debuginfo=0" RUSTDOCFLAGS: "-Dwarnings" jobs: brew-dispatcher: name: Release on homebrew-prql runs-on: ubuntu-24.04 if: github.event_name == 'release' steps: - uses: actions/github-script@v8 with: github-token: ${{ secrets.PRQL_BOT_GITHUB_TOKEN }} script: | await github.rest.actions.createWorkflowDispatch({ owner: 'prql', repo: 'homebrew-prql', workflow_id: 'update.yaml', ref: 'main', inputs: { version: '${{ github.ref }}', URL: 'https://github.com/PRQL/prql/archive/${{ github.ref }}.tar.gz' } }) build-prqlc: runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - os: ubuntu-24.04 target: x86_64-unknown-linux-musl - os: ubuntu-24.04 target: aarch64-unknown-linux-musl - os: macos-15 target: aarch64-apple-darwin - os: windows-latest target: x86_64-pc-windows-msvc # Intel macOS build - os: macos-15-intel target: x86_64-apple-darwin features: default,test-dbs permissions: contents: write steps: - name: 📂 Checkout code uses: actions/checkout@v5 - uses: ./.github/actions/build-prqlc id: build-artifact with: target: ${{ matrix.target }} profile: release features: cli - name: Upload release artifact if: github.event_name == 'release' uses: softprops/action-gh-release@v2 with: append_body: true files: ${{ steps.build-artifact.outputs.artifact-name }} - name: test the CLI works # TODO: Add for Windows too (but will require unzipping rather than # un-taring) # # Currently filtering by x86, since that's the same as those not # cross-compiled. But we'll have to refine this in the future. if: ${{ contains(matrix.target, 'x86') && matrix.target != 'x86_64-pc-windows-msvc' }} run: | # `prqlc` is an existing path at the root mkdir -p temp_path tar xzf ${{ steps.build-artifact.outputs.artifact-name }} -C temp_path ./temp_path/prqlc --help build-prqlc-c: # Mostly a copy/paste of `build-prqlc`. runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - os: ubuntu-24.04 target: x86_64-unknown-linux-musl - os: ubuntu-24.04 target: aarch64-unknown-linux-musl - os: macos-15 target: aarch64-apple-darwin - os: windows-latest target: x86_64-pc-windows-msvc permissions: contents: write steps: - name: 📂 Checkout code uses: actions/checkout@v5 - uses: ./.github/actions/build-prqlc-c id: build-artifact with: target: ${{ matrix.target }} profile: release - name: Upload release artifact if: github.event_name == 'release' uses: softprops/action-gh-release@v2 with: append_body: true files: ${{ steps.build-artifact.outputs.artifact-name }} publish-winget: runs-on: ubuntu-24.04 needs: build-prqlc if: github.event_name == 'release' steps: - name: publish uses: vedantmgoyal2009/winget-releaser@v2 with: identifier: PRQL.prqlc version: ${{ github.ref_name }} installers-regex: '^prqlc-.*-windows-.*\.zip$' token: ${{ secrets.PRQL_BOT_GITHUB_TOKEN }} fork-user: prql-bot build-deb-package: runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: target: - x86_64-unknown-linux-musl - aarch64-unknown-linux-musl needs: build-prqlc permissions: contents: write steps: - uses: actions/download-artifact@v6 with: name: prqlc-${{ matrix.target }}-release - name: Copy files into .deb package run: | tar -xf prqlc-*.tar.gz mkdir -p .debpkg/usr/bin mv prqlc .debpkg/usr/bin/prqlc chmod +x .debpkg/usr/bin/prqlc - name: Set arch variable run: | if [ "${{ matrix.target }}" == "aarch64-unknown-linux-musl" ]; then echo "package_arch=arm64" >> "$GITHUB_ENV" elif [ "${{ matrix.target }}" == "x86_64-unknown-linux-musl" ]; then echo "package_arch=amd64" >> "$GITHUB_ENV" else echo "::error::Unknown architecture: ${{ matrix.target }}" exit 1 fi - name: 📦 Build .deb package uses: jiro4989/build-deb-action@v4 with: package: prqlc package_root: .debpkg maintainer: The PRQL Project version: ${{ github.event_name == 'release' && github.ref_name || 0 }} arch: ${{ env.package_arch }} desc: > prqlc is the CLI for the PRQL compiler. It compiles PRQL to SQL, and offers various diagnostics. PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement. - uses: actions/upload-artifact@v5 with: name: deb-${{ matrix.target }} path: ./*.deb - name: Release if: github.event_name == 'release' uses: softprops/action-gh-release@v2 with: files: prqlc_*.deb build-rpm-package: runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: target: - x86_64-unknown-linux-musl #- aarch64-unknown-linux-musl # https://github.com/jiro4989/build-rpm-action/issues/6 needs: build-prqlc permissions: contents: write steps: - uses: actions/download-artifact@v6 with: name: prqlc-${{ matrix.target }}-release - name: Copy files into .rpm package run: | tar -xf prqlc-*.tar.gz mkdir -p .rpmpkg/usr/bin mv prqlc .rpmpkg/usr/bin/prqlc chmod +x .rpmpkg/usr/bin/prqlc - name: Set variables run: | if [ "${{ matrix.target }}" == "aarch64-unknown-linux-musl" ]; then echo "package_arch=aarch64" >> "$GITHUB_ENV" elif [ "${{ matrix.target }}" == "x86_64-unknown-linux-musl" ]; then echo "package_arch=x86_64" >> "$GITHUB_ENV" else echo "::error::Unknown architecture: ${{ matrix.target }}" fi - name: 📦 Build .rpm package uses: jiro4989/build-rpm-action@v2 with: summary: CLI for PRQL, a modern language for transforming data package: prqlc package_root: .rpmpkg maintainer: The PRQL Project vendor: The PRQL Project version: ${{ github.event_name == 'release' && github.ref_name || 0 }} arch: ${{ env.package_arch }} desc: > prqlc is the CLI for the PRQL compiler. It compiles PRQL to SQL, and offers various diagnostics. PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement. license: Apache-2.0 - uses: actions/upload-artifact@v5 with: name: artifact-rpm path: | ./*.rpm !./*-debuginfo-*.rpm - run: rm prqlc-debuginfo-*.rpm - name: Release if: github.event_name == 'release' uses: softprops/action-gh-release@v2 with: files: prqlc-*.rpm build-and-publish-snap: runs-on: ubuntu-24.04 if: ${{ github.event_name == 'release' }} steps: - name: 📂 Checkout code uses: actions/checkout@v5 - name: Move Snap to project root directory run: cp -r prqlc/packages/snap/ . - name: 📦 Build Snap id: build uses: snapcore/action-build@v1 - name: 🆙 Publish Snap if: github.event_name == 'release' uses: snapcore/action-publish@v1 env: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} with: snap: ${{ steps.build.outputs.snap }} release: stable build-python-wheels: runs-on: ${{ matrix.os }} strategy: fail-fast: true matrix: os: [ubuntu-24.04, windows-latest] package: - prqlc-python target: [x86_64] include: # MacOS with universal builds - os: macos-15 package: prqlc-python target: universal2-apple-darwin # Also produce more targets for ubuntu: - os: ubuntu-24.04 package: prqlc-python target: aarch64 - os: ubuntu-24.04 package: prqlc-python target: source steps: - name: 📂 Checkout code uses: actions/checkout@v5 - uses: ./.github/actions/build-python with: target: ${{ matrix.target }} package: ${{ matrix.package }} profile: release publish-python: runs-on: ubuntu-24.04 needs: [build-python-wheels] if: github.event_name == 'release' permissions: id-token: write strategy: matrix: package: - prqlc-python steps: - uses: actions/download-artifact@v6 with: # `*` covers target & OS pattern: ${{ matrix.package }}-*-release merge-multiple: true - name: Publish to PyPI uses: PyO3/maturin-action@v1 with: command: upload args: --skip-existing * publish-js: runs-on: ubuntu-24.04 permissions: contents: read id-token: write steps: - name: 📂 Checkout code uses: actions/checkout@v5 - uses: baptiste0928/cargo-install@v3 with: crate: wasm-pack - name: Setup Node uses: actions/setup-node@v6 with: # Node 24+ includes npm 11.5.1+ required for OIDC trusted publishing node-version: "24.x" registry-url: "https://registry.npmjs.org" - run: ./.github/workflows/scripts/set_version.sh - name: 💰 Cache uses: Swatinem/rust-cache@v2 with: prefix-key: ${{ env.version }}-${{ hashFiles('./Cargo.lock') }} # Share key with the `build-web` job shared-key: web save-if: false # This is only required in order to have `cross-env` installed, since `npx # cross-env` doesn't seem to work in CI (https://github.com/PRQL/prql/pull/3728) - run: npm install working-directory: prqlc/bindings/js/ - name: Publish to npm run: npm publish --provenance --access public ${{ (github.event_name != 'release') && '--dry-run' || '' }} working-directory: prqlc/bindings/js/ publish-to-cargo: runs-on: ubuntu-24.04 steps: - name: 📂 Checkout code uses: actions/checkout@v5 - uses: baptiste0928/cargo-install@v3 with: crate: cargo-release # Currently, we can only check prqlc-parser which is not dependent other local crates with --dry-run. # https://github.com/crate-ci/cargo-release/issues/691 # --no-verify is required to prevent build. - run: cargo release publish --no-confirm ${{ github.event_name == 'release' && '--execute' || '--no-verify --package prqlc-parser'}} env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} # Requires another pass: https://github.com/PRQL/prql/issues/850 # publish-prql-java: # runs-on: ubuntu-24.04 # steps: # - name: Checkout code # uses: actions/checkout@v5 # - name: Install Java and Maven # uses: actions/setup-java@v3 # with: # java-version: 8 # - name: Release Maven package # uses: samuelmeuli/action-maven-publish@v1 # with: # gpg_private_key: ${{ secrets.gpg_private_key }} # gpg_passphrase: ${{ secrets.gpg_passphrase }} # nexus_username: ${{ secrets.nexus_username }} # nexus_password: ${{ secrets.nexus_password }} # directory: prql-java/java/ push-web-branch: runs-on: ubuntu-24.04 if: github.event_name == 'release' steps: - name: 📂 Checkout code uses: actions/checkout@v5 with: token: ${{ secrets.PRQL_BOT_GITHUB_TOKEN }} - run: git push origin HEAD:web --force push-devcontainer: if: github.event_name == 'release' uses: ./.github/workflows/build-devcontainer.yaml with: push: true ================================================ FILE: .github/workflows/scripts/set_version.sh ================================================ #!/bin/bash # We set prefix-key to the version from Cargo.toml for Swatinem/rust-cache@v2 # since the caches seem to accumulate cruft over time; # ref https://github.com/PRQL/prql/pull/2407 version=$(cargo metadata --format-version=1 --no-deps | jq --raw-output '.packages[] | select(.name == "prqlc") | .version') echo "version=${version}" >>"$GITHUB_ENV" ================================================ FILE: .github/workflows/scripts/util_free_space.sh ================================================ #!/usr/bin/env bash # From https://github.com/apache/arrow/blob/4011058f4a56bdcf160f46373355ffa0e22bcd2c/ci/scripts/util_free_space.sh # # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. set -eux if [ "${GITHUB_ACTIONS}" = "true" ]; then df -h echo "::group::/usr/local/*" du -hsc /usr/local/* echo "::endgroup::" # ~1GB sudo rm -rf \ /usr/local/aws-cli \ /usr/local/aws-sam-cil \ /usr/local/julia* || : echo "::group::/usr/local/bin/*" du -hsc /usr/local/bin/* echo "::endgroup::" # ~1GB (From 1.2GB to 214MB) sudo rm -rf \ /usr/local/bin/aliyun \ /usr/local/bin/aws \ /usr/local/bin/aws_completer \ /usr/local/bin/azcopy \ /usr/local/bin/bicep \ /usr/local/bin/cmake-gui \ /usr/local/bin/cpack \ /usr/local/bin/helm \ /usr/local/bin/hub \ /usr/local/bin/kubectl \ /usr/local/bin/minikube \ /usr/local/bin/node \ /usr/local/bin/packer \ /usr/local/bin/pulumi* \ /usr/local/bin/sam \ /usr/local/bin/stack \ /usr/local/bin/terraform || : # 142M sudo rm -rf /usr/local/bin/oc || : \ echo "::group::/usr/local/share/*" du -hsc /usr/local/share/* echo "::endgroup::" # 506MB sudo rm -rf /usr/local/share/chromium || : # 1.3GB sudo rm -rf /usr/local/share/powershell || : echo "::group::/usr/local/lib/*" du -hsc /usr/local/lib/* echo "::endgroup::" # 15GB sudo rm -rf /usr/local/lib/android || : # 341MB sudo rm -rf /usr/local/lib/heroku || : # 1.2GB sudo rm -rf /usr/local/lib/node_modules || : echo "::group::/opt/*" du -hsc /opt/* echo "::endgroup::" # 679MB sudo rm -rf /opt/az || : echo "::group::/opt/microsoft/*" du -hsc /opt/microsoft/* echo "::endgroup::" # 197MB sudo rm -rf /opt/microsoft/powershell || : echo "::group::/opt/hostedtoolcache/*" du -hsc /opt/hostedtoolcache/* echo "::endgroup::" # 5.3GB sudo rm -rf /opt/hostedtoolcache/CodeQL || : # 1.4GB sudo rm -rf /opt/hostedtoolcache/go || : # 489MB sudo rm -rf /opt/hostedtoolcache/PyPy || : # 376MB sudo rm -rf /opt/hostedtoolcache/node || : # Remove Web browser packages sudo apt purge -y \ firefox \ google-chrome-stable \ microsoft-edge-stable df -h fi ================================================ FILE: .github/workflows/test-dotnet.yaml ================================================ name: test-dotnet on: workflow_call: workflow_dispatch: # We need consistent env vars across all workflows for the cache to work env: CARGO_TERM_COLOR: always CLICOLOR_FORCE: 1 RUSTFLAGS: "-C debuginfo=0" RUSTDOCFLAGS: "-Dwarnings" jobs: test: runs-on: ubuntu-24.04 steps: - name: 📂 Checkout code uses: actions/checkout@v5 - name: 🏗 Build prqlc-c run: cargo build --package prqlc-c - name: 🔧 Setup dotnet uses: actions/setup-dotnet@v5 with: dotnet-version: 7 - name: 🧪 Build and test working-directory: prqlc/bindings run: | dotnet build dotnet cp ../../target/debug/libprqlc_c.* dotnet/PrqlCompiler/bin/Debug/net*/ cp ../../target/debug/libprqlc_c.* dotnet/PrqlCompiler.Tests/bin/Debug/net*/ dotnet test dotnet ================================================ FILE: .github/workflows/test-elixir.yaml ================================================ name: test-elixir on: workflow_call: inputs: oss: type: string default: '["ubuntu-24.04"]' workflow_dispatch: inputs: oss: type: string default: '["ubuntu-24.04"]' defaults: run: working-directory: prqlc/bindings/elixir env: MIX_ENV: test # We need consistent env vars across all workflows for the cache to work CARGO_TERM_COLOR: always CLICOLOR_FORCE: 1 RUSTFLAGS: "-C debuginfo=0" RUSTDOCFLAGS: "-Dwarnings" jobs: test: strategy: matrix: os: ${{ fromJSON(inputs.oss) }} otp: ["25.1.2"] elixir: ["1.15.7"] runs-on: ${{matrix.os}} steps: # Step: Check out the code. - name: Checkout code uses: actions/checkout@v5 # Step: Setup Elixir + Erlang image as the base. - name: Set up Elixir on Windows or Linux if: runner.os != 'macOS' uses: erlef/setup-beam@v1.20.4 with: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} - name: Install Erlang/Elixir on Mac if: runner.os == 'macOS' run: | brew install elixir mix local.hex --force # Step: Define how to cache deps. Restores existing cache if present. - name: Cache deps id: cache-deps uses: actions/cache@v4 env: cache-name: cache-elixir-deps with: path: elixir/deps key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} restore-keys: | ${{ runner.os }}-mix-${{ env.cache-name }}- # Step: Define how to cache the `_build` directory. After the first run, # this speeds up tests runs a lot. This includes not re-compiling our # project's downloaded deps every run. - name: Cache compiled build id: cache-build uses: actions/cache@v4 env: cache-name: cache-compiled-build with: path: elixir/_build key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} restore-keys: | ${{ runner.os }}-mix-${{ env.cache-name }}- ${{ runner.os }}-mix- # Step: Download project dependencies. If unchanged, uses # the cached version. - name: Install dependencies run: mix deps.get # Step: Compile the project treating any warnings as errors. # Customize this step if a different behavior is desired. - name: Compiles without warnings run: mix compile --warnings-as-errors # Step: Check that the checked in code has already been formatted. # This step fails if something was found unformatted. # Customize this step as desired. - name: Check Formatting run: mix format --check-formatted # Step: Execute the tests. - name: Run tests run: mix test ================================================ FILE: .github/workflows/test-java.yaml ================================================ name: test-java on: workflow_call: inputs: oss: type: string default: '["ubuntu-24.04"]' workflow_dispatch: inputs: oss: type: string default: '["ubuntu-24.04"]' # We need consistent env vars across all workflows for the cache to work env: CARGO_TERM_COLOR: always CLICOLOR_FORCE: 1 RUSTFLAGS: "-C debuginfo=0" RUSTDOCFLAGS: "-Dwarnings" jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: ${{ fromJSON(inputs.oss) }} steps: - name: Checkout code uses: actions/checkout@v5 - run: ./.github/workflows/scripts/set_version.sh - name: Compute Cargo.lock hash shell: bash run: | if command -v sha256sum &> /dev/null; then echo "cargo_lock_hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_ENV" else echo "cargo_lock_hash=$(shasum -a 256 Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_ENV" fi - name: 💰 Cache uses: Swatinem/rust-cache@v2 with: prefix-key: ${{ env.version }}-${{ env.cargo_lock_hash }} save-if: ${{ github.ref == 'refs/heads/main' }} shared-key: lib - name: Maven test working-directory: prqlc/bindings/java/java/ run: ./mvnw test ================================================ FILE: .github/workflows/test-js.yaml ================================================ name: test-js on: workflow_call: inputs: oss: type: string default: '["ubuntu-24.04"]' workflow_dispatch: inputs: oss: type: string default: '["ubuntu-24.04"]' # We need consistent env vars across all workflows for the cache to work env: CARGO_TERM_COLOR: always CLICOLOR_FORCE: 1 RUSTFLAGS: "-C debuginfo=0" RUSTDOCFLAGS: "-Dwarnings" jobs: test: runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.os == 'windows-latest' }} strategy: fail-fast: false matrix: os: ${{ fromJSON(inputs.oss) }} steps: - name: 📂 Checkout code uses: actions/checkout@v5 - name: Setup Node uses: actions/setup-node@v6 with: node-version: "21.x" - uses: baptiste0928/cargo-install@v3 with: crate: wasm-pack - run: ./.github/workflows/scripts/set_version.sh - name: Compute Cargo.lock hash shell: bash run: | if command -v sha256sum &> /dev/null; then echo "cargo_lock_hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_ENV" else echo "cargo_lock_hash=$(shasum -a 256 Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_ENV" fi - name: 💰 Cache uses: Swatinem/rust-cache@v2 id: cache with: prefix-key: ${{ env.version }}-${{ env.cargo_lock_hash }} # `web` is the closest rust cache key. It's useful on ubuntu (last # checked, roughly halved the time, to 4 min), but not on other OSs # given we don't have those caches. We can't use a separate one given # we're out of cache space. shared-key: web save-if: false - run: npm cit working-directory: prqlc/bindings/js ================================================ FILE: .github/workflows/test-php.yaml ================================================ name: test-php on: workflow_call: workflow_dispatch: # We need consistent env vars across all workflows for the cache to work env: CARGO_TERM_COLOR: always CLICOLOR_FORCE: 1 RUSTFLAGS: "-C debuginfo=0" RUSTDOCFLAGS: "-Dwarnings" jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-24.04] steps: - name: 📂 Checkout code uses: actions/checkout@v5 - uses: go-task/setup-task@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - run: ./.github/workflows/scripts/set_version.sh - name: 💰 Cache uses: Swatinem/rust-cache@v2 with: prefix-key: ${{ env.version }}-${{ hashFiles('./Cargo.lock') }} save-if: ${{ github.ref == 'refs/heads/main' }} shared-key: lib - run: task build-php - run: task test-php ================================================ FILE: .github/workflows/test-prqlc-c.yaml ================================================ name: test-prqlc-c on: workflow_call: workflow_dispatch: # We need consistent env vars across all workflows for the cache to work env: CARGO_TERM_COLOR: always CLICOLOR_FORCE: 1 RUSTFLAGS: "-C debuginfo=0" RUSTDOCFLAGS: "-Dwarnings" jobs: test-c: runs-on: ubuntu-24.04 steps: - name: 📂 Checkout code uses: actions/checkout@v5 - run: ./.github/workflows/scripts/set_version.sh - name: 💰 Cache uses: Swatinem/rust-cache@v2 with: prefix-key: ${{ env.version }}-${{ hashFiles('./Cargo.lock') }} save-if: ${{ github.ref == 'refs/heads/main' }} shared-key: lib - name: Build uses: clechasseur/rs-cargo@v4 with: command: build # Currently requires a release build; would be useful to allow a debug build. args: --release --package prqlc-c - name: Run example minimal-c working-directory: prqlc/bindings/prqlc-c/examples/minimal-c run: make run - name: Run example minimal-cpp working-directory: prqlc/bindings/prqlc-c/examples/minimal-cpp run: make run - uses: go-task/setup-task@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - name: 🔧 Setup Zig uses: mlugg/setup-zig@v2 - name: Run example minimal-zig run: task zig ================================================ FILE: .github/workflows/test-python.yaml ================================================ name: test-python on: workflow_call: inputs: oss: type: string default: '["ubuntu-24.04"]' workflow_dispatch: inputs: oss: type: string default: '["ubuntu-24.04"]' # We need consistent env vars across all workflows for the cache to work env: CARGO_TERM_COLOR: always CLICOLOR_FORCE: 1 RUSTFLAGS: "-C debuginfo=0" RUSTDOCFLAGS: "-Dwarnings" jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: ${{ fromJSON(inputs.oss) }} steps: - name: 📂 Checkout code uses: actions/checkout@v5 - name: Build wheel uses: ./.github/actions/build-python with: target: ${{ matrix.os == 'ubuntu-24.04' && 'x86_64' || '' }} package: prqlc-python profile: dev - uses: actions/download-artifact@v6 with: # `*` covers all targets (we could make this explicit...) pattern: prqlc-python-${{ runner.os }}-*-dev path: target/python - uses: actions/setup-python@v6 with: python-version: "3.10" - uses: actions/setup-python@v6 with: python-version: "3.12" - name: Install uv uses: astral-sh/setup-uv@v7 - name: Install nox run: uv tool install nox shell: bash - name: Compute pyproject.toml hash shell: bash run: | if command -v sha256sum &> /dev/null; then echo "pyproject_hash=$(sha256sum prqlc/bindings/prqlc-python/pyproject.toml | cut -d' ' -f1)" >> "$GITHUB_ENV" else echo "pyproject_hash=$(shasum -a 256 prqlc/bindings/prqlc-python/pyproject.toml | cut -d' ' -f1)" >> "$GITHUB_ENV" fi - name: Cache uv and Nox uses: actions/cache@v4 with: path: | .nox ~/.cache/uv key: nox-uv-${{ env.pyproject_hash }} - name: Run tests and typing shell: bash run: | export UV_SYSTEM_PYTHON=1 nox -s tests typing -f prqlc/bindings/prqlc-python/noxfile.py ================================================ FILE: .github/workflows/test-rust.yaml ================================================ name: test-rust on: # Currently we only run this as `workflow_call`, since `tests.yaml` always calls it. workflow_call: inputs: os: type: string required: true target: type: string required: true features: type: string required: true nightly: description: "Whether to run extra tests (this is not nightly rust)" type: boolean default: false env: CARGO_TERM_COLOR: always CLICOLOR_FORCE: 1 # This used to reduce the size of the cargo cache by ~25%. It's not as # effective as it once was, as explained in # https://github.com/PRQL/prql/pull/2797 RUSTFLAGS: "-C debuginfo=0" RUSTDOCFLAGS: "-Dwarnings" jobs: test-rust: runs-on: ${{ inputs.os }} steps: - name: 📂 Checkout code uses: actions/checkout@v5 with: fetch-tags: true - name: Run docker compose # This can go early because the DBs take a few seconds to start up. if: ${{ contains(inputs.features, 'test-dbs-external') }} run: docker compose up -d working-directory: ./prqlc/prqlc/tests/integration/dbs - if: ${{ contains(inputs.target, 'musl') }} run: | sudo apt-get update sudo apt-get install -y musl-tools - run: rustup target add ${{ inputs.target }} - uses: baptiste0928/cargo-install@v3 with: crate: wasm-bindgen-cli if: inputs.target == 'wasm32-unknown-unknown' - uses: baptiste0928/cargo-install@v3 with: crate: cargo-insta - uses: baptiste0928/cargo-install@v3 with: crate: cargo-nextest - run: ./.github/workflows/scripts/set_version.sh shell: bash - name: Compute Cargo.lock hash shell: bash run: | if command -v sha256sum &> /dev/null; then echo "cargo_lock_hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_ENV" else echo "cargo_lock_hash=$(shasum -a 256 Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_ENV" fi - name: 💰 Cache uses: Swatinem/rust-cache@v2 id: cache with: # We add the hash of the Cargo.lock file to the key, to prevent the # gradual accumulation of disk space that comes from using previous # caches. The rust cache is designed to remove old packages, but isn't # that successful at it, and our current cache size at ~1.3GB is about # as much as GHA can handle, such that if we hold old packages, it # balloons to 2GB and fails. Some discussion at: # https://github.com/Swatinem/rust-cache/issues/177 prefix-key: ${{ env.version }}-${{ env.cargo_lock_hash }} shared-key: rust-${{ inputs.target }} # Don't save if empty features, because we run a test with empty # features on linux, and don't want to save that instead of the one # with all features. I tried including features in the key, but it # contains a comma, which isn't allowed; a whole extra task to remove # the comma seemed over the top. save-if: ${{ github.ref == 'refs/heads/main' && inputs.features != '' }} - uses: actions/setup-python@v6 # python isn't natively installed on macos-15, so we need to install it if: ${{ inputs.os == 'macos-15' }} with: python-version: "3.11" - name: Free up disk space # https://github.com/actions/runner-images/issues/2840 run: ./.github/workflows/scripts/util_free_space.sh # This takes ~3 minutes, so we'd really prefer not to run it on every # PR. Trying to run it only when there is no cache hit. We may need to # remove that condition and run it whenever we pull the docker images, # or explore breaking up the tests more (though that's not easy to get # savings on either). Ideally GH will allow for more disk space in the # future... (deleting things doesn't actually remove the amount of stuff # that GH needs to store, so it's purely performative!) if: ${{ contains(inputs.features, 'test-dbs-external') && steps.cache.outputs.cache-hit == 'false' }} # We split up the test compilation as recommended in # https://matklad.github.io/2021/09/04/fast-rust-builds.html - name: 🏭 Compile uses: clechasseur/rs-cargo@v4 with: command: test args: > --no-run --locked --target=${{ inputs.target }} --no-default-features --features=${{ inputs.features }} - name: Wait for database uses: ifaxity/wait-on-action@v1.2.1 with: resource: "tcp:1433 tcp:3306 tcp:5432 tcp:9004" timeout: 60000 if: ${{ contains(inputs.features, 'test-dbs-external') }} - name: 📋 Test uses: clechasseur/rs-cargo@v4 with: command: insta # Here, we also add: # - Unreferenced snapshots - `--unreferenced=auto` when testing on # linux & with `test-dbs` feature. # - Test runner - `--test-runner=nextest` when not targeting wasm32. # - Skip doc tests on Windows due to LNK1318 PDB errors args: > test --dnd --target=${{ inputs.target }} --no-default-features --features=${{ inputs.features }} ${{ contains(inputs.features, 'test-dbs') && inputs.target == 'x86_64-unknown-linux-gnu' && '--unreferenced=auto' || '' }} ${{ inputs.target != 'wasm32-unknown-unknown' && '--test-runner=nextest' || '' }} ${{ inputs.os == 'windows-latest' && '--lib --bins --tests --examples' || '' }} - name: 📋 Doctest # Skip doctests on Windows (LNK1318 PDB errors) and wasm32 if: inputs.os != 'windows-latest' && inputs.target != 'wasm32-unknown-unknown' uses: clechasseur/rs-cargo@v4 with: command: test args: > --doc --target=${{ inputs.target }} --no-default-features --features=${{ inputs.features }} - name: 📎 Clippy uses: clechasseur/rs-cargo@v4 with: command: clippy # Note that `--all-targets` doesn't refer to targets like # `wasm32-unknown-unknown`; it refers to lib / bin / tests etc. # args: > --all-targets --target=${{ inputs.target }} --no-default-features --features=${{ inputs.features }} -- -D warnings - name: ⌨️ Fmt uses: clechasseur/rs-cargo@v4 with: command: fmt args: --all --check - name: 🗒️ Doc # Only running on nightly, because of # https://github.com/duckdb/duckdb-rs/issues/179#issuecomment-1710986020. if: inputs.nightly == 'true' && inputs.target != 'wasm32-unknown-unknown' uses: clechasseur/rs-cargo@v4 with: command: doc # Only run with deps on nightly, since it's much slower, and so far™ # we haven't seen any errors. args: --target=${{ inputs.target }} --no-default-features --features=${{ inputs.features }} ${{ inputs.nightly != 'true' && '--no-deps' || '' }} - name: Build extra targets for cache # When building the cache, we also run with `--all-targets` so that # prqlc builds can use the same cache. if: ${{ github.ref == 'refs/heads/main' && steps.cache.outputs.cache-hit == 'false' }} uses: clechasseur/rs-cargo@v4 with: command: build args: --all-targets --target=${{ inputs.target }} --no-default-features --features=${{ inputs.features }} ================================================ FILE: .github/workflows/tests.yaml ================================================ # This file has transitioning to run almost everything, with rules defined in # this file rather than across lots of workflow files. name: tests on: pull_request: # Add `labeled`, so we can trigger a new run by adding a `pr-nightly` # label, which we then use to trigger a `nightly` run. types: [opened, reopened, synchronize, labeled] branches: - "*" push: branches: - main schedule: # Pick a random time, something that others won't pick, to be good citizens # and reduce GH's demand variance. - cron: "49 10 * * *" workflow_dispatch: workflow_call: concurrency: group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }} cancel-in-progress: true # We need consistent env vars across all workflows for the cache to work env: CARGO_TERM_COLOR: always CLICOLOR_FORCE: 1 RUSTFLAGS: "-C debuginfo=0" RUSTDOCFLAGS: "-Dwarnings" jobs: # This assesses whether we need to run jobs. Some of them are defined only by # the changes in PR, others also define a set of other criteria, such as # whether a label has been added, or we're on `main` branch. rules: runs-on: ubuntu-24.04 permissions: pull-requests: read outputs: book: ${{ steps.changes.outputs.book }} dotnet: ${{ steps.changes.outputs.dotnet }} devcontainer-push: ${{ steps.devcontainer-push.outputs.run }} devcontainer-build: ${{ steps.devcontainer-build.outputs.run }} elixir: ${{ steps.changes.outputs.elixir }} grammars: ${{ steps.changes.outputs.grammars }} java: ${{ steps.changes.outputs.java }} js: ${{ steps.changes.outputs.js }} prqlc-c: ${{ steps.changes.outputs.prqlc-c }} # Run tests such as rust tests for all-OSs, and bindings tests on ubuntu. # Somewhat a tradeoff between coverage and ensuring our CI queues stay # short. main: ${{ steps.main.outputs.run }} # Run all tests nightly: ${{ steps.nightly.outputs.run }} # For tasks which are very expensive or can only run on # the main repo, such as pushing devcontainer or creating issues nightly-upstream: ${{ steps.nightly-upstream.outputs.run }} php: ${{ steps.changes.outputs.php }} python: ${{ steps.changes.outputs.python }} rust: ${{ steps.changes.outputs.rust }} taskfile: ${{ steps.changes.outputs.taskfile }} web: ${{ steps.changes.outputs.web }} steps: - name: 📂 Checkout code uses: actions/checkout@v5 with: fetch-tags: true - uses: dorny/paths-filter@v3 id: changes with: filters: | book: - .github/workflows/check-links-book.yaml - web/book/** dotnet: - prqlc/bindings/prql-dotnet/** - prqlc/bindings/prqlc-c/** - .github/workflows/test-dotnet.yaml devcontainer-push: - .devcontainer/**/*Dockerfile - .github/workflows/build-devcontainer.yaml - Taskfile.yaml devcontainer-build: - .devcontainer/**/*Dockerfile - .github/workflows/build-devcontainer.yaml - Taskfile.yaml grammars: - grammars/** elixir: - prqlc/bindings/elixir/** - prqlc/bindings/prqlc-c/** - .github/workflows/test-elixir.yaml java: - prqlc/bindings/java/** - prqlc/bindings/prqlc-c/** - .github/workflows/test-java.yaml js: - prqlc/bindings/js/** - .github/workflows/test-js.yaml prqlc-c: - prqlc/bindings/prqlc-c/** - .github/workflows/test-prqlc-c.yaml main: - "**/Cargo.*" - .github/** - .config/** nightly: - .github/workflows/nightly.yaml - .github/workflows/release.yaml - Cargo.toml - Cargo.lock - rust-toolchain.toml - .cargo/** php: - prqlc/bindings/php/** - prqlc/bindings/prqlc-c/** - .github/workflows/test-php.yaml python: - prqlc/bindings/prqlc-python/** - .github/workflows/test-python.yaml rust: - "**/*.rs" - prqlc/** - web/book/** - .github/workflows/test-rust.yaml taskfile: # Run taskfile test on any Taskfile change, since the tasks pull in tasks from # other taskfiles. (But we don't run the container rebuilds, since those are # much heavier) - "**/Taskfile.yaml" web: - "web/**" - ".github/workflows/build-web.yaml" - "**.md" # We put a few of the more complex rules as steps here, rather than having # them inline. There's no strict delineation between logic here vs. inline. - id: nightly # TODO: actionlint annoyingly blocks this — try and find a way of getting # it back without too much trouble... # contains(github.event.pull_request.title, '!') || run: echo "run=${{ steps.changes.outputs.nightly == 'true' || contains(github.event.pull_request.labels.*.name, 'pr-nightly') || github.event_name == 'schedule' }}" >>"$GITHUB_OUTPUT" - id: nightly-upstream run: echo "run=${{ github.event_name == 'schedule' && github.repository_owner == 'prql' }}" >>"$GITHUB_OUTPUT" - id: main run: echo "run=${{ steps.changes.outputs.main == 'true' || github.ref == 'refs/heads/main' || steps.nightly.outputs.run == 'true' }}" >> "$GITHUB_OUTPUT" - id: devcontainer-push # We push the devcontainer if the files have changed, and we've merged # to main. run: echo "run=${{ steps.changes.outputs.devcontainer-push == 'true' && github.ref == 'refs/heads/main' && github.event_name == 'push' }}" >> "$GITHUB_OUTPUT" - id: devcontainer-build run: echo "run=${{ steps.devcontainer-push.outputs.run == 'true' || steps.changes.outputs.devcontainer-build == 'true' || steps.changes.outputs.nightly-upstream == 'true' }}" >> "$GITHUB_OUTPUT" test-rust: needs: rules if: needs.rules.outputs.rust == 'true' || needs.rules.outputs.main == 'true' uses: ./.github/workflows/test-rust.yaml strategy: matrix: include: - target: x86_64-unknown-linux-gnu os: ubuntu-24.04 features: default,test-dbs-external # Only run wasm on ubuntu, given it's the same rust target. (There is # a possibility of having a failure on just one platform, but it's # quite unlikely. If we do observe this, we can add those tests them # to nightly. - target: wasm32-unknown-unknown os: ubuntu-24.04 features: default with: os: ubuntu-24.04 target: ${{ matrix.target }} features: ${{ matrix.features }} nightly: ${{ needs.rules.outputs.nightly == 'true' }} test-python: needs: rules if: needs.rules.outputs.python == 'true' || needs.rules.outputs.main == 'true' uses: ./.github/workflows/test-python.yaml with: # Only run on ubuntu unless there's a lang-specific change or we're # running nightly. # # An alternative to these somewhat horrible expressions would be # `test-python` & `test-python-more` workflows; though it would use up our # 20 workflow limit. oss: ${{ (needs.rules.outputs.python == 'true' || needs.rules.outputs.nightly == 'true') && '["ubuntu-24.04", "macos-15", "windows-latest"]' || '["ubuntu-24.04"]' }} test-js: needs: rules if: needs.rules.outputs.js == 'true' || needs.rules.outputs.main == 'true' uses: ./.github/workflows/test-js.yaml with: # Only run on ubuntu unless there's a lang-specific change or we're running nightly. oss: ${{ (needs.rules.outputs.js == 'true' || needs.rules.outputs.nightly == 'true') && '["ubuntu-24.04", "macos-15", "windows-latest"]' || '["ubuntu-24.04"]' }} test-dotnet: needs: rules if: needs.rules.outputs.dotnet == 'true' || needs.rules.outputs.main == 'true' uses: ./.github/workflows/test-dotnet.yaml test-php: needs: rules if: needs.rules.outputs.php == 'true' || needs.rules.outputs.main == 'true' uses: ./.github/workflows/test-php.yaml test-java: needs: rules if: needs.rules.outputs.java == 'true' || needs.rules.outputs.main == 'true' uses: ./.github/workflows/test-java.yaml with: # Currently we never run windows oss: ${{ (needs.rules.outputs.java == 'true' || needs.rules.outputs.nightly == 'true') && '["ubuntu-24.04", "macos-15"]' || '["ubuntu-24.04"]' }} test-elixir: needs: rules if: needs.rules.outputs.elixir == 'true' || needs.rules.outputs.main == 'true' uses: ./.github/workflows/test-elixir.yaml with: # Currently we never run Mac, see prql-elixir docs for details oss: ${{ (needs.rules.outputs.elixir == 'true' || needs.rules.outputs.nightly == 'true') && '["ubuntu-24.04", "windows-2022"]' || '["ubuntu-24.04"]' }} test-prqlc-c: needs: rules if: needs.rules.outputs.prqlc-c == 'true' || needs.rules.outputs.main == 'true' uses: ./.github/workflows/test-prqlc-c.yaml # Disabled due to https://github.com/PRQL/prql/pull/4876 # To re-enable, uncomment this entire job block # test-taskfile: # needs: rules # if: | # needs.rules.outputs.taskfile == 'true' || # needs.rules.outputs.nightly-upstream == 'true' # runs-on: macos-15 # steps: # - name: 📂 Checkout code # uses: actions/checkout@v5 # - run: ./.github/workflows/scripts/set_version.sh # - uses: actions/setup-python@v6 # with: # python-version: "3.11" # - name: 💰 Cache # uses: Swatinem/rust-cache@v2 # with: # prefix-key: ${{ env.version }}-${{ hashFiles('./Cargo.lock') }} # # The mac rust cache key. It's not _that_ useful since this will build # # much more, but it's better than nothing. This task can't have our own # # cache, since we're out of cache space and this workflow takes 1.5GB. # shared-key: rust-aarch64-apple-darwin # save-if: false # - name: Install Task # uses: go-task/setup-task@v1 # with: # repo-token: ${{ secrets.GITHUB_TOKEN }} # # Required because of https://github.com/cargo-bins/cargo-binstall/issues/1254 # - run: brew install bash # - run: task install-brew-dependencies # - run: task setup-dev # # This also encompasses `build-all` # - run: task test-all # - run: task prqlc:test # - run: task lint test-rust-main: needs: rules if: needs.rules.outputs.main == 'true' strategy: fail-fast: false matrix: include: - os: macos-15 target: aarch64-apple-darwin features: default,test-dbs - os: windows-latest target: x86_64-pc-windows-msvc # We'd like to reenable integration tests on Windows, ref https://github.com/wangfenjin/duckdb-rs/issues/179. features: default - os: ubuntu-24.04 target: x86_64-unknown-linux-gnu # One test with no features features: "" # TODO: potentially enable these # - os: ubuntu-24.04 # target: aarch64-unknown-linux-musl uses: ./.github/workflows/test-rust.yaml with: os: ${{ matrix.os }} target: ${{ matrix.target }} features: ${{ matrix.features }} build-web: needs: rules if: needs.rules.outputs.web == 'true' || needs.rules.outputs.main == 'true' uses: ./.github/workflows/build-web.yaml lint-megalinter: uses: ./.github/workflows/lint-megalinter.yaml publish-web: uses: ./.github/workflows/publish-web.yaml if: contains(github.event.pull_request.labels.*.name, 'pr-publish-web') nightly: needs: rules uses: ./.github/workflows/nightly.yaml if: needs.rules.outputs.nightly == 'true' secrets: inherit check-links-markdown: needs: rules # Using lychee via pre-commit for consistency with local development. # Lychee has better retry support for transient network errors than markdown-link-check. runs-on: ubuntu-24.04 steps: - name: 📂 Checkout code uses: actions/checkout@v5 - uses: baptiste0928/cargo-install@v3 with: crate: lychee - uses: actions/setup-python@v6 with: python-version: "3.11" - name: Install pre-commit run: pip install pre-commit - name: Cache lychee results uses: actions/cache@v4 with: path: .lycheecache key: lychee-${{ github.sha }} restore-keys: lychee- # For PRs: get list of modified markdown files - name: Get modified files if: needs.rules.outputs.nightly != 'true' id: changed-files uses: tj-actions/changed-files@v47 with: files: | **/*.md **/*.rst - name: Link Checker (All files - Nightly) if: needs.rules.outputs.nightly == 'true' run: pre-commit run --hook-stage manual lychee-all --all-files continue-on-error: true - name: Link Checker (Modified files only - PRs) if: needs.rules.outputs.nightly != 'true' && steps.changed-files.outputs.any_modified == 'true' run: pre-commit run lychee --files ${{ steps.changed-files.outputs.all_changed_files }} continue-on-error: true check-links-book: # We also have a check-links-markdown job, however it will not spot mdbook # mistakes such as forgetting to list an .md file in SUMMARY.md. # Running a link checker on the generated HTML is more reliable. needs: rules if: needs.rules.outputs.book == 'true' || needs.rules.outputs.nightly == 'true' runs-on: ubuntu-24.04 steps: - name: 📂 Checkout code uses: actions/checkout@v5 - uses: baptiste0928/cargo-install@v3 with: crate: mdbook - uses: baptiste0928/cargo-install@v3 with: crate: mdbook-footnote # the link checker - uses: baptiste0928/cargo-install@v3 with: crate: hyperlink - run: ./.github/workflows/scripts/set_version.sh - name: Cache uses: Swatinem/rust-cache@v2 with: prefix-key: ${{ env.version }}-${{ hashFiles('./Cargo.lock') }} shared-key: web # Created by `build-web` save-if: false # Only build the book — rather than `build-web` which also builds the playground - name: Build the mdbook run: mdbook build web/book/ - name: Check links run: hyperlink web/book/book/ measure-code-cov: runs-on: ubuntu-24.04 needs: rules steps: - name: 📂 Checkout code uses: actions/checkout@v5 - run: ./.github/workflows/scripts/set_version.sh - uses: actions/setup-python@v6 with: python-version: "3.11" - uses: baptiste0928/cargo-install@v3 with: crate: cargo-llvm-cov - name: 💰 Cache uses: Swatinem/rust-cache@v2 with: prefix-key: ${{ env.version }}-${{ hashFiles('./Cargo.lock') }} save-if: ${{ github.ref == 'refs/heads/main' }} # Ensure nothing remains from caching - run: cargo llvm-cov clean --workspace - run: # We considered moving to using `codecov.json` with # `--codecov --output-path=codecov.json` since that has branch & region # coverage. But the coverage is lower, in a way that doesn't represent # what is useful coverage cargo llvm-cov --cobertura --output-path=cobertura.xml --no-default-features --features=default,test-dbs - name: Upload code coverage results uses: actions/upload-artifact@v5 with: name: code-coverage-report path: cobertura.xml - name: Upload to codecov.io if: # This action raises an error on forks. It allows running on PRs to # the main repo, which is important. Rarely do we need this uploading # from forks so while we can reenable running from forks if it works, # it's not that important. # # As of 2024-06, codecov was still working through how they handle # forks / tokens on PRs given rate limits, expect some failures for a # bit. # # As of 2024-06, we're also seeing that uploading on schedule can # measure very slightly different coverage, which can then cause PRs # based off that base to show reduced coverage, and show a failure. So # we disable it on schedule. Not sure that's a perfect solution — is # it giving different coverage _because_ it's on schedule, or is it # random such that limiting to running on main will sometimes show # reduced coverage? Because we're making comparisons, reproducible # accuracy is important. ${{ github.repository_owner == 'prql' && github.event_name != 'schedule' }} uses: codecov/codecov-action@v5 with: files: cobertura.xml fail_ci_if_error: true # As discussed in # https://community.codecov.com/t/upload-issues-unable-to-locate-build-via-github-actions-api/3954, # without this the upload has a fairly high failure rate. The only # thing the token allows is uploading coverage, so there are # apparently no security risks. # # Edit: actually no luck, waiting on # https://github.com/codecov/codecov-action/issues/1469 token: cab4ace5-4f10-4027-8b5c-d79722234571 test-grammars: # Currently tests lezer grammars. We could split that out into a separate # job if we want when we add more. runs-on: ubuntu-24.04 needs: rules if: needs.rules.outputs.grammars == 'true' || needs.rules.outputs.main == 'true' steps: - name: 📂 Checkout code uses: actions/checkout@v5 - name: 🧅 Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: latest - name: Install dependencies working-directory: grammars/prql-lezer/ run: bun install - name: Build grammar working-directory: grammars/prql-lezer/ run: bun run build - name: Test grammar working-directory: grammars/prql-lezer/ run: bun run test build-devcontainer: needs: rules if: needs.rules.outputs.devcontainer-build == 'true' uses: ./.github/workflows/build-devcontainer.yaml # One problem with this setup is that if another commit is merged to main, # this workflow will cancel existing jobs, and so this won't get pushed. We # have another workflow which runs on each release, so the image should get # pushed eventually. The alternative is to have a separate workflow, but # then we can't use the nice logic of when to run the workflow that we've # built up here. with: # This needs to compare to the string `'true'`, because of GHA awkwardness push: ${{ needs.rules.outputs.devcontainer-push == 'true' }} test-msrv: runs-on: ubuntu-24.04 needs: rules if: needs.rules.outputs.nightly == 'true' steps: - name: 📂 Checkout code uses: actions/checkout@v5 - uses: baptiste0928/cargo-install@v3 with: crate: cargo-msrv # TODO: remove this version pinning # The latest 0.16 supports workspace inheritance, so the check will fail version: "0.15" # Note this currently uses a manually maintained key in # `prqlc/prqlc/Cargo.toml`, because of # https://github.com/foresterre/cargo-msrv/issues/590 - name: Verify minimum rust version — prqlc # Ideally we'd check all crates, ref https://github.com/foresterre/cargo-msrv/issues/295 working-directory: prqlc/prqlc run: cargo msrv verify test-deps-min-versions: runs-on: ubuntu-24.04 needs: rules if: needs.rules.outputs.nightly == 'true' steps: - name: 📂 Checkout code uses: actions/checkout@v5 - run: rustup override set nightly-2025-11-10 - uses: actions/setup-python@v6 with: python-version: "3.11" - uses: baptiste0928/cargo-install@v3 with: crate: cargo-hack - uses: baptiste0928/cargo-install@v3 with: crate: cargo-minimal-versions - run: ./.github/workflows/scripts/set_version.sh - name: 💰 Cache uses: Swatinem/rust-cache@v2 with: prefix-key: ${{ env.version }}-${{ hashFiles('./Cargo.lock') }} save-if: ${{ github.ref == 'refs/heads/main' }} - name: Verify minimum rust version run: cargo minimal-versions test --direct check-ok-to-merge: # This indicates to GitHub whether everything in this workflow has passed # and (unlike if we included each task in the branch's GitHub required # tests) will pass when a task is skipped. if: always() needs: - build-devcontainer - build-web - check-links-book - check-links-markdown - lint-megalinter - nightly - publish-web - test-deps-min-versions - test-dotnet - test-elixir - test-grammars - test-java - test-js - test-msrv - test-php - test-prqlc-c - test-python - test-rust - test-rust-main # - test-taskfile # Disabled due to https://github.com/PRQL/prql/pull/4876 runs-on: ubuntu-24.04 steps: - name: Decide whether the needed jobs succeeded or failed # https://github.com/re-actors/alls-green/issues/23 uses: re-actors/alls-green@cf9edfcf932a0ed6b431433fa183829c68b30e3f with: jobs: ${{ toJSON(needs) }} # We don't include `check-links-markdown`, since occasionally we'll want to merge # something which temporarily fails that, such as if we're changing the # location of a file in this repo which is linked to. # # We're currently including `nightly` because I'm not sure whether # it's always reliable; e.g. `cargo-audit` allowed-failures: | [ "check-links-markdown", "nightly" ] # We skip jobs deliberately, so we are OK if any are skipped. # # Copy-pasted from `needs`, since it needs to be a json list, so `${{ # toJSON(needs) }}` (which is a map) doesn't work. # https://github.com/re-actors/alls-green/issues/23 allowed-skips: | [ "build-devcontainer", "build-web", "check-links-book", "check-links-markdown", "lint-megalinter", "nightly", "publish-web", "test-deps-min-versions", "test-dotnet", "test-elixir", "test-grammars", "test-java", "test-js", "test-msrv", "test-php", "test-prqlc-c", "test-python", "test-rust", "test-rust-main", "test-taskfile" ] build-prqlc: runs-on: ${{ matrix.os }} needs: rules if: needs.rules.outputs.rust == 'true' || needs.rules.outputs.main == 'true' strategy: fail-fast: false matrix: include: # Match the features with the available caches from tests - os: ubuntu-24.04 target: x86_64-unknown-linux-musl features: default # TODO: Until we have tests for these, we don't have a cache for them. # If we can add tests, then re-enable them. They run on `release.yaml` # regardless. # # - os: ubuntu-24.04 # target: aarch64-unknown-linux-musl - os: macos-15 target: aarch64-apple-darwin features: default,test-dbs - os: windows-latest target: x86_64-pc-windows-msvc features: default steps: - name: 📂 Checkout code uses: actions/checkout@v5 - uses: ./.github/actions/build-prqlc with: target: ${{ matrix.target }} profile: dev features: ${{ matrix.features }} # We need consistent env vars across all workflows for the cache to work env: CARGO_TERM_COLOR: always CLICOLOR_FORCE: 1 RUSTFLAGS: "-C debuginfo=0" RUSTDOCFLAGS: "-Dwarnings" build-prqlc-c: runs-on: ${{ matrix.os }} needs: rules if: needs.rules.outputs.main == 'true' strategy: fail-fast: false matrix: include: # Match the features with the available caches from tests - os: ubuntu-24.04 target: x86_64-unknown-linux-musl features: default - os: macos-15 target: aarch64-apple-darwin features: default,test-dbs - os: windows-latest target: x86_64-pc-windows-msvc features: default steps: - name: 📂 Checkout code uses: actions/checkout@v5 - uses: ./.github/actions/build-prqlc-c with: target: ${{ matrix.target }} profile: dev features: ${{ matrix.features }} # We need consistent env vars across all workflows for the cache to work env: CARGO_TERM_COLOR: always CLICOLOR_FORCE: 1 RUSTFLAGS: "-C debuginfo=0" RUSTDOCFLAGS: "-Dwarnings" create-issue-on-nightly-failure: runs-on: ubuntu-24.04 needs: - check-ok-to-merge - rules if: ${{ always() && contains(needs.*.result, 'failure') && needs.rules.outputs.nightly-upstream == 'true' }} permissions: contents: read issues: write steps: - name: 📂 Checkout code uses: actions/checkout@v5 - uses: JasonEtco/create-an-issue@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} LINK: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} with: filename: .github/nightly-failure.md update_existing: true search_existing: open update-rust-toolchain: runs-on: ubuntu-24.04 needs: rules if: ${{ needs.rules.outputs.nightly-upstream == 'true' }} # Note that this doesn't change the minimum supported version, only the # default toolchain to run on. The minimum is defined by Cargo.toml's # metadata.msrv and is updated manually based on when build environments # such as debian & winget are updated. steps: - name: 📂 Checkout code uses: actions/checkout@v5 - uses: a-kenji/update-rust-toolchain@main with: # Discussion in #1561 minor-version-delta: 1 toolchain-path: "./rust-toolchain.toml" pr-title: "build: Update rust toolchain version" token: ${{ secrets.PRQL_BOT_GITHUB_TOKEN }} ================================================ FILE: .gitignore ================================================ .DS_Store dist target*/ *.out *.log lcov.info *.profraw **/.vscode/settings.json **/.vscode/launch.json **/.vscode/tasks.json .idea _*.prql /*.prql /*.sql _*.sql # Appears when using Docker container .Trash-0 **/node_modules/ .task .direnv *.snap.tmp # These shouldn't be committed, and cause watchers to re-run when they're # created, which we don't want. That said, if ignoring them causes confusion # (e.g. folks look at their git status to assess whether there are pending # snapshots), we can adjust. *.pending-snap *.snap.new *.my.csv log*.json log*.html cobertura.xml codecov.json .envrc # Some paths we don't want to include. We could ignore these further down the # tree, but prettier relies on this file rather than resolving all the way down. .venv .nox vendor .mypy_cache/ .hypothesis/ .aider* .env # Lychee link checker cache .lycheecache ================================================ FILE: .markdownlint-cli2.yaml ================================================ config: # We use prettier for line length & wrapping MD013: false # Code block style — generally this is fine, but it has false positives on # footnotes. See below re markdown-it-footnote issue. If we could resolve # that, we could turn this back on. MD046: false # markdownItPlugins: # # Doesn't seem to help with some issues — if someone wants to help resolve then # great, but it's no huge stress to have some false positives. # # https://github.com/DavidAnson/markdownlint/issues/689 # - ["markdown-it-footnote"] ================================================ FILE: .mega-linter.yaml ================================================ GITHUB_COMMENT_REPORTER: false DISABLE: - RUST - JAVASCRIPT - PYTHON DISABLE_LINTERS: - SPELL_CSPELL - CSS_STYLELINT - PHP_PSALM - PHP_PHPSTAN # Disabled for now as we couldn't figure out how to prevent false positives. #2069 - SQL_TSQLLINT - REPOSITORY_KICS - SPELL_LYCHEE # Throwing network errors. We already check link in other GH actions. - MARKDOWN_MARKDOWN_LINK_CHECK # Slow (40+ seconds). We already check links in other GH actions. - REPOSITORY_TRUFFLEHOG # Detecting secrets in .git/config, which is not even committed. - REPOSITORY_GRYPE # Slow (10+ seconds). Blocking unrelated PRs. We already have depandabot. - YAML_V8R # Slow (70+ seconds). We don't use YAML schema. - JSON_V8R # Failing for vscode-style syntax (comments). - REPOSITORY_GITLEAKS # False positive on codecov token, which according to codecov is fine to have hardcoded DISABLE_ERRORS_LINTERS: - COPYPASTE_JSCPD - REPOSITORY_TRIVY # Long system prompt line in claude.yaml exceeds yamllint's 500 char limit - YAML_YAMLLINT - REPOSITORY_CHECKOV - REPOSITORY_DEVSKIM - BASH_SHELLCHECK - C_CPPLINT - CPP_CPPLINT - DOCKERFILE_HADOLINT - HTML_DJLINT - HTML_HTMLHINT - JAVA_CHECKSTYLE - JAVA_PMD - JSON_JSONLINT - MAKEFILE_CHECKMAKE - MARKDOWN_MARKDOWN_LINK_CHECK # Prevents us from starting a new library, since it raises an error on unpublished libraries. Can remove after publishing... - REPOSITORY_DUSTILOCK - SPELL_MISSPELL # Disabled for now, as @max-sixty didn't know whether "Unable to locate the # project file. A project file (tsconfig.json or tsconfig.eslint.json) is # required in order to use ts-standard." was worth fixing, from #3608. Happy # for someone more informed to turn it back on. - TYPESCRIPT_STANDARD # Disabled for now, as @max-sixty didn't know how to fix # `./prqlc/bindings/php/tests/CompilerTest.php (trailing_comma_in_multiline)`. # Fine for someone else to take a look. - PHP_PHPCSFIXER PHP_PHPCS_ARGUMENTS: - --standard=PSR12 RAKU_RAKU_ARGUMENTS: -I ./grammars/raku/lib/ ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: end-of-file-fixer exclude: '(.*\.snap|.*render-link.html|head.hbs)' - id: check-yaml - id: mixed-line-ending - id: trailing-whitespace # rustfmt handles rust files, and in some snapshots we expect trailing spaces. exclude: '.*\.(rs|snap)$' - repo: https://github.com/crate-ci/typos rev: v1 hooks: - id: typos # https://github.com/crate-ci/typos/issues/347 pass_filenames: false - repo: https://github.com/rbubley/mirrors-prettier rev: v3.8.1 hooks: - id: prettier additional_dependencies: - prettier # TODO: This doesn't seem to work, would be great to fix. # https://github.com/PRQL/prql/issues/3078 - prettier-plugin-go-template - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.6 hooks: - id: ruff args: [--fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-clang-format rev: v22.1.1 hooks: - id: clang-format types_or: [c, c++] - repo: https://github.com/r0x0d/pre-commit-rust rev: v1.0.1 hooks: - id: fmt - repo: https://github.com/r0x0d/pre-commit-rust rev: v1.0.1 hooks: - id: clippy stages: [manual] - repo: https://github.com/rhysd/actionlint rev: v1.7.11 hooks: - id: actionlint # Link checker using local hooks with language: rust # Pre-commit will automatically install lychee via cargo - repo: local hooks: - id: lychee name: lychee (local links only) entry: lychee language: rust additional_dependencies: ["cli:lychee:0.20.1"] files: \.(md|rst)$ exclude: ^web/website/themes/ args: - --no-progress # For PRs: whitelist PRQL domains and local files only # Pattern matches: # - github.com/PRQL/* and github.com/prql/* # - prql-lang.org/* # - raw.githubusercontent.com/PRQL/* and raw.githubusercontent.com/prql/* # - file://* (local/relative links) # All other external links are skipped for faster PR checks - --include=^(https://(github\.com/(PRQL|prql)|prql-lang\.org|raw\.githubusercontent\.com/(PRQL|prql))|file://) - id: lychee-all name: lychee-all (all links) entry: lychee language: rust additional_dependencies: ["cli:lychee:0.20.1"] stages: [manual] files: \.(md|rst)$ exclude: ^web/website/themes/ args: ["--config=.config/lychee.toml", "--no-progress"] - repo: local hooks: - id: no-dbg name: no-dbg description: We shouldn't merge code with `dbg!` in language: pygrep types: ["rust"] entry: "dbg!" - repo: local hooks: - id: prql-codeblock name: Prevent prql codeblocks evaluating in book description: prql code blocks are evaluated and replaced in the book; instead use `prql no-eval` language: pygrep entry: "```prql$" files: 'CHANGELOG\.md$' # This is quite strict, and doesn't fix a large enough share of the issues it # finds, so we don't include it. But it's reasonable to run every now & again # manually and take its fixes. # # - repo: https://github.com/DavidAnson/markdownlint-cli2 # rev: v0.5.1 # hooks: # - id: markdownlint-cli2 # args: ["--fix"] # additional_dependencies: # - markdown-it-footnote ci: # Currently network access isn't supported in the CI product. skip: [fmt, lychee, lychee-all] autoupdate_commit_msg: "chore: pre-commit autoupdate" ================================================ FILE: .prettierignore ================================================ # prettier respects the root `.gitignore` file, so prefer that for files which # we also want to ignore from our version control. This should contain files we # want to track but not format. **/*.rs **/*.min.* /web/book/theme/highlight.js /web/playground/build /web/website/public /web/book/book # TODO: move these into content out of layouts /web/website/themes/prql-theme/layouts/_default/_markup/render-link.html # TODO: fix the go-template issue in `.prettierrc.yaml` **/*.html ================================================ FILE: .prettierrc.yaml ================================================ proseWrap: always # TODO: fix # overrides: # # https://github.com/NiklasPor/prettier-plugin-go-template # - files: # - "*.html" # options: # parser: "go-template" ================================================ FILE: .sqlfluff ================================================ [sqlfluff] dialect = ansi exclude_rules = references.keywords ================================================ FILE: .typos.toml ================================================ [files] extend-exclude = [ "web/book/theme/highlight.js", "prqlc/prqlc/tests/integration/data/", "prqlc/prqlc/tests/integration/snapshots/", "web/website/themes/prql-theme/static/plugins/bootstrap", "web/website/themes/prql-theme/static/plugins/highlight/highlight.min.js", ] [default.extend-words] # in grammars/KSyntaxHighlighting/prql.xml datas = "datas" # Java test framework testng = "testng" # Readme highlighting the first characters of these words anguage = "anguage" elational = "elational" ipelined = "ipelined" uery = "uery" # `wee-alloc` is a crate name wee = "wee" flate = "flate" # distinct_ons is correct (SQL DISTINCT ON) ons = "ons" # SQL UNIONs - typos incorrectly thinks UNIO should be UNION # This happens because typos tokenizes UNIONs and finds UNIO UNIO = "UNIO" ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ // Keep in sync with Taskfile.yaml "prql-lang.prql-vscode", "rust-lang.rust-analyzer", "mitsuhiko.insta", "esbenp.prettier-vscode", "budparr.language-hugo-vscode" ] } ================================================ FILE: CHANGELOG.md ================================================ # PRQL Changelog ## [unreleased] **Language**: **Features**: **Fixes**: **Documentation**: **Web**: **Integrations**: **Internal changes**: **New Contributors**: ## 0.13.11 — 2026-03-19 0.13.11 has 75 commits from 7 contributors. Selected changes: **Features**: - Add support for `date.to_text` for BigQuery (@segv, #5712) **Fixes**: - Prevent panic on bare `*` in `select` (@max-sixty, #5696) - Handle partial application of transforms in user-defined functions (@max-sixty, #5663; reported by @Rafferty97) - Keep Computes with Aggregate when Filter follows (@max-sixty, #5639; reported by @matiastoro) - Preserve sort before take in CTE (@max-sixty, #5638; reported by @lukapeschke) - Preserve sort with empty group `{}` (@max-sixty, #5635) - Return proper error for bare lambda expressions (@max-sixty, #5634) - Prevent DISTINCT ON internal sorting from leaking past joins (@max-sixty, #5633; reported by @annashmatko) - Fix invalid MSSQL SQL when DISTINCT and FETCH are combined (@max-sixty, #5630) **Documentation**: - Editorial tweaks (@richb-hanover, #5645) **Integrations**: - [micro](https://micro-editor.github.io/) 2.0.15 has syntax highlighting for PRQL. (@vanillajonathan) **Internal changes**: - Use chumsky 0.12.0 for all targets (#5659) - Update rust toolchain version (#5673) - Fix mdbook admonition syntax for mdBook 0.5 native support (#5649) **New Contributors**: - @segv, with #5712 ## 0.13.10 — 2025-12-16 0.13.10 completes the npm OIDC publishing fix from 0.13.9. **Features**: - Add `read_json` support for DuckDB and ClickHouse (@adamnemecek, #5608) **Fixes**: - Fix `text.length` on Snowflake (@priithaamer, #5614) **Internal changes**: - Fix npm OIDC publishing by upgrading to Node 24 (@max-sixty, #5619) - Fix push-web-branch workflow permissions (@max-sixty, #5620) ## 0.13.9 — 2025-12-12 0.13.9 is a re-release of 0.13.8 with fixed npm publishing. **Internal changes**: - Use OIDC for npm publishing (@max-sixty, #5609) ## 0.13.8 — 2025-12-12 0.13.8 has 47 commits from 6 contributors. Selected changes: **Features**: - Add named parameter support for DuckDB read_parquet (@max-sixty, #5563) **Fixes**: - Snowflake interval quoting style (@priithaamer, #5604) - Return error instead of panicking when window rows/range is not a range (@max-sixty, #5603) - Add ORDER BY fallback for Snowflake window functions (@max-sixty, #5583) - DISTINCT ON to include wildcard when projection is empty (@max-sixty, #5562) - Lineage traces through CTEs to source tables (@nightscape, #5581) - Lineage works with unions (@nightscape, #5550) - Handle lineage correctly when group pipeline simplifies to non-TransformCall (@max-sixty, #5584) - Return error for join referencing inaccessible table (@max-sixty, #5587) - Error when table variable used in scalar context (@max-sixty, #5585) - Improve error messages for missing main pipeline (@max-sixty, #5565) - Provide clear error for empty tuple/array in from (@max-sixty, #5564) - Improve error message for negative numbers in transforms (@max-sixty, #5566) **Documentation**: - Close HTML comment in sort.md documentation (@max-sixty, #5597) **Integrations**: - Add grammar file for Raku (@vanillajonathan, #5576) **Internal changes**: - Upgrade mdbook to 0.5.0 (#5568) - Update macOS runners to version 15 (#5596) - Remove 2nd person from error messages (#5567) **New Contributors**: - @nightscape, with #5550 ## 0.13.7 — 2025-11-08 0.13.7 has 8 commits from 6 contributors. Selected changes: **Fixes**: - Fix INTERVAL quoting in Redshift (@lukapeschke, #5545) - Fix support for text.contains in sql.redshift target (@dimtion, #5549) - Fix division in Redshift produces float (@priithaamer, #5546) - Use || operator over CONCAT for std.concat in Redshift (@lukapeschke, #5540) **Web**: - Bump vite from 7.1.11 to 7.2.0 in playground (#5547) ## 0.13.6 — 2025-11-01 0.13.6 has 40 commits from 6 contributors. Selected changes: **Features**: - Add support for Redshift dialect (@priithaamer, #5537) **Fixes**: - Fix append regression in 0.13.5 (@priithaamer, #5495) - Fix s-string escaping (@priithaamer, #5497) - Filter out attestation manifests in platform verification (@max-sixty, #5509) **Internal changes**: - Migrate devcontainer build from QEMU to native ARM64 runners (@max-sixty, #5506) - Migrate from markdown-link-check to lychee (@max-sixty, #5519) - Use zero-copy slice for binary/hex/octal number parsing (perf improvement, @max-sixty, #5488) - Update rust toolchain version (#5536) - Do not allow incompatible rust-version dependencies (@eitsupi, #5493) - Pin typos to v1.37.2 (@max-sixty, #5510) ## 0.13.5 — 2025-10-09 0.13.5 has 237 commits from 14 contributors. Selected changes: **Features**: - Support for SQL arrays as `s[...]` syntax (@Robert Valek, #5312) - Extract SQL column names from s-string tables when possible (@lukapeschke, #5310) **Fixes**: - Sort step before an aggregate step no longer requires its columns to avoid a group by clause error (@julien-pinchelimouroux, #5347) - Always add quotes on identifiers for Snowflake dialect (@julien-pinchelimouroux, #5461) - Join with table containing column named "source" now works correctly (@Priit Haamer, #5468) - Columns required by sorting are properly redirected (@lukapeschke, #5464) - Ensure sorts are done on columns of the right table (@lukapeschke, #5338) - Deduplicate selected items in gen_projection (@lukapeschke, #5305) - Handle complex append cases (@Elouan Poupard-Cosquer, #5366) - Improve requirement logic (@Elouan Poupard-Cosquer, #5357) - Avoid type mismatch with Postgres in append (@Elouan Poupard-Cosquer, #5343) - Apply column order on CTEs in append (@Elouan Poupard-Cosquer, #5323) **Documentation**: - Fix binary literal example (@ftsfranklin, #5475) - Use correct table in grouping tutorial (@fnuttens, #5332) **Integrations**: - TEA 63.3.1, a Qt-based text editor has syntax highlighting for PRQL (@vanillajonathan, #5220) - Micro text editor grammar is now upstream (@vanillajonathan, #5353) - Add LSP stub (@vanillajonathan, #5197) **Internal changes**: - Upgrade parser and lexer to chumsky 0.11, providing a 7x performance improvement (#5223, #5476, #5477) - Set Rust linker on win64, fix build crash (@kgutwin, #5345) - Integration tests compile all dialects and diff (@kgutwin, #5344) **New Contributors**: - @Elouan Poupard-Cosquer, with #5366 - @Priit Haamer, with #5468 - @Robert Valek, with #5312 - @fnuttens, with #5332 - @ftsfranklin, with #5475 - @julien-pinchelimouroux, with #5347 ## 0.13.4 — 2025-03-26 0.13.4 is a small bugfix release. It has 57 commits from 10 contributors. Selected changes (in particular, a few bugfixes aren't listed here): **Integrations**: - Add syntax highlight file for KSyntaxHighlighting. (@vanillajonathan, #5177) - Add syntax highlight file for Vim. (@vanillajonathan, #5185) - Add syntax highlight file for GNU Emacs. (@vanillajonathan, #5189) - [Kakoune](https://kakoune.org/), a terminal-based text editor has syntax highlighting for PRQL. (@vanillajonathan) - [Neovim](https://neovim.io/) 0.11 has syntax highlighting for PRQL. (@vanillajonathan) ## 0.13.3 — 2025-01-25 0.13.3 is a small release containing a few bug fixes and improvements. It has 86 commits from 10 contributors. Selected changes: **Fixes**: - Sort steps in sub-pipelines no longer cause a column lookup error (@lukapeschke, #5066) - Dereferencing of sort columns when rendering SQL now done in context of main pipeline (@kgutwin, #5098) **New Contributors**: - @lukapeschke, with #5066 ## 0.13.2 0.13.2 is a tiny release to fix an issue publishing 0.13.1 to crates.io. ## 0.13.1 0.13.1 is a small release containing a few bug fixes and improvements. Velocity has slowed down a bit in recent months, we're still hoping to finish the new resolver and the new formatter in the near future. It has 97 commits from 10 contributors. Selected changes: **Features**: - Add a option to the experimental documentation generator to output the docs in HTML format. The option is given using the `--format=html` option. (@vanillajonathan, 4791) - The version of the library is now read from `git describe`. This doesn't affect libraries built on git tags (such as our releases), only those built when developing. When reporting bugs, this helps identify the exact version. (@max-sixty & @m-span, #4804) **Fixes**: - Raw strings (`r"..."`) are retained through `prqlc fmt` (@max-sixty, #4848) - Strings containing an odd contiguous number of quotes are now delimited by an odd number of quotes when being formatted. The previous implementation would use an even number, which is invalid PRQL. (@max-sixty, #4850) - A few more keywords are quoted, such as `user`, which is a reserved keyword in PostgreSQL. (@max-sixty) ## 0.13.0 — 2024-07-25 0.13.0 brings a new debug logging framework, a big refactor of the parser, a new highlighter, an `**` operator for exponentiation, a few bug fixes, and lots of other changes. It has 153 commits from 11 contributors. Our work continues on rewriting the resolver and completing `prqlc fmt`. Selected changes: **Language**: - Parentheses are always required around pipelines, even within tuples. For example: ```prql no-eval from artists # These parentheses are now required derive {a=(b | math.abs)} # No change — doesn't affect expressions or function calls without pipelines derive {x = 3 + 4} ``` This is a small breaking change. The new behavior matches the existing documentation. (@max-sixty, #4775) - A new `**` operator for exponentiation. (@aljazerzen & @max-sixty, #4125) **Features**: - `prqlc compile --debug-log=log.html` will generate an HTML file with a detailed log of the compilation process. (@aljazerzen, #4646) - Added `prqlc debug json-schema` command to auto-generate JSON Schema representations of commonly exposed IR types such as PL and RQ. (@kgutwin, #4698) - Add documentation comments to the output of the documentation generator. (@vanillajonathan, #4729) - Add CLI syntax highlighting to `prqlc`. You can try it as `prqlc experimental highlight example.prql`. (@vanillajonathan, #4755) **Fixes**: - Using `in` with an empty array pattern (e.g. `expr | in []`) will now output a constant `false` condition instead of an `expr IN ()`, which is syntactically invalid in some SQL dialects (@Globidev, #4598) **Integrations**: - The Snap package previously released on the edge channel is now released on the stable channel. (@vanillajonathan, #4784) **Internal changes**: - Major reorganization of `prqlc-parser` — `prqlc-ast` is merged into `prqlc-parser`, and `prqlc-parser`'s files are rearranged, including its exports. This is part of an effort to modularize the compiler by stage, reducing the amount of context that's required to understand a single stage. There will likely be some further changes (more detail in the PR description). (@m-span, #4634) - This is a breaking change for any libraries that depend on `prqlc-parser` (which should be fairly rare). - Renamed `prql-compiler-macros` to `prqlc-macros` for consistency with other crates (@max-sixty, #4565) - `prql-compiler`, the old name for `prqlc`, is removed as a facade to `prqlc`. It had been deprecated for a few versions and will no longer be updated. (@max-sixty) - New benchmarks (@max-sixty, #4654) **New Contributors**: - @Globidev, with #4598 ## 0.12.2 — 2024-06-10 0.12.2 is a very small release which renames `prql-js` to `prqlc-js` to match our standard naming scheme. Within node the package is imported as `prqlc`. It also fixes a mistake in the `prqlc-python` release pipeline. ## 0.12.1 — 2024-06-09 0.12.1 is a tiny hotfix release which fixes how intra-prql crate dependencies were specified. ## 0.12.0 — 2024-06-08 0.12.0 contains a few months of smaller features. Our focus has been on rewriting the resolver, an effort that is still ongoing. It has 239 commits from 12 contributors. Selected changes (most are not listed here, possibly we should be more conscientious about adding them...): **Features**: - Add `prqlc lex` command to the CLI (@max-sixty) - Add `prqlc debug lineage` command to the CLI, creating an expression lineage graph from a query (@kgutwin, #4533) - Initial implementation of an experimental documentation generator that generates Markdown documentation from `.prql` files. (@vanillajonathan, #4152). - Join's `side` parameter can take a reference that resolves to a literal (note: this is an experimental feature which may change in the future) (@kgutwin, #4499) **Fixes**: - Support expressions on left hand side of `std.in` operator. (@kgutwin, #4498) - Prevent panic for `from {}` and `std` (@m-span, #4538) **Web**: - The `browser` dist files are now built with `wasm-pack`'s `web` target. As a result, they should be usable as ES Modules, through JS CDNs, and for example with Observable Framework (@srenatus, #4274). **Integrations**: - The syntax highlighter package for Sublime Text is now [published](https://packagecontrol.io/packages/PRQL) (@vanillajonathan). - The [VSCode Great Icons](https://marketplace.visualstudio.com/items?itemName=emmanuelbeziat.vscode-great-icons) icon pack extension shows a database icon for `.prql` files. (@EmmanuelBeziat) - [Tokei](https://github.com/XAMPPRocky/tokei), a source lines of code counter now has support for `.prql` files. (@vanillajonathan) - Add syntax highlight file for the [micro](https://micro-editor.github.io/) text editor. (@vanillajonathan) **New Contributors**: - @srenatus, with #4274 - @jacquayj, with #4332 - @pdelewski, with #4337 - @m-span, with #4422 - @kgutwin, with #4498 ## 0.11.4 — 2024-02-25 0.11.4 is a hotfix release, fixing a CI issue that caused the CLI binaries to be built without the `cli` feature. ## 0.11.3 — 2024-02-10 0.11.3 is a very small release, mostly a rename of the Python bindings. The release has 13 commits from 4 contributors. **Internal changes**: - As part of making our names more consistent, the Python bindings are renamed. `prql-python` becomes a package published and importable as `prqlc`. The internal Rust crate is named `prqlc-python`. ## 0.11.2 — 2024-02-07 0.11.2 contains lots of internal changes, lots of syntax highlighting, and the beginning of `lutra`, a query runner. This release has 122 commits from 9 contributors. Selected changes: **Features**: - Initial implementation of `lutra`, a query runner. (@aljazerzen, #4182, #4174, #4134) - `prqlc fmt` works on projects with multiple files. (@max-sixty, #4028) **Fixes**: - Reduce stack memory usage (@aljazerzen, #4103) **Integrations**: - Add syntax highlight file for GtkSourceView. (@vanillajonathan, #4062) - Add syntax highlight file for CotEditor. (@vanillajonathan) - Add syntax highlight file for Sublime Text. (@vanillajonathan, #4127) - [sloc](https://github.com/flosse/sloc), a source lines of code counter now has support for `.prql` files. (@vanillajonathan) **Internal changes**: - `prql-compiler` has been renamed to `prqlc`, and we've established a more consistent naming scheme. The existing crate will still be published, re-exporting `prqlc`, so no dependencies will break. A future version will add a deprecation warning. - The `prqlc-clib` crate was renamed to `prqlc-c`, and associated artifacts were renamed. We're trying to make names consistent (ideally for the final time!), and have a plan to rename some other bindings. (@max-sixty, #4077) - Add lots of whitespace items to the lexer, in preparation for the completion of `prqlc fmt` (@max-sixty, #4109, #4105) - Table declarations (@aljazerzen, #4126) **New Contributors**: - @kaspermarstal, with #4124 ## 0.11.1 — 2023-12-26 0.11.1 fixes a couple of small bugs; it comes a few days after 0.11. This release has 16 commits from 6 contributors. Selected changes: **Features**: - Infer the type of array literals to be the union of types of its items. (@aljazerzen, #3989) - `prql` module is added and the `prql_version` function is renamed to the `prql.version` function. The old `prql_version` function is deprecated and will be removed in the future release. (@eitsupi, #4006) **Fixes**: - Do not compile to `DISTINCT ON` when `take n` is used with `group` for the targets `clickhouse`, `duckdb` and `postgres`. (@PrettyWood, #3988) - Fix `take` n rows for `mssql` dialect by switching from TOP to FETCH (@PrettyWood, #3994) ## 0.11.0 — 2023-12-19 0.11.0 introduces new `date`, `text` & `math` modules with lots of standard functions, including a new `date.to_text` function. It contains a few bugs fixes, and lots of internal improvements to the compiler. This release has 119 commits from 9 contributors. Selected changes: **Language**: - _Breaking_: `group`'s `by` columns are now excluded from the partition. (#3490) - _Breaking_: `round` is now in the `math` module and needs to be called via `math.round`. (#3928) - _Breaking_: `lower` and `upper` are now in the `text` module and need to be called via `text.lower` and `text.upper`. (#3913, #3973) **Features**: - The `std.in` function now supports a list of values (@PrettyWood, #3883) - Most standard mathematical functions are now supported: `abs`, `floor`, `ceil`, `pi`, `exp`, `ln`, `log10`, `log`, `sqrt`, `degrees`, `radians`, `cos`, `acos`, `sin`, `asin`, `tan`, `atan`, `pow` and `round`.\ Those functions are in the `math` module (@PrettyWood, #3909, #3916 & 3928) - Most standard string functions are now supported: `ltrim`, `rtrim`, `trim`, `length`, `extract`, `replace`. Utility functions `starts_with`, `contains` and `ends_with` are also available.\ Those functions are in the `text` module (@PrettyWood, #3913, #3973) - Formatting a date to a text is now available for Clickhouse, DuckDB, MySQL, MSSQL and Postgres. A new `date` module has been added with the `to_text` function (@PrettyWood, #3951, #3954 & #3955) **Fixes**: - Fix an issue with arithmetic precedence (@max-sixty, #3846) - `+` and `-` can be used after a cast (@PrettyWood, #3923) - The [Lezer](https://lezer.codemirror.net/) grammar had plenty of improvements and fixes. (@vanillajonathan) **Web**: - The Playground now uses [Vite](https://vitejs.dev/). (@vanillajonathan) **Internal changes**: - Bump `prql-compiler`'s MSRV to 1.70.0 (@eitsupi, #3876) **New Contributors**: - @PrettyWood, with #3883 ## 0.10.1 — 2023-11-14 0.10.1 is a small release containing some internal fixes of the compiler. This release has 36 commits from 7 contributors. Selected changes: **Features**: - The `std.sql.read_csv` function and the `std.sql.read_parquet` function supports the `sql.glaredb` target. (@eitsupi, #3749) **Fixes**: - Fix the bug of compiling to `DISTINCT ON` when `take 1` is used with `group by` for the targets `sql.clickhouse`, `sql.duckdb` and `sql.postgres`. (@aljazerzen, #3792) **Integrations**: - Enable integration tests for GlareDB. (@eitsupi, #3749) - [trapd00r/LS_COLORS](https://github.com/trapd00r/LS_COLORS), a collection of LS_COLORS definitions colorizes `.prql` files. (@vanillajonathan) - [vivid](https://github.com/sharkdp/vivid), a themeable LS_COLORS generator colorizes `.prql` files. (@vanillajonathan) - [colorls](https://github.com/athityakumar/colorls), displays `.prql` files with a database icon. (@vanillajonathan) - [Emoji File Icons](https://marketplace.visualstudio.com/items?itemName=mightbesimon.emoji-icons), a VS Code extension displays `.prql` files with a database emoji icon. (@vanillajonathan) - [eza](https://eza.rocks/), a modern ls replacement colorizes `.prql` files. (@vanillajonathan) - [lsd](https://github.com/lsd-rs/lsd), next gen ls command displays `.prql` files with a database icon. (@vanillajonathan) ## 0.10.0 — 2023-10-26 0.10.0 contains lots of small improvements, including support for new types of literal notation, support for `read_*` functions in more dialects, playground improvements, and a better Lezer grammar (which we're planning on using for a Jupyter extension). This release has 155 commits from 9 contributors. Selected changes: **Language**: - _Breaking:_ Case syntax now uses brackets `[]` rather than braces `{}`. To convert previous PRQL queries to this new syntax simply change `case { ... }` to `case [ ... ]`. (@AaronMoat, #3517) **Features**: - _Breaking_: The `std.sql.read_csv` function is now compiled to `read_csv` by default. Please set the target `sql.duckdb` to use the DuckDB's `read_csv_auto` function as previously. (@eitsupi, #3599) - _Breaking_: The `std.every` function is renamed to `std.all` (@aljazerzen, #3703) - The `std.sql.read_csv` function and the `std.sql.read_parquet` function supports the `sql.clickhouse` target. (@eitsupi, #1533) - Add `std.prql_version` function to return PRQL version (@hulxv, #3533) - A new type `anytype` is added. (@aljazerzen, #3703) - Add support for hex escape sequences in strings. Example `"Hello \x51"`. (@vanillajonathan, #3568) - Add support for long Unicode escape sequences. Example `"Hello \u{01F422}"`. (@vanillajonathan, #3569) - Add support for binary numerical notation. Example `filter status == 0b1111000011110000`. (@vanillajonathan, #3661) - Add support for hexadecimal numerical notation. Example `filter status == 0xff`. (@vanillajonathan, #3654) - Add support for octal numerical notation. Example `filter status == 0o777`. (@vanillajonathan, #3672) - New compile target `sql.glaredb` for [GlareDB](https://docs.glaredb.com/) and integration tests for it (However, there is a bug in the test and it is currently not running). (@universalmind303, @scsmithr, @eitsupi, #3669) **Web**: - Allow cmd-/ (Mac) or ctrl-/ (Windows) to toggle comments in the playground editor (@AaronMoat, #3522) - Limit maximum height of the playground editor's error panel to avoid taking over whole screen (@AaronMoat, #3524) - The playground now uses [Vite](https://vitejs.dev/) (@vanillajonathan). **Integrations**: - Add a CLI command `prqlc collect` to collect a project's modules into a single file (@aljazerzen, #3739) - Add a CLI command `prqlc debug expand-pl` to parse & and expand into PL without resolving (@aljazerzen, #3739) - Bump `prqlc`'s MSRV to 1.70.0 (@eitsupi, #3521) - [Pygments](https://pygments.org/), a syntax highlighting library now has syntax highlighting for PRQL. (@vanillajonathan, #3564) - [chroma](https://github.com/alecthomas/chroma), a syntax highlighting library written in Go and used by the static website generator [Hugo](https://gohugo.io/). (@vanillajonathan, #3597) - [scc](https://github.com/boyter/scc), a source lines of code counter now has support for `.prql` files. (@vanillajonathan) - [gcloc](https://github.com/JoaoDanielRufino/gcloc) a source lines of code counter now has support for `.prql` files. (@vanillajonathan) - [cloc](https://github.com/AlDanial/cloc) a source lines of code counter now has support for `.prql` files. (@AlDanial) - [gocloc](https://github.com/hhatto/gocloc) a source lines of code counter now has support for `.prql` files. (@vanillajonathan) - [The Quarto VS Code extension](https://marketplace.visualstudio.com/items?itemName=quarto.quarto) supports editing PRQL code blocks ([`prqlr`](https://prql-lang.org/book/project/bindings/r.html) is required to render Quarto Markdown with PRQL code blocks). (@jjallaire) **Internal**: - Rename some of the internal crates, and refactored their paths in the repo. (@aljazerzen, #3683). - Add a `justfile` for developers who prefer that above our `Taskfile.yaml` (@aljazerzen, #3681) **New Contributors**: - @hulxv, with #3533 - @AaronMoat, with #3522 - @jangorecki, with #3634 ## 0.9.5 — 2023-09-16 0.9.5 adds a line-wrapping character, fixes a few bugs, and improves our CI. The release has 77 commits from 8 contributors. Selected changes are below. Look out for some conference talks coming up over the next few weeks, including [QCon SF on Oct 2](https://qconsf.com/presentation/oct2023/prql-simple-powerful-pipelined-sql-replacement) and [date2day on Oct 12](https://www.data2day.de/veranstaltung-21353-0-prql-a-modern-language-for-data-transformation.html). **Language**: - A new line-wrapping character, for lines that are long and we want to break up into multiple physical lines. This is slightly different from from many languages — it's on the subsequent line: ```prql no-eval from artists select is_europe = \ country == "DE" \ || country == "FR" \ || country == "ES" ``` This allows for easily commenting out physical lines while maintaining a correct logical line; for example: ```diff from artists select is_europe = \ country == "DE" \ || country == "FR" \ || country == "FR" -\ || country == "ES" +#\ || country == "ES" ``` (@max-sixty, #3408) **Fixes**: - Fix stack overflow on very long queries in Windows debug builds (@max-sixty, #2908) - Fix panic when unresolved lineage appears in group or window (@davidot, #3266) - Fix a corner-case in handling precedence, and remove unneeded parentheses in some outputs (@max-sixty, #3472) **Web**: - Compiler panics are now printed to the console (@max-sixty, #3446) **Integrations**: - [Ace](https://ace.c9.io/), the JavaScript code editor now has syntax highlighting for PRQL. (@vanillajonathan, #3493) **Internal changes**: - Simplify & speed up lexer (@max-sixty, #3426, #3418) **New Contributors**: - @davidot, with #3450 ## 0.9.4 — 2023-08-24 0.9.4 is a small release with some improvements and bug fixes in the compiler and `prqlc`. And, the documentation and CI are continually being improved. This release has 110 commits from 9 contributors. Selected changes: **Features**: - Strings can be delimited with any odd number of quote characters. The logic for lexing quotes is now simpler and slightly faster. Escapes in single-quote-delimited strings escape single-quotes rather than double-quotes. (@max-sixty, #3274) **Fixes**: - S-strings within double braces now parse correctly (@max-sixty, #3265) **Documentation**: - New docs for strings (@max-sixty, #3281) **Web**: - Improve syntax highlighting for numbers in the book & website (@max-sixty, #3261) - Add ClickHouse integration to docs (@max-sixty, #3251) **Integrations**: - `prqlc` no longer displays a prompt when piping a query into its stdin (@max-sixty, #3248). - Add a minimal example for use `prql-lib` with Zig (@vanillajonathan, #3372) **Internal changes**: - Overhaul our CI to run a cohesive set of tests depending on the specific changes in the PR, and elide all others. This cuts CI latency to less than three minutes for most changes, and enables GitHub's auto-merge to wait for all relevant tests. It also reduces the CI time on merging to main, by moving some tests to only run on specific path changes or on our nightly run. We now have one label we can add to PRs to run more tests — `pr-nightly`. (@max-sixty, #3317 & others). - Auto-merge PRs for backports or pre-commit updates (@max-sixty, #3246) - Add a workflow to create an issue when the scheduled nightly workflow fails (@max-sixty, #3304) **New Contributors**: - @FinnRG, with #3292 - @sitiom, with #3353 ## 0.9.3 — 2023-08-02 0.9.3 is a small release, with mostly documentation, internal, and CI changes. This release has 85 commits from 10 contributors. We'd like to welcome @not-my-profile as someone who has helped with lots of internal refactoring in the past couple of weeks. **New Contributors**: - @vthriller, with #3171 - @postmeback, with #3216 ## 0.9.2 — 2023-07-25 0.9.2 is a hotfix release to fix an issue in the 0.9.0 & 0.9.1 release pipelines. ## 0.9.1 — 2023-07-25 0.9.1 is a hotfix release to fix an issue in the 0.9.0 release pipeline. ## 0.9.0 — 2023-07-24 0.9.0 is probably PRQL's biggest ever release. We have dialect-specific standard-libraries, a regex operator, an initial implementation of multiple-file projects & modules, lots of bug fixes, and many many internal changes. We've made a few backward incompatible syntax changes. Most queries will work with a simple find/replace; see below for details. The release has 421 commits from 12 contributors. A small selection of the changes: **Language**: - The major breaking change is a new syntax for lists, which have been renamed to _tuples_, and are now represented with braces `{}` rather than brackets `[]`. To convert previous PRQL queries to this new syntax simply change `[ ... ]` to `{ ... }`. We made the syntax change to incorporate arrays. Almost every major language uses `[]` for arrays. We are adopting that convention — arrays use `[]`, tuples will use `{}`. (Though we recognize that `{}` for tuples is also rare (Hi, Erlang!), but didn't want to further load parentheses with meaning.) Arrays are conceptually similar to columns — their elements have a single type. Array syntax can't contain assignments. As part of this, we've also formalized tuples as containing both individual items (`select {foo, baz}`), and assignments (`select {foo=bar, baz=fuz}`). - Some significant changes regarding SQL dialects: - Operators and functions can be defined on per-dialect basis. (@aljazerzen, #2681) - _Breaking_: The `sql.duckdb` target supports DuckDB 0.8 (@eitsupi, #2810). - _Breaking_: The `sql.hive` target is removed (@eitsupi, #2837). - New arithmetic operators. These compile to different function or operator depending on the target. - _Breaking_: Operator `/` now always performs floating division (@aljazerzen, #2684). See the [Division docs](https://prql-lang.org/book/reference/syntax/operators.html#division-and-integer-division) for details. - Truncated integer division operator `//` (@aljazerzen, #2684). See the [Division docs](https://prql-lang.org/book/reference/syntax/operators.html#division-and-integer-division) for details. - Regex search operator `~=` (@max-sixty, #2458). An example: ```prql no-eval from tracks filter (name ~= "Love") ``` ...compiles to; ```sql SELECT * FROM tracks WHERE REGEXP(name, 'Love') ``` ...though the exact form differs by dialect; see the [Regex docs](https://prql-lang.org/book/reference/syntax/operators.html#regex) for more details. - New aggregation functions: `every`, `any`, `average`, and `concat_array`. _Breaking:_ Remove `avg` in favor of `average`. - _Breaking:_ We've changed our function declaration syntax to match other declarations. Functions were one of the first language constructs in PRQL, and since then we've added normal declarations there's no compelling reason for functions to be different. ```prql no-eval let add = a b -> a + b ``` Previously, this was: ```prql no-eval func add a b -> a + b ``` - Experimental modules, which allow importing declarations from other files. Docs are forthcoming. - Relation literals create a relation (a "table") as an _array_ of _tuples_. This example demonstrates the new syntax for arrays `[]` and tuples `{}`. (@aljazerzen, #2605) ```prql no-eval from [{a=5, b=false}, {a=6, b=true}] filter b == true select a ``` - `this` can be used to refer to the current pipeline, for situations where plain column name would be ambiguous: ```prql no-eval from x derive sum = my_column select this.sum # does not conflict with `std.sum` ``` Within a `join` transform, there is also a reference to the right relation: `that`. - _Breaking:_ functions `count`, `rank` and `row_number` now require an argument of the array to operate on. In most cases you can directly replace `count` with `count this`. The `non_null` argument of `count` has been removed. **Features**: - We've changed how we handle colors. `Options::color` is deprecated and has no effect. Code which consumes `prql_compiler::compile` should instead accept the output with colors and use a library such as `anstream` to handle the presentation of colors. To ensure minimal disruption, `prql_compiler` will currently strip color codes when a standard environment variable such as `CLI_COLOR=0` is set or when it detects `stderr` is not a TTY. We now use the [`anstream`](https://github.com/rust-cli/anstyle) library in `prqlc` & `prql-compiler`. (@max-sixty, #2773) - `prqlc` can now show backtraces when the standard backtrace env var (`RUST_BACKTRACE`) is active. (@max-sixty, #2751) **Fixes**: - Numbers expressed with scientific notation — `1e9` — are now handled correctly by the compiler (@max-sixty, #2865). **Integrations**: - prql-python now provides type hints (@philpep, #2912) **Internal changes**: - Annotations in PRQL. These have limited support but are currently used to specify binding strengths. They're modeled after Rust's annotations, but with `@` syntax, more similar to traditional decorators. (#2729) ```prql no-eval @{binding_strength=11} let mod = l r -> s"{l} % {r}" ``` - Remove BigQuery's special handling of quoted identifiers, now that our module system handles its semantics (@max-sixty, #2609). - ClickHouse is tested in CI (@eitsupi, #2815). **New Contributors**: - @maxmcd, with #2533 - @khoa165, with #2876 - @philpep, with #2912 - @not-my-profile, with #2971 ## 0.8.1 — 2023-04-29 0.8.1 is a small release with a new `list-targets` command in `prqlc`, some documentation improvements, and some internal improvements. This release has 41 commits from 8 contributors. From the broader perspective of the project, we're increasing the relative prioritization of it being easy for folks to actually use PRQL — either with existing tools, or a tool we'd build. We'll be thinking about & discussing the best way to do that over the next few weeks. ## 0.8.0 — 2023-04-14 0.8.0 renames the `and` & `or` operators to `&&` & `||` respectively, reorganizes the Syntax section in the book, and introduces `read_parquet` & `read_csv` functions for reading files with DuckDB. This release has 38 commits from 8 contributors. Selected changes: **Features**: - Rename `and` to `&&` and `or` to `||`. Operators which are symbols are now consistently infix, while "words" are now consistently functions (@aljazerzen, #2422). - New functions `read_parquet` and `read_csv`, which mirror the DuckDB functions, instructing the database to read from files (@max-sixty, #2409). ## 0.7.1 — 2023-04-03 0.7.1 is a hotfix release to fix `prql-js`'s `npm install` behavior when being installed as a dependency. This release has 17 commits from 4 contributors. ## 0.7.0 — 2023-04-01 0.7.0 is a fairly small release in terms of new features, with lots of internal improvements, such as integration tests with a whole range of DBs, a blog post on Pi day, RFCs for a type system, and more robust language bindings. There's a very small breaking change to the Rust API, hence the minor version bump. Here's our April 2023 Update, from our [Readme](https://github.com/PRQL/prql/blob/main/README.md): > ### April 2023 update > > PRQL is being actively developed by a growing community. It's ready to use by > the intrepid, either as part of one of our supported extensions, or within > your own tools, using one of our supported language bindings. > > PRQL still has some minor bugs and some missing features, and probably is only > ready to be rolled out to non-technical teams for fairly simple queries. > > Here's our current [Roadmap](https://prql-lang.org/roadmap/) and our > [Milestones.](https://github.com/PRQL/prql/milestones) > > Our immediate focus for the code is on: > > - Building out the next few big features, including > [types](https://github.com/PRQL/prql/pull/1964) and > [modules](https://github.com/PRQL/prql/pull/2129). > - Ensuring our supported features feel extremely robust; resolving any > [priority bugs](https://github.com/PRQL/prql/issues?q=is%3Aissue+is%3Aopen+label%3Abug+label%3Apriority). > > We're also spending time thinking about: > > - Making it really easy to start using PRQL. We're doing that by building > integrations with tools that folks already use; for example our VS Code > extension & Jupyter integration. If there are tools you're familiar with > that you think would be open to integrating with PRQL, please let us know in > an issue. > - Making it easier to contribute to the compiler. We have a wide group of > contributors to the project, but contributions to the compiler itself are > quite concentrated. We're keen to expand this; > [#1840](https://github.com/PRQL/prql/issues/1840) for feedback. --- The release has 131 commits from 10 contributors. Particular credit goes to to @eitsupi & @jelenkee, who have made significant contributions, and @vanillajonathan, whose prolific contribution include our growing language bindings. A small selection of the changes: **Features**: - `prqlc compile` adds `--color` & `--include-signature-comment` options. (@max-sixty, #2267) **Web**: - Added the PRQL snippets from the book to the [Playground](https://prql-lang.org/playground/) (@jelenkee, #2197) **Internal changes**: - _Breaking_: The `compile` function's `Options` now includes a `color` member, which determines whether error messages use ANSI color codes. This is technically a breaking change to the API. (@max-sixty, #2251) - The `Error` struct now exposes the `MessageKind` enum. (@vanillajonathan, #2307) - Integration tests run in CI with DuckDB, SQLite, PostgreSQL, MySQL and SQL Server (@jelenkee, #2286) **New Contributors**: - @k-nut, with #2294 ## 0.6.1 — 2023-03-12 0.6.1 is a small release containing an internal refactoring and improved bindings for C, PHP & .NET. This release has 54 commits from 6 contributors. Selected changes: **Fixes**: - No longer incorrectly compile to `DISTINCT` when a `take 1` refers to a different set of columns than are in the `group`. (@max-sixty, with thanks to @cottrell, #2109) - The version specification of the dependency Chumsky was bumped from `0.9.0` to `0.9.2`. `0.9.0` has a bug that causes an infinite loop. (@eitsupi, #2110) **Documentation**: - Add a policy for which bindings are supported / unsupported / nascent. See for more details (@max-sixty, #2062) (@max-sixty, #2062) **Integrations**: - [prql-lib] Added C++ header file. (@vanillajonathan, #2126) **Internal changes**: - Many of the items that were in the root of the repo have been aggregated into `web` & `bindings`, simplifying the repo's structure. There's also `grammars` & `packages` (@max-sixty, #2135, #2117, #2121). ## 0.6.0 — 2023-03-08 0.6.0 introduces a rewritten parser, giving us the ability to dramatically improve error messages, renames `switch` to `case` and includes lots of minor improvements and fixes. It also introduces `loop`, which compiles to `WITH RECURSIVE`, as a highly experimental feature. There are a few cases of breaking changes, including switching `switch` to `case`, in case that's confusing. There are also some minor parsing changes outlined below. This release has 108 commits from 11 contributors. Selected changes: **Features**: - Add a (highly experimental) `loop` language feature, which translates to `WITH RECURSIVE`. We expect changes and refinements in upcoming releases. (#1642, @aljazerzen) - Rename the experimental `switch` function to `case` given it more closely matches the traditional semantics of `case`. (@max-sixty, #2036) - Change the `case` syntax to use `=>` instead of `->` to distinguish it from function syntax. - Convert parser from pest to Chumsky (@aljazerzen, #1818) - Improved error messages, and the potential to make even better in the future. Many of these improvements come from error recovery. - String escapes (`\n \t`). - Raw strings that don't escape backslashes. - String interpolations can only contain identifiers and not any expression. - Operator associativity has been changed from right-to-left to left-to-right to be more similar to other conventional languages. - `and` now has a higher precedence than `or` (of same reason as the previous point). - Dates, times and timestamps have stricter parsing rules. - `let`, `func`, `prql`, `case` are now treated as keywords. - Float literals without fraction part are not allowed anymore (`1.`). - Add a `--format` option to `prqlc parse` which can return the AST in YAML (@max-sixty, #1962) - Add a new subcommand `prqlc jinja`. (@aljazerzen, #1722) - _Breaking_: prql-compiler no longer passes text containing `{{` & `}}` through to the output. (@aljazerzen, #1722) For example, the following PRQL query ```prql no-eval from {{foo}} ``` was compiled to the following SQL previously, but now it raises an error. ```sql SELECT * FROM {{ foo }} ``` This pass-through feature existed for integration with dbt. We're again considering how to best integrate with dbt, and this change is based on the idea that the jinja macro should run before the PRQL compiler. If you're interested in dbt integration, subscribe or 👍 to . - A new compile target `"sql.any"`. When `"sql.any"` is used as the target of the compile function's option, the target contained in the query header will be used. (@aljazerzen, #1995) - Support for SQL parameters with similar syntax (#1957, @aljazerzen) - Allow `:` to be elided in timezones, such as `0800` in `@2020-01-01T13:19:55-0800` (@max-sixty, #1991). - Add `std.upper` and `std.lower` functions for changing string casing (@Jelenkee, #2019). **Fixes**: - `prqlc compile` returns a non-zero exit code for invalid queries. (@max-sixty, #1924) - Identifiers can contain any alphabetic unicode characters (@max-sixty, #2003) **Documentation**: - Operator precedence (@aljazerzen, #1818) - Error messages for invalid queries are displayed in the book (@max-sixty, #2015) **Integrations**: - [prql-php] Added PHP bindings. (@vanillajonathan, #1860) - [prql-dotnet] Added .NET bindings. (@vanillajonathan, #1917) - [prql-lib] Added C header file. (@vanillajonathan, #1879) - Added a workflow building a `.deb` on each release. (Note that it's not yet published on each release). (@vanillajonathan, #1883) - Added a workflow building a `.rpm` on each release. (Note that it's not yet published on each release). (@vanillajonathan, #1918) - Added a workflow building a Snap package on each release. (@vanillajonathan, #1881) **Internal changes**: - Test that the output of our nascent autoformatter can be successfully compiled into SQL. Failing examples are now clearly labeled. (@max-sixty, #2016) - Definition files have been added to configure [Dev Containers](https://containers.dev/) for Rust development environment. (@eitsupi, #1893, #2025, #2028) **New Contributors**: - @linux-china, with #1971 - @Jelenkee, with #2019 ## 0.5.2 — 2023-02-18 0.5.2 is a tiny release to fix an build issue in yesterday's `prql-js` 0.5.1 release. This release has 7 commits from 2 contributors. **New Contributors**: - @matthias-Q, with #1873 ## 0.5.1 — 2023-02-17 0.5.1 contains a few fixes, and another change to how bindings handle default target / dialects. This release has 53 commits from 7 contributors. Selected changes: **Fixes**: - Delegate dividing literal integers to the DB. Previously integer division was executed during PRQL compilation, which could be confusing given that behavior is different across DBs. Other arithmetic operations are still executed during compilation. (@max-sixty, #1747) **Documentation**: - Add docs on the `from_text` transform (@max-sixty, #1756) **Integrations**: - [prql-js] Default compile target changed from `Sql(Generic)` to `Sql(None)`. (@eitsupi, #1856) - [prql-python] Compilation options can now be specified from Python. (@eitsupi, #1807) - [prql-python] Default compile target changed from `Sql(Generic)` to `Sql(None)`. (@eitsupi, #1861) **New Contributors**: - @vanillajonathan, with #1766 ## 0.5.0 — 2023-02-08 0.5.0 contains a few fixes, some improvements to bindings, lots of docs improvements, and some work on forthcoming features. It contains one breaking change in the compiler's `Options` interface. This release has 74 commits from 12 contributors. Selected changes: **Features**: - Change public API to use target instead of dialect in preparation for feature work (@aljazerzen, #1684) - `prqlc watch` command which watches filesystem for changes and compiles .prql files to .sql (@aljazerzen, #1708) **Fixes**: - Support double brackets in s-strings which aren't symmetric (@max-sixty, #1650) - Support Postgres's Interval syntax (@max-sixty, #1649) - Fixed tests for `prql-elixir` with macOS (@kasvith, #1707) **Documentation**: - Add a documentation test for prql-compiler, update prql-compiler README, and include the README in the prql book section for Rust bindings. The code examples in the README are included and tested as doctests in the prql-compiler (@nkicg6, #1679) **Internal changes**: - Add tests for all PRQL website examples to prql-python to ensure compiled results match expected SQL (@nkicg6, #1719) **New Contributors**: - @ruslandoga, with #1628 - @RalfNorthman, with #1632 - @nicot, with #1662 ## 0.4.2 — 2023-01-25 **Features**: - New `from_text format-arg string-arg` function that supports JSON and CSV formats. _format-arg_ can be `format:csv` or `format:json`. _string-arg_ can be a string in any format. (@aljazerzen & @snth, #1514) ```prql no-eval from_text format:csv """ a,b,c 1,2,3 4,5,6 """ ``` ```prql no-eval from_text format:json ''' [{"a": 1, "b": "x", "c": false }, {"a": 4, "b": "y", "c": null }] ''' ``` ```prql no-eval from_text format:json '''{ "columns": ["a", "b", "c"], "data": [ [1, "x", false], [4, "y", null] ] }''' ``` For now, the argument is limited to string constants. **Fixes** - Export constructor for SQLCompileOptions (@bcho, #1621) - Remove backticks in count_distinct (@aljazerzen, #1611) **New Contributors** - @1Kinoti, with #1596 - @veenaamb, with #1614 ## 0.4.1 — 2023-01-18 0.4.1 comes a few days after 0.4.0, with a couple of features and the release of `prqlc`, the CLI crate. 0.4.1 has 35 commits from 6 contributors. **Features**: - Inferred column names include the relation name (@aljazerzen, #1550): ```prql no-eval from albums select title # name used to be inferred as title only select albums.title # so using albums was not possible here ``` - Quoted identifiers such as `dir/*.parquet` are passed through to SQL. (@max-sixty, #1516). - The CLI is installed with `cargo install prqlc`. The binary was renamed in 0.4.0 but required an additional `--features` flag, which has been removed in favor of this new crate (@max-sixty & @aljazerzen, #1549). **New Contributors**: - @fool1280, with #1554 - @nkicg6, with #1567 ## 0.4.0 — 2023-01-15 0.4.0 brings lots of new features including `case`, `select ![]` and numbers with underscores. We have initial (unpublished) bindings to Elixir. And there's the usual improvements to fixes & documentation (only a minority are listed below in this release). 0.4.0 also has some breaking changes: `table` is `let`, `dialect` is renamed to `target`, and the compiler's API has changed. Full details below. **Features**: - Defining a temporary table is now expressed as `let` rather than `table` (@aljazerzen, #1315). See the [tables docs](https://prql-lang.org/book/reference/declarations/variables.html) for details. - _Experimental:_ The [`case`](https://prql-lang.org/book/reference/syntax/case.html) function sets a variable to a value based on one of several expressions (@aljazerzen, #1278). ```prql no-eval derive var = case [ score <= 10 -> "low", score <= 30 -> "medium", score <= 70 -> "high", true -> "very high", ] ``` ...compiles to: ```sql SELECT *, CASE WHEN score <= 10 THEN 'low' WHEN score <= 30 THEN 'medium' WHEN score <= 70 THEN 'high' ELSE 'very high' END AS var FROM bar ``` Check out the [`case` docs](https://prql-lang.org/book/reference/syntax/case.html) for more details. - _Experimental:_ Columns can be excluded by name with `select` (@aljazerzen, #1329) ```prql no-eval from albums select ![title, composer] ``` - _Experimental:_ `append` transform, equivalent to `UNION ALL` in SQL. (@aljazerzen, #894) ```prql no-eval from employees append managers ``` Check out the [`append` docs](https://prql-lang.org/book/reference/stdlib/transforms/append.html) for more details. - Numbers can contain underscores, which can make reading long numbers easier (@max-sixty, #1467): ```prql no-eval from numbers select { small = 1.000_000_1, big = 5_000_000, } ``` - The SQL output contains a comment with the PRQL compiler version (@aljazerzen, #1322) - `dialect` is renamed to `target`, and its values are prefixed with `sql.` (@max-sixty, #1388); for example: ```prql no-eval prql target:sql.bigquery # previously was `dialect:bigquery` from employees ``` This gives us the flexibility to target other languages than SQL in the long term. - Tables definitions can contain a bare s-string (@max-sixty, #1422), which enables us to include a full CTE of SQL, for example: ```prql no-eval let grouping = s""" SELECT SUM(a) FROM tbl GROUP BY GROUPING SETS ((b, c, d), (d), (b, d)) """ ``` - Ranges supplied to `in` can be half-open (@aljazerzen, #1330). - The crate's external API has changed to allow for compiling to intermediate representation. This also affects bindings. See [`prql-compiler` docs](https://docs.rs/prql-compiler/latest/prql_compiler/) for more details. **Fixes**: [This release, the changelog only contains a subset of fixes] - Allow interpolations in table s-strings (@aljazerzen, #1337) **Documentation**: [This release, the changelog only contains a subset of documentation improvements] - Add docs on aliases in [Select](https://prql-lang.org/book/reference/stdlib/transforms/select.html) - Add JS template literal and multiline example (@BCsabaEngine, #1432) - JS template literal and multiline example (@BCsabaEngine, #1432) - Improve prql-compiler docs & examples (@aljazerzen, #1515) - Fix string highlighting in book (@max-sixty, #1264) **Web**: - The playground allows querying some sample data. As before, the result updates on every keystroke. (@aljazerzen, #1305) **Integrations**: [This release, the changelog only contains a subset of integration improvements] - Added Elixir integration exposing PRQL functions as NIFs (#1500, @kasvith) - Exposed Elixir flavor with exceptions (#1513, @kasvith) - Rename `prql-compiler` binary to `prqlc` (@aljazerzen #1515) **Internal changes**: [This release, the changelog only contains a subset of internal changes] - Add parsing for negative select (@max-sixty, #1317) - Allow for additional builtin functions (@aljazerzen, #1325) - Add an automated check for typos (@max-sixty, #1421) - Add tasks for running playground & book (@max-sixty, #1265) - Add tasks for running tests on every file change (@max-sixty, #1380) **New contributors**: - @EArazli, with #1359 - @boramalper, with #1362 - @allurefx, with #1377 - @bcho, with #1375 - @JettChenT, with #1385 - @BlurrechDev, with #1411 - @BCsabaEngine, with #1432 - @kasvith, with #1500 ## 0.3.1 - 2022-12-03 0.3.1 brings a couple of small improvements and fixes. **Features**: - Support for using s-strings for `from` (#1197, @aljazerzen) ```prql no-eval from s"SELECT * FROM employees WHERE foo > 5" ``` - Helpful error message when referencing a table in an s-string (#1203, @aljazerzen) **Fixes**: - Multiple columns with same name created (#1211, @aljazerzen) - Renaming via select breaks preceding sorting (#1204, @aljazerzen) - Same column gets selected multiple times (#1186, @mklopets) **Internal**: - Update Github Actions and Workflows to current version numbers (and avoid using Node 12) ## 0.3.0 — 2022-11-29 🎉 0.3.0 is the biggest ever change in PRQL's compiler, rewriting much of the internals: the compiler now has a semantic understanding of expressions, including resolving names & building a DAG of column lineage 🎉. While the immediate changes to the language are modest — some long-running bugs are fixed — this unlocks the development of many of the project's long-term priorities, such as type-checking & auto-complete. And it simplifies the building of our next language features, such as match-case expressions, unions & table expressions. @aljazerzen has (mostly single-handedly) done this work over the past few months. The project owes him immense appreciation. **Breaking changes**: We've had to make some modest breaking changes for 0.3: - _Pipelines must start with `from`_. For example, a pipeline with only `derive foo = 5`, with no `from` transform, is no longer valid. Depending on demand for this feature, it would be possible to add this back. - _Shared column names now require `==` in a join_. The existing approach is ambiguous to the compiler — `id` in the following example could be a boolean column. ```diff from employees -join positions [id] +join positions [==id] ``` - _Table references containing periods must be surrounded by backticks_. For example, when referencing a schema name: ```diff -from public.sometable +from `public.sometable` ``` **Features**: - Change self equality op to `==` (#1176, @aljazerzen) - Add logging (@aljazerzen) - Add clickhouse dialect (#1090, @max-sixty) - Allow namespaces & tables to contain `.` (#1079, @aljazerzen) **Fixes**: - Deduplicate column appearing in `SELECT` multiple times (#1186, @aljazerzen) - Fix uppercase table names (#1184, @aljazerzen) - Omit table name when only one ident in SELECT (#1094, @aljazerzen) **Documentation**: - Add chapter on semantics' internals (@aljazerzen, #1028) - Add note about nesting variables in s-strings (@max-sixty, #1163) **Internal changes**: - Flatten group and window (#1120, @aljazerzen) - Split ast into expr and stmt (@aljazerzen) - Refactor associativity (#1156, @aljazerzen) - Rename Ident constructor to `from_name` (#1084, @aljazerzen) - Refactor rq folding (#1177, @aljazerzen) - Add tests for reported bugs fixes in semantic (#1174, @aljazerzen) - Bump duckdb from 0.5.0 to 0.6.0 (#1132) - Bump once_cell from 1.15.0 to 1.16.0 (#1101) - Bump pest from 2.4.0 to 2.5.0 (#1161) - Bump pest_derive from 2.4.0 to 2.5.0 (#1179) - Bump sqlparser from 0.25.0 to 0.27.0 (#1131) - Bump trash from 2.1.5 to 3.0.0 (#1178) ## 0.2.11 — 2022-11-20 0.2.11 contains a few helpful fixes. Work continues on our `semantic` refactor — look out for 0.3.0 soon! Many thanks to @aljazerzen for his continued contributions to this. Note: 0.2.10 was skipped due to this maintainer's inability to read his own docs on bumping versions... **Features**: - Detect when compiler version is behind query version (@MarinPostma, #1058) - Add `__version__` to prql-python package (@max-sixty, #1034) **Fixes**: - Fix nesting of expressions with equal binding strength and left associativity, such as `a - (b - c)` (@max-sixty, #1136) - Retain floats without significant digits as floats (@max-sixty, #1141) **Documentation**: - Add documentation of `prqlr` bindings (@eitsupi, #1091) - Add a 'Why PRQL' section to the website (@max-sixty, #1098) - Add @snth to core-devs (@max-sixty, #1050) **Internal changes**: - Use workspace versioning (@max-sixty, #1065) ## 0.2.9 — 2022-10-14 0.2.9 is a small release containing a bug fix for empty strings. **Fixes**: - Fix parsing of empty strings (@aljazerzen, #1024) ## 0.2.8 — 2022-10-10 0.2.8 is another modest release with some fixes, doc improvements, bindings improvements, and lots of internal changes. Note that one of the fixes causes the behavior of `round` and `cast` to change slightly — though it's handled as a fix rather than a breaking change in semantic versioning. **Fixes**: - Change order of the `round` & `cast` function parameters to have the column last; for example `round 2 foo_col` / `cast int foo`. This is consistent with other functions, and makes piping possible: ```prql no-eval derive [ gross_salary = (salary + payroll_tax | as int), gross_salary_rounded = (gross_salary | round 0), ] ``` **Documentation**: - Split `DEVELOPMENT.md` from `CONTRIBUTING.md` (@richb-hanover, #1010) - Make s-strings more prominent in website intro (@max-sixty, #982) **Web**: - Add GitHub star count to website (@max-sixty, #990) **Integrations**: - Expose a shortened error message, in particular for the VS Code extension (@aljazerzen, #1005) **Internal changes**: - Specify 1.60.0 as minimum Rust version (@max-sixty, #1011) - Remove old `wee-alloc` code (@max-sixty, #1013) - Upgrade clap to version 4 (@aj-bagwell, #1004) - Improve book-building script in Taskfile (@max-sixty, #989) - Publish website using an artifact rather than a long-lived branch (@max-sixty, #1009) ## 0.2.7 — 2022-09-17 0.2.7 is a fairly modest release, six weeks after 0.2.6. We have some more significant features, including a `union` operator and an overhaul of our type system, as open PRs which will follow in future releases. We also have new features in the [VS Code extension](https://github.com/PRQL/prql-code), courtesy of @jiripospisil, including a live output panel. **Fixes**: - `range_of_ranges` checks the Range end is smaller than its start (@shuozeli, #946) **Documentation**: - Improve various docs (@max-sixty, #974, #971, #972, #970, #925) - Add reference to EdgeDB's blog post in our FAQ (@max-sixty, #922) - Fix typos (@kianmeng, #943) **Integrations**: - Add `prql-lib`, enabling language bindings with `go` (@sigxcpu76, #923) - Fix line numbers in JS exceptions (@charlie-sanders, #929) **Internal changes**: - Lock the version of the rust-toolchain, with auto-updates (@max-sixty, #926, #927) ## 0.2.6 — 2022-08-05 **Fixes**: - Adjust `fmt` to only escape names when needed (@aljazerzen, #907) - Fix quoting on upper case `table` names (@max-sixty, #893) - Fix scoping of identical column names from multiple tables (@max-sixty, #908) - Fix parse error on newlines in a `table` (@sebastiantoh 🆕, #902) - Fix quoting of upper case table names (@max-sixty, #893) **Documentation**: - Add docs on Architecture (@aljazerzen, #904) - Add Changelog (@max-sixty, #890 #891) **Internal changes**: - Start trial using Conventional Commits (@max-sixty, #889) - Add crates.io release workflow, docs (@max-sixty, #887) ## 0.2.5 - 2022-07-29 0.2.5 is a very small release following 0.2.4 yesterday. It includes: - Add the ability to represent single brackets in an s-string, with two brackets (#752, @max-sixty) - Fix the "Copy to Clipboard" command in the Playground, for Firefox (#880, @mklopets) ## 0.2.4 - 2022-07-28 0.2.4 is a small release following 0.2.3 a few days ago. The 0.2.4 release includes: - Enrich our CLI, adding commands to get different stages of the compilation process (@aljazerzen , #863) - Fix multiple `take n` statements in a query, leading to duplicate proxy columns in generated SQL (@charlie-sanders) - Fix BigQuery quoting of identifiers in `SELECT` statements (@max-sixty) - Some internal changes — reorganize top-level functions (@aljazerzen), add a workflow to track our Rust compilation time (@max-sixty), simplify our simple prql-to-sql tests (@max-sixty) Thanks to @ankane, `prql-compiler` is now available from homebrew core; `brew install prql-compiler`[^1]. [^1]: we still need to update docs and add a release workflow for this: ## 0.2.3 - 2022-07-24 A couple of weeks since the 0.2.2 release: we've squashed a few bugs, added some mid-sized features to the language, and made a bunch of internal improvements. The 0.2.3 release includes: - Allow for escaping otherwise-invalid identifiers (@aljazerzen & @max-sixty) - Fix a bug around operator precedence (@max-sixty) - Add a section the book on the language bindings (@charlie-sanders) - Add tests for our `Display` representation while fixing some existing bugs. This is gradually becoming our code formatter (@arrizalamin) - Add a "copy to clipboard" button in the Playground (@mklopets) - Add lots of guidance to our `CONTRIBUTING.md` around our tests and process for merging (@max-sixty) - Add a `prql!` macro for parsing a prql query at compile time (@aljazerzen) - Add tests for `prql-js` (@charlie-sanders) - Add a `from_json` method for transforming json to a PRQL string (@arrizalamin) - Add a workflow to release `prql-java` to Maven (@doki23) - Enable running all tests from a PR by adding a `pr-run-all-tests` label (@max-sixty) - Have `cargo-release` to bump all crate & npm versions (@max-sixty) - Update `prql-js` to use the bundler build of `prql-js` (@mklopets) As well as those contribution changes, thanks to those who've reported issues, such as @mklopets @huw @mm444 @ajfriend. From here, we're planning to continue squashing bugs (albeit more minor than those in this release), adding some features like `union`, while working on bigger issues such as type-inference. We're also going to document and modularize the compiler further. It's important that we give more people an opportunity to contribute to the guts of PRQL, especially given the number and enthusiasm of contributions to project in general — and it's not that easy to do so at the moment. While this is ongoing if anyone has something they'd like to work on in the more difficult parts of the compiler, let us know on GitHub or Discord, and we'd be happy to work together on it. Thank you! ## 0.2.2 - 2022-07-10 We're a couple of weeks since our 0.2.0 release. Thanks for the surge in interest and contributions! 0.2.2 has some fixes & some internal improvements: - We now test against SQLite & DuckDB on every commit, to ensure we're producing correct SQL. (@aljazerzen) - We have the beginning of Java bindings! (@doki23) - Idents surrounded by backticks are passed through to SQL (@max-sixty) - More examples on homepage; e.g. `join` & `window`, lots of small docs improvements - Automated releases to homebrew (@roG0d) - [prql-js](https://github.com/PRQL/prql/tree/main/prqlc/bindings/js) is now a single package for Node, browsers & webpack (@charlie-sanders) - Parsing has some fixes, including `>=` and leading underscores in idents (@mklopets) - Ranges receive correct syntax highlighting (@max-sixty) Thanks to Aljaž Mur Eržen @aljazerzen , George Roldugin @roldugin , Jasper McCulloch @Jaspooky , Jie Han @doki23 , Marko Klopets @mklopets , Maximilian Roos @max-sixty , Rodrigo Garcia @roG0d , Ryan Russell @ryanrussell , Steven Maude @StevenMaude , Charlie Sanders @charlie-sanders . We're planning to continue collecting bugs & feature requests from users, as well as working on some of the bigger features, like type-inference. For those interesting in joining, we also have a new [Contributing page](https://github.com/PRQL/prql/blob/main/.github/CONTRIBUTING.md). ## 0.2.0 - 2022-06-27 🎉 🎉 **After several months of building, PRQL is ready to use!** 🎉 🎉 --- How we got here: At the end of January, we published a proposal of a better language for data transformation: PRQL. The reception was better than I could have hoped for — we were no. 2 on HackerNews for a day, and gained 2.5K GitHub stars over the next few days. But man cannot live on GitHub Stars alone — we had to do the work to build it. So over the next several months, during many evenings & weekends, a growing group of us gradually built the compiler, evolved the language, and wrote some integrations. We want to double-down on the community and its roots in open source — it's incredible that a few of us from all over the globe have collaborated on a project without ever having met. We decided early-on that PRQL would always be open-source and would never have a commercial product (despite lots of outside interest to fund a seed round!). Because languages are so deep in the stack, and the data stack has so many players, the best chance of building a great language is to build an open language. --- We still have a long way to go. While PRQL is usable, it has lots of missing features, and an incredible amount of unfulfilled potential, including a language server, cohesion with databases, and type inference. Over the coming weeks, we'd like to grow the number of intrepid users experimenting PRQL in their projects, prioritize features that will unblock them, and then start fulfilling PRQL's potential by working through our [roadmap](https://prql-lang.org/roadmap/). The best way to experience PRQL is to try it. Check out our [website](https://prql-lang.org) and the [Playground](https://prql-lang.org/playground). Start using PRQL for your own projects in [dbt](https://github.com/prql/dbt-prql), [Jupyter notebooks](https://pyprql.readthedocs.io/en/latest/magic_readme.html) and Prefect workflows. Keep in touch with PRQL by following the project on [Twitter](https://twitter.com/prql_lang), joining us on [Discord](https://discord.gg/eQcfaCmsNc), starring the [repo](https://github.com/PRQL/prql). [Contribute](https://github.com/PRQL/prql/blob/main/.github/CONTRIBUTING.md) to the project — we're a really friendly community, whether you're a recent SQL user or an advanced Rust programmer. We need bug reports, documentation tweaks & feature requests — just as much as we need compiler improvements written in Rust. --- I especially want to give [Aljaž Mur Eržen](https://github.com/aljazerzen) (@aljazerzen) the credit he deserves, who has contributed the majority of the difficult work of building out the compiler. Much credit also goes to @charlie-sanders, one of PRQL's earliest supporters and the author of pyprql, and [Ryan Patterson-Cross](https://github.com/rbpatt2019) (@rbpatt2019), who built the Jupyter integration among other Python contributions. Other contributors who deserve a special mention include: @roG0d, @snth, @kwigley --- Thank you, and we look forward to your feedback! ================================================ FILE: CLAUDE.md ================================================ # Claude ## Development Workflow Use a tiered testing approach—iterate quickly, validate thoroughly: **Inner loop** (during development, ~5s): ```sh # Fast tests on core packages task prqlc:test # Filtered by test name cargo insta test -p prqlc --lib -- resolver cargo insta test -p prqlc --test integration -- date ``` **Before returning to user** (~30s): ```sh # Comprehensive prqlc tests - sufficient for most changes task prqlc:pull-request ``` **Cross-binding changes only** (~2min): ```sh # Only when changes affect JS/Python/wasm bindings task test-all ``` The test suite is configured to minimize token usage: - **Nextest** only shows failures and slow tests (not 600 PASS lines) - **Cargo builds** use `--quiet` flag (no compilation spam) - **Result**: ~52% reduction in output (1128 → 540 lines, ~4.5k tokens) ## Tests Prefer inline snapshots for almost all tests: ```rust insta::assert_snapshot!(result, @"expected output"); ``` Initialize tests with empty snapshots, then run with `--accept`: ```rust insta::assert_snapshot!(result, @""); ``` The test commands above with `--accept` will fill in the result automatically. ### Test Strategy **Prefer small inline `insta` snapshot tests** over full integration tests: - **Use inline tests** for most bug fixes and small features - Add `#[test]` functions in a `#[cfg(test)]` module at the end of the file - Use `insta::assert_snapshot!` for compact, readable test assertions - Fast to run, easy to review in PRs - **Use integration tests** (`prqlc/tests/integration/queries/*.prql`) only when: - Developing large, complex features that need comprehensive testing - Testing end-to-end behavior across multiple compilation stages - The test requires external resources or multi-file scenarios Example of a good inline test: ```rust #[cfg(test)] mod test { use insta::assert_snapshot; #[test] fn test_my_feature() { let query = "from employees | filter country == 'USA'"; assert_snapshot!(crate::tests::compile(query).unwrap(), @""); } } ``` ## Running the CLI For viewing `prqlc` output, for any stage of the compilation process: ```sh # Compile PRQL to SQL cargo run -p prqlc -- compile "from employees | filter country == 'USA'" # Format PRQL code cargo run -p prqlc -- fmt "from employees | filter country == 'USA'" # See all available commands cargo run -p prqlc -- --help ``` ## Linting Run all lints with ```sh task lint ``` ## Error Handling Never panic on user input or recoverable errors. Use proper error returns: - ❌ `.unwrap()` on operations that can fail with user input - ✅ `?` operator or `return Err(Error::new_simple("message"))` - ✅ `.expect("reason")` or `unreachable!()` only for compiler-bug invariants ## Error Messages Error messages should avoid 2nd person (you/your). Use softer modal verbs like "might" for a friendlier tone: - ❌ "are you missing `from` statement?" → ✅ "`from` statement might be missing?" - ❌ "did you forget to specify the column name?" → ✅ "column name might be missing?" - ❌ "you can only use X" → ✅ "X requires Y" (for hard constraints) - ❌ "Have you forgotten an argument?" → ✅ "Argument might be missing?" ## Documentation For Claude to view crate documentation: ```sh # Build documentation for a specific crate cargo doc -p prqlc # View the generated HTML documentation with the View tool # The docs are generated at target/doc/{crate_name}/index.html View target/doc/prqlc/index.html # For specific module documentation View target/doc/prqlc/module_name/index.html # For function documentation View target/doc/prqlc/fn.compile.html ``` ## Releases & Environment For releases or environment issues, see `web/book/src/project/contributing/development.md`. ================================================ FILE: Cargo.toml ================================================ [workspace] members = [ "prqlc/bindings/elixir/native/prql", "prqlc/bindings/java", "prqlc/bindings/js", "prqlc/bindings/prqlc-c", "prqlc/bindings/prqlc-python", "prqlc/prqlc-macros", "prqlc/prqlc-parser", "prqlc/prqlc", "prqlc/prqlc/examples/compile-files", # An example "web/book", ] resolver = "2" [workspace.package] authors = ["PRQL Developers"] edition = "2021" license = "Apache-2.0" repository = "https://github.com/PRQL/prql" # This isn't tested since `cargo-msrv` doesn't support workspaces; instead we # test `metadata.msrv` in `prqlc` rust-version = "1.75.0" version = "0.13.12" [profile.release] lto = true # Optimize for binary size in releases of all crates, # since compiler is fast enough as it is (for now). opt-level = "s" [profile.release.package.prqlc-c] # Remove some debug symbols (linker needs some of them) strip = "debuginfo" # Insta runs faster this way, ref https://insta.rs/docs/quickstart/ [profile.dev.package.insta] opt-level = 3 [profile.dev.package.similar] opt-level = 3 [workspace.metadata.release] allow-branch = ["*"] consolidate-commits = true [workspace.dependencies] anyhow = "1.0.102" arrow = { version = "53", features = [ "pyarrow", "prettyprint", ], default-features = false } enum-as-inner = "0.7.0" insta = { version = "1.46.3", features = ["colors", "glob", "yaml", "filters"] } insta-cmd = "0.6.0" itertools = "0.14.0" log = "0.4.29" pyo3 = { version = "0.27.1", features = ["abi3-py37", "anyhow"] } pyo3-build-config = "0.27.1" schemars = "1.2.1" semver = { version = "1.0.27", features = ["serde"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" serde_yaml = { version = "0.9.34" } similar = "2.7.0" similar-asserts = "1.7.0" strum = { version = "0.28.0", features = ["std", "derive"] } strum_macros = "0.28.0" ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # PRQL [![Website](https://img.shields.io/badge/INTRO-WEB-blue?style=for-the-badge)](https://prql-lang.org) [![Playground](https://img.shields.io/badge/INTRO-PLAYGROUND-blue?style=for-the-badge)](https://prql-lang.org/playground) [![Language Docs](https://img.shields.io/badge/DOCS-BOOK-blue?style=for-the-badge)](https://prql-lang.org/book) [![Discord](https://img.shields.io/badge/DISCORD-CHAT-indigo?style=for-the-badge&logo=discord)](https://discord.gg/eQcfaCmsNc) [![Twitter](https://img.shields.io/twitter/follow/prql_lang?color=%231DA1F2&style=for-the-badge&logo=x)](https://twitter.com/prql_lang) [![GitHub CI Status](https://img.shields.io/github/actions/workflow/status/prql/prql/tests.yaml?event=push&branch=main&logo=github&style=for-the-badge)](https://github.com/PRQL/prql/actions?query=branch%3Amain+workflow%3Atests) [![GitHub contributors](https://img.shields.io/github/contributors/PRQL/prql?style=for-the-badge&logo=github)](https://github.com/PRQL/prql/graphs/contributors) [![Stars](https://img.shields.io/github/stars/PRQL/prql?style=for-the-badge&logo=github)](https://github.com/PRQL/prql/stargazers) **P**ipelined **R**elational **Q**uery **L**anguage, pronounced "Prequel". PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement. Like SQL, it's readable, explicit and declarative. Unlike SQL, it forms a logical pipeline of transformations, and supports abstractions such as variables and functions. It can be used with any database that uses SQL, since it compiles to SQL. PRQL can be as simple as: ```elm from tracks filter artist == "Bob Marley" # Each line transforms the previous result aggregate { # `aggregate` reduces each column to a value plays = sum plays, longest = max length, shortest = min length, # Trailing commas are allowed } ``` Here's a larger example of the language: ```elm from employees filter start_date > @2021-01-01 # Clear date syntax derive { # `derive` adds columns / variables gross_salary = salary + (tax ?? 0), # Terse coalesce gross_cost = gross_salary + benefits_cost, # Variables can use other variables } filter gross_cost > 0 group {title, country} ( # `group` runs a pipeline over each group aggregate { # `aggregate` reduces each group to a value average gross_salary, sum_gross_cost = sum gross_cost, # `=` sets a column name } ) filter sum_gross_cost > 100_000 # `filter` replaces both of SQL's `WHERE` & `HAVING` derive id = f"{title}_{country}" # F-strings like Python derive country_code = s"LEFT(country, 2)" # S-strings allow using SQL as an escape hatch sort {sum_gross_cost, -country} # `-country` means descending order take 1..20 # Range expressions (also valid here as `take 20`) ``` For more on the language, more examples & comparisons with SQL, visit [prql-lang.org][prql website]. To experiment with PRQL in the browser, check out [PRQL Playground][prql playground]. ## Current Status - December 2025 PRQL is ready to use by the intrepid, either with our supported integrations, or within your own tools, using one of our supported language bindings. PRQL still has some bugs and some missing features, and is probably only ready to be rolled out to non-technical teams for fairly simple queries. Development has slowed in the past few months as we decide how to work on a new resolver, which will let us squash many bugs and simplify our code a lot. It'll also let us scale the language without scaling the complexity of the compiler. While we figure that out, we're also thinking about: - Ensuring our supported features feel extremely robust; resolving any [priority bugs](https://github.com/PRQL/prql/issues?q=is%3Aissue+is%3Aopen+label%3Abug+label%3Apriority). As more folks have started using PRQL, we've had more bug reports — good news, but also gives us more to work on. - Filling remaining feature gaps, so that PRQL is possible to use for almost all standard SQL queries. - Expanding our set of supported features — we are working to add experimental support for modules / multi-file projects, and for auto-formatting. And: - Making it really easy to start using PRQL. We're doing that by building integrations with tools that folks already use; for example a VS Code extension, Jupyter integration, and the recent [QStudio](https://www.timestored.com/qstudio/prql-ide) integration. If there are tools you're familiar with that you think would be open to integrating with PRQL, please let us know in an issue. - Whether all our initial decisions were correct — for example [how we handle window functions outside of a `window` transform](https://github.com/PRQL/prql/issues/2723). - Making it easier to contribute to the compiler. We have a wide group of contributors to the project, but contributions to the compiler itself are quite concentrated. We're keen to expand this; [#1840](https://github.com/PRQL/prql/issues/1840) for feedback, some suggestions on starter issues are below. We're increasingly open to contributions for bigger rewrites of the resolver given how bottlenecked we are on it. If you're interested in contributing, please reach out in an issue or on Discord. ## Get involved To stay in touch with PRQL: - Follow us on [Twitter](https://twitter.com/prql_lang) - Join us on [Discord](https://discord.gg/eQcfaCmsNc) - Star this repo - [Contribute][contributing] — join us in building PRQL, through writing code [(send us your use-cases!)](https://github.com/PRQL/prql/discussions), or inspiring others to use it. - See the [development][development] documentation for PRQL. It's easy to get started — the project can be built in a couple of commands, and we're a really friendly community! - For those who might be interested in contributing to the code now, check out issues with the [good first issue](https://github.com/PRQL/prql/labels/good%20first%20issue) label. Always feel free to ask questions or open a draft PR. ## Explore - [PRQL Playground][prql playground] — experiment with PRQL in the browser. - [PRQL Book][prql book] — the language documentation. - [Jupyter magic](https://pyprql.readthedocs.io/en/latest/magic_readme.html) — run PRQL in Jupyter, either against a DB, or a Pandas DataFrame / CSV / Parquet file through DuckDB. - [pyprql Docs](https://pyprql.readthedocs.io) — the pyprql documentation, the Python bindings to PRQL, including Jupyter magic. - [PRQL VS Code extension](https://marketplace.visualstudio.com/items?itemName=prql-lang.prql-vscode) - [prqlc-js](https://www.npmjs.com/package/prqlc) — JavaScript bindings for PRQL. ## Repo organization This repo is composed of: - **[prqlc](./prqlc/)** — the compiler, written in rust, whose main role is to compile PRQL into SQL. Also contains the CLI and bindings from various languages. - **[web](./web/)** — our web content: the [Book][prql book], [Website][prql website], and [Playground][prql playground]. It also contains our testing / CI infrastructure and development tools. Check out our [development docs][development] for more details. ## Contributors Many thanks to those who've made our progress possible: [![Contributors](https://contrib.rocks/image?repo=PRQL/prql)](https://github.com/PRQL/prql/graphs/contributors) [prql book]: https://prql-lang.org/book [prql website]: https://prql-lang.org [contributing]: https://prql-lang.org/book/project/contributing/ [development]: https://prql-lang.org/book/project/contributing/development.html [prql playground]: https://prql-lang.org/playground ================================================ FILE: Taskfile.yaml ================================================ # yaml-language-server: $schema=https://json.schemastore.org/taskfile.json # Root Taskfile - handles setup, orchestration, and repo-wide operations. # # This file gives new contributors an easy way to get everything they need, # assuming `cargo` and [Task](https://taskfile.dev) are installed. # # Structure: # - Root (this file): Setup, orchestration, repo-wide tasks (lint, test-all, build-all) # - prqlc/: Core development tasks (test, test-all, pull-request) # - web/: Web-related tasks (build, run-*) # # Beyond installing requirements, we generally shouldn't be using this as a # Makefile — in other words, we shouldn't require running this as part of normal # development. Rust tools are independently good, and adding an intermediate # layer means we're reimplementing things or getting in the way. Instead, this # can be used to aggregate commands that are currently separate; e.g. check out # `test-all`. # Some of the file may be somewhat over-engineered! version: "3" includes: prqlc: taskfile: ./prqlc dir: ./prqlc zig: taskfile: ./prqlc/bindings/prqlc-c/examples/minimal-zig dir: ./prqlc/bindings/prqlc-c/examples/minimal-zig web: taskfile: ./web dir: ./web python: taskfile: ./prqlc/bindings/prqlc-python dir: ./prqlc/bindings/prqlc-python vars: # Keep in sync with .vscode/extensions.json vscode_extensions: | budparr.language-hugo-vscode esbenp.prettier-vscode mitsuhiko.insta prql-lang.prql-vscode rust-lang.rust-analyzer cargo_crates: > bacon cbindgen cargo-audit cargo-insta cargo-llvm-cov cargo-release cargo-nextest default-target mdbook mdbook-admonish mdbook-footnote mdbook-toc wasm-bindgen-cli wasm-opt # wasm-pack waiting on https://github.com/rustwasm/wasm-pack/issues/1426 # # Excluding `elixir` atm given it's not enabled on Mac and currently unsupported brew_dependencies: | hugo jq npm uv pre-commit python3 tasks: # main installer is "setup-dev" which calls other tasks setup-dev: preconditions: - sh: which clang msg: | 🔴 Can't find `clang`, which is required to install a full development environment (we use `duckdb` in our tests, which requires it). Please install it. On macOS, that's xcode-select --install On Debian Linux, that's apt-get update && apt-get install clang desc: Install tools for PRQL development. cmds: - task: install-cargo-tools # We only suggest, rather than install; we don't want to intrude (maybe # we're being too conservative?). - cmd: task check-vscode-extensions ignore_error: true - cmd: task check-brew-dependencies ignore_error: true - task: install-maturin - task: install-npm-dependencies - pre-commit install-hooks - rustup component add llvm-tools-preview - cmd: | echo " 🟢 Setup complete! ✅🚀" silent: true install-cargo-tools: desc: Install cargo tools for PRQL development. cmds: # In CI we use `binstall`, because it's faster, and without it we can't # even get the arm64 docker image to build within the GHA timeout. But it # produces lots of confusing warning messages about 429 errors, I think # because it's querying GitHub for so many packages; so we only use it for # CI atm. If the warnings were less alarming, we could use for all # installs. # - | {{ if .CI }} task install-cargo-tools-binstall {{ else }} task install-cargo-tools-source {{ end }} - cmd: | [ "$(which cargo-insta)" ] || echo "🔴 Can't find a binary that cargo just installed. Is the cargo bin path (generally at `~/.cargo/bin`) on the \$PATH?" silent: true install-maturin: desc: Install maturin. # Someone might have this installed with another approach, so only install # if it can't be found. status: - which maturin cmds: - uv tool install maturin - uv tool upgrade maturin install-cargo-tools-source: cmds: # `--locked` installs from the underlying lock files (which is not the # default...) - "cargo install --locked {{.cargo_crates}}" - cargo install wasm-pack install-cargo-tools-binstall: cmds: - cmd: cargo install --locked cargo-binstall platforms: [linux, darwin] - "cargo binstall -y --locked {{.cargo_crates}}" - cargo binstall -y wasm-pack check-vscode-extensions: desc: Check and suggest VS Code extensions. vars: extensions: # List extensions, or just return true if we can't find `code`. sh: which code && code --list-extensions || true missing_extensions: | {{ range ( .vscode_extensions | trim | splitLines ) -}} {{ if not (contains . $.extensions) }}❌ {{.}} {{else}}✅ {{.}} {{ end }} {{ end -}} status: # If vscode isn't installed, or there are no missing extensions, # return 0 and exit early. - '[ ! -x "$(which code)" ] || {{ not (contains "❌" .missing_extensions) }}' silent: true cmds: - | echo " 🟡 It looks like VS Code is installed but doesn't have all recommended extensions installed: {{ .missing_extensions }} Install them with: task install-vscode-extensions " - exit 1 install-vscode-extensions: desc: Install recommended VS Code extensions. cmds: - | {{ range ( .vscode_extensions | trim | splitLines ) -}} code --install-extension {{.}} {{ end -}} check-brew-dependencies: status: - | {{ range (.brew_dependencies | trim | splitLines) -}} [ -n "$(which {{ . }})" ] {{ end -}} - | [ "$(npm -v | awk -F. '{print ($1 > 9 || ($1 == 9 && $2 > 4)) ? 0 : 1}')" -eq 0 ] silent: true cmds: - cmd: | echo " 🟡 It looks like at least one brew dependency is missing from: {{ .brew_dependencies }} ...or alternatively that npm < 9.4 These aren't required for initial PRQL development, but they are required for some of the extras. Install them with: task install-brew-dependencies " - exit 1 install-brew-dependencies: preconditions: - sh: which brew msg: | 🔴 Can't find `brew`, which we use to install {{ .brew_dependencies | trim | splitLines | join " & " }}. Either install brew & re-run this, or install the dependencies with a different approach, or use PRQL without them. Brew installation instructions at: https://brew.sh/ status: - task check-brew-dependencies cmds: - brew install {{.brew_dependencies | trim | splitLines | join " " }} install-npm-dependencies: cmds: - npm install --global prettier prettier-plugin-go-template - cmd: echo "In order to get nice auto-formatting of web code in VS Code, VS Code requires configuration to use the system-wide install of prettier. See https://github.com/NiklasPor/prettier-plugin-go-template/issues/58#issuecomment-1085060511 for more info." silent: true build-all: desc: Build everything. summary: | Build everything. Running this isn't required when developing; it's for caching or as a reference. cmds: - cargo build --all-targets --all-features --quiet - cargo build --all-targets --all-features --target=wasm32-unknown-unknown --quiet # Build without features, as the dependencies have slightly different # features themselves and so require recompiling. This is only useful for # caching. - cargo build --all-targets --quiet - cargo build --all-targets --features=default,test-dbs --quiet - cargo doc --quiet - task: build-each-crate - task: web:build - task: python:build build-each-crate: summary: | Builds each crates individually. This is helpful for caching, since often we'll want to run `cargo build -p prqlc`, and the dependencies can have different features for that relative to `cargo build`. vars: PACKAGES: sh: cargo metadata --format-version=1 | jq -r '.workspace_members[] | split(" ")[0]' preconditions: - sh: command -v jq msg: "jq is not available. Please install it to continue." cmds: - | {{ range ( .PACKAGES | splitLines ) -}} cargo build --all-targets -p {{ . }} --quiet {{ end -}} test-rust-api: summary: | Run tests, excluding some internal crates vars: PACKAGES: sh: cargo metadata --format-version=1 | jq -r '.workspace_members[] | split(" ")[0]' cmds: - | cargo test {{ range without (splitLines .PACKAGES) "prqlc-parser" }} -p={{ . }} {{ end }} test-all: desc: Test everything across all workspaces, accepting snapshots. summary: | Test everything, accepting snapshots. Running this isn't required when developing; it's for caching or as a reference. cmds: # TODO: # - We could add `prqlc-c` here. # - We deliberately don't test some other bindings, such as `prql-php`, # given they require more dependencies and aren't yet Supported. They # run in CI. - task: prqlc:pull-request - task: test-js # No nextest here as doesn't work with wasm - cargo test --target=wasm32-unknown-unknown - task: python:test - task: build-all test-rust-coverage: desc: Run tests with instrumentation for code coverage. summary: Run tests with instrumentation for code coverage. This works with VS Code's [Coverage Gutters](https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters) extension. # We previously had a different target dir, but the default seems to use `target/llvm-cov` as its target already, so we can remove this assuming it works OK # env: # CARGO_TARGET_DIR: "./target-cov" cmds: - cargo llvm-cov --lcov --output-path lcov.info --features=default,test-dbs lint: desc: Run all lints (pre-commit & cargo clippy). cmds: - pre-commit run --all-files - cargo clippy --all-targets --all-features test-rust-external-dbs: desc: Run tests which require external databases, with docker cmds: - cmd: echo "🔵 Starting docker containers. In some circumstances the tests will start before images are ready. After one initial failure, try re-running." silent: true - cd prqlc/prqlc/tests/integration/dbs && docker compose up --wait # This _only_ runs the external dbs tests, but we could make it run # everything? - cargo test --features=test-dbs-external --test=integration -- --nocapture queries::results # We could run this ourselves, but makes iteration times much longer - cmd: | echo "🔵 To remove containers and remove local built images, run cd prqlc/prqlc/tests/integration/dbs && docker compose down --rmi local" silent: true test-js: dir: prqlc/bindings/js cmds: - npm cit # Currently disabled; see prql-elixir/README.md for details test-elixir: dir: prqlc/bindings/elixir cmds: # We could move this line into an `install` task - mix local.hex --force - mix deps.get --force - mix compile - mix test build-php: - cargo build --package prqlc-c --release - mkdir -p prqlc/bindings/php/lib/ - cp target/release/libprqlc_c.* prqlc/bindings/prqlc-c/prqlc.h prqlc/bindings/php/lib/ - cd prqlc/bindings/php && composer install build-prqlc-c-header: desc: Build the C header for the C bindings. dir: prqlc/bindings/prqlc-c cmds: - cbindgen --crate prqlc-c --output prqlc.h - cbindgen --crate prqlc-c --lang C++ --output prqlc.hpp test-php: dir: prqlc/bindings/php cmds: - vendor/bin/phpunit tests # The next two tasks are not used for either: # - the Dockerfile (installing brew takes forever) # so the Dockerfile simply installs hugo & nodejs directly # - the "desktop setup" - it uses other tasks in the Taskfile.yaml # They remain in the Taskfile.yaml as a hint if they should ever be needed # install-hugo: # cmds: # # - /home/linuxbrew/.linuxbrew/bin/brew install hugo # - curl -L https://github.com/gohugoio/hugo/releases/download/v0.91.2/hugo_0.91.2_Linux-64bit.deb -o hugo.deb # - apt install ./hugo.deb # install-nodejs: # cmds: # # - /home/linuxbrew/.linuxbrew/bin/brew install nodejs # - curl -fsSL https://deb.nodesource.com/setup_16.x | bash - # - apt install -y nodejs # - cd /app/playground/ ; npm install ================================================ FILE: bacon.toml ================================================ # Initial bacon config file; edits and contributions welcome. default_job = "clippy" # PRQL additions [jobs.test] command = ['cargo', 'insta', 'test', "--color=always", "--features=default,test-dbs"] [jobs.test-accept] command = ['cargo', 'insta', 'test', '--accept', "--color=always", "--features=default,test-dbs", "--unreferenced=auto"] watch = ["*"] [jobs.test-accept-fast] command = ['cargo', 'insta', 'test', '--accept', "--color=always", "-p=prqlc", "--lib"] watch = ["*"] # Standard tasks [jobs.check] command = ["cargo", "check", "--color=always"] need_stdout = false watch = ["*"] [jobs.check-all] command = ["cargo", "check", "--all-targets", "--all-features", "--color=always"] need_stdout = false watch = ["*"] [jobs.clippy] command = ["cargo", "clippy", "--all-targets", "--all-features", "--color=always"] need_stdout = false watch = ["*"] [jobs.test-cargo] command = ["cargo", "test", "--color=always", "--no-fail-fast"] need_stdout = true watch = ["*"] [jobs.doc] command = ["cargo", "doc", "--color=always", "--no-deps"] need_stdout = false # If the doc compiles, then it opens in your browser and bacon switches # to the previous job [jobs.doc-open] command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] need_stdout = false on_success = "back" # so that we don't open the browser at each change # You may define here keybindings that would be specific to # a project, for example a shortcut to launch a specific job. # Shortcuts to internal functions (scrolling, toggling, etc.) # should go in your personal prefs.toml file instead. [keybindings] a = "job:test-accept" c = "job:clippy" d = "job:doc-open" f = "job:test-accept-fast" # `g` for no insta; bacon is better at displaying errors, although have been # trying to work through why the errors from insta aren't picked up: https://github.com/rust-lang/cargo/issues/12220 g = "job:test-cargo" r = "job:run" t = "job:test" ================================================ FILE: flake.nix ================================================ { description = "PRQL development environment"; inputs = { nixpkgs.url = "github:nixos/nixpkgs"; flake-utils.url = "github:numtide/flake-utils"; naersk.url = "github:nix-community/naersk"; mdbook-footnote = { url = "github:aljazerzen/mdbook-footnote"; inputs.nixpkgs.follows = "nixpkgs"; inputs.flake-utils.follows = "flake-utils"; inputs.naersk.follows = "naersk"; }; hyperlink = { url = "github:aljazerzen/hyperlink"; inputs.nixpkgs.follows = "nixpkgs"; inputs.flake-utils.follows = "flake-utils"; inputs.naersk.follows = "naersk"; }; fenix = { url = "github:nix-community/fenix"; inputs.nixpkgs.follows = "nixpkgs"; }; }; outputs = { self, nixpkgs, flake-utils, mdbook-footnote, hyperlink, fenix, naersk }: flake-utils.lib.eachDefaultSystem (system: let pkgs = nixpkgs.legacyPackages.${system}; fenix_pkgs = fenix.packages.${system}; essentials = with pkgs; [ # rust toolchain (fenix_pkgs.combine [ (fenix_pkgs.fromToolchainFile { file = ./rust-toolchain.toml; sha256 = "sha256-s1RPtyvDGJaX/BisLT+ifVfuhDT1nZkZ1NcK8sbwELM="; }) (fenix_pkgs.stable.withComponents [ "cargo" "clippy" "rust-src" "rustc" "rustfmt" "rust-analyzer" "llvm-tools-preview" ]) ]) # tooling clang # for llvm debugger in VSCode # tools cargo-nextest bacon cargo-audit cargo-insta cargo-release pkg-config openssl cargo-llvm-cov # actions go-task sd ripgrep nodePackages.prettier #nodePackages.prettier-plugin-go-template #nixpkgs-fmt rsync ]; web = with pkgs; [ # book mdbook mdbook-admonish mdbook-footnote.defaultPackage.${system} # website hugo # playground nodejs nodePackages.npm # link check hyperlink.defaultPackage.${system} ]; bindings = with pkgs; [ # bindings python311 zlib maturin ruff black wasm-bindgen-cli wasm-pack ]; in { devShells.default = pkgs.mkShell { buildInputs = essentials; }; devShells.web = pkgs.mkShell { buildInputs = essentials ++ web; }; devShells.full = pkgs.mkShell { buildInputs = essentials ++ web ++ bindings; # needed for running wheels produced by Python maturin builds that are not manylinux # shellHook = '' # export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath bindings}:$LD_LIBRARY_PATH" # export LD_LIBRARY_PATH="${pkgs.stdenv.cc.cc.lib.outPath}/lib:$LD_LIBRARY_PATH" # ''; }; }); } ================================================ FILE: grammars/CotEditor/PRQL.yaml ================================================ --- kind: code extensions: - keyString: prql filenames: [] metadata: author: vanillajonathan description: PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement distributionURL: https://coteditor.com lastModified: 2023-12-29 license: Same as CotEditor version: 0.0.1 outlineMenu: [] attributes: - beginString: "@{\\w+(=\\w+)?}" regularExpression: true keywords: - beginString: aggregate - beginString: derive - beginString: filter - beginString: from - beginString: group - beginString: join - beginString: select - beginString: sort - beginString: take - beginString: window - beginString: case - beginString: let - beginString: module - beginString: prql commands: - beginString: abs - beginString: any - beginString: average - beginString: concat_array - beginString: count - beginString: every - beginString: min - beginString: max - beginString: stddev - beginString: sum - beginString: read_csv - beginString: read_json - beginString: read_parquet - beginString: all - beginString: map - beginString: zip - beginString: from_text - beginString: lower - beginString: upper - beginString: lead - beginString: lag - beginString: first - beginString: last - beginString: rank - beginString: rank_dense - beginString: row_number types: - beginString: bool - beginString: float - beginString: int - beginString: int8 - beginString: int16 - beginString: int32 - beginString: int64 - beginString: int128 - beginString: time - beginString: timestamp - beginString: text - beginString: date - beginString: math variables: [] values: - beginString: "true" - beginString: "false" - beginString: "null" numbers: - beginString: ((? Format_ and choose _Import…_ in the gear icon menu that is just below the installed style list. 2. Choose the `PRQL.yaml` file. ================================================ FILE: grammars/GtkSourceView/README.md ================================================ # Syntax highlighting for GtkSourceView This is a syntax highlighting file the [GtkSourceView](https://gitlab.gnome.org/GNOME/gtksourceview) component used by text editors and integrated development environments such as [GNOME Text Editor](https://apps.gnome.org/TextEditor/) and [GNOME Builder](https://apps.gnome.org/Builder/). ## Installation To install system-wide, copy the `prql.xml` file to: /usr/share/gtksourceview-5/language-specs/ To install for the current user, copy the `prql.xml` file to: ~/.local/share/gtksourceview-5/language-specs/ ## Embedding To embed it in your GTK application using the `GtkSourceView` widget add it to a [`LanguageManager`](https://gnome.pages.gitlab.gnome.org/gtksourceview/gtksourceview5/class.LanguageManager.html) which you add to your [`Buffer`](https://gnome.pages.gitlab.gnome.org/gtksourceview/gtksourceview5/method.Buffer.set_language.html). ================================================ FILE: grammars/GtkSourceView/prql.lang ================================================ application/x.prql;application/prs.prql;application/vnd.prql *.prql # .prql ")?; writeln!(w, "")?; writeln!(w, "")?; writeln!(w, "")?; // header { writeln!(w, "
")?; writeln!(w, "

prqlc debug log

")?; write_key_values( w, &[ ("started_at", &debug_log.started_at), ("version", &debug_log.version), ], )?; writeln!(w, "
")?; } // pre-stage entries (this should be mostly source PRQL) for (index, entry) in debug_log.entries.iter().enumerate() { if let DebugEntryKind::NewStage(_) = &entry.kind { break; } match &entry.kind { DebugEntryKind::Message(message) => { write_message(w, message)?; } _ => { write_titled_entry(w, index, entry)?; } } } { writeln!(w, "
")?; for level in ["off", "error", "warn", "info", "debug", "trace"] { writeln!( w, r#""# )?; writeln!(w, r#""#)?; } writeln!(w, "
")?; } { writeln!(w, "
")?; // stage tabs // tab handles let mut entries = debug_log.entries.iter().peekable(); while entries.peek().is_some() { write_stage_handles(w, &mut entries)?; } // tab panels writeln!(w, "
")?; let mut entries = debug_log.entries.iter().enumerate().peekable(); while entries.peek().is_some() { write_stage_contents(w, &mut entries)?; } writeln!(w, "
")?; writeln!(w, "
")?; } writeln!(w, "")?; writeln!(w, "")?; Ok(()) } fn write_stage_handles<'a, W: Write>( w: &mut W, entries: &mut Peekable>, ) -> Result { // find a new stage let stage = loop { match entries.next() { Some(entry) => { if let DebugEntryKind::NewStage(s) = entry.kind { break s; } } None => return Ok(()), } }; let stage_name = stage.full_name(); writeln!( w, r#""# )?; writeln!(w, r#""#)?; Ok(()) } fn write_stage_contents<'a, W: Write>( w: &mut W, entries: &mut Peekable>, ) -> Result { // find a new stage let stage = loop { match entries.next() { Some((_, entry)) => { if let DebugEntryKind::NewStage(s) = entry.kind { break s; } } None => return Ok(()), } }; writeln!(w, "
", stage.full_name())?; while let Some((_, entry)) = entries.peek() { if matches!(entry.kind, DebugEntryKind::NewStage(_)) { break; } let (index, entry) = entries.next().unwrap(); match &entry.kind { DebugEntryKind::Message(message) => { write_message(w, message)?; } _ => { write_titled_entry(w, index, entry)?; } } } writeln!(w, "
")?; Ok(()) } fn write_message(w: &mut W, message: &Message) -> Result { writeln!(w, r#"
"#, message.level)?; write!(w, "[{}", message.level)?; if let Some(module_path) = &message.module_path { write!(w, r#" {module_path}"#)?; } writeln!(w, "] {}", message.text)?; writeln!(w, "
") } fn write_titled_entry(w: &mut W, index: usize, entry: &DebugEntry) -> Result { writeln!(w, r#"
"#)?; let entry_id = format!("entry-{index}"); writeln!( w, "" )?; { writeln!(w, r#""#)?; } { writeln!(w, r#"
"#)?; match &entry.kind { DebugEntryKind::ReprPrql(a) => write_repr_prql(w, a)?, DebugEntryKind::ReprLr(a) => write_repr_lr(w, a)?, DebugEntryKind::ReprPr(a) => write_repr_pr(w, a)?, DebugEntryKind::ReprPl(a) => write_repr_pl(w, a)?, DebugEntryKind::ReprDecl(a) => write_repr_decl(w, a)?, DebugEntryKind::ReprRq(a) => write_repr_rq(w, a)?, DebugEntryKind::ReprPqEarly(a) => write_repr_pq_early(w, a)?, DebugEntryKind::ReprPq(a) => write_repr_pq(w, a)?, DebugEntryKind::ReprSqlParser(a) => write_repr_sql_parser(w, a)?, DebugEntryKind::ReprSql(a) => write_repr_sql(w, a)?, DebugEntryKind::NewStage(_) | DebugEntryKind::Message(_) => unreachable!(), } writeln!(w, "
")?; } writeln!(w, "
")?; Ok(()) } fn write_repr_prql(w: &mut W, source_tree: &SourceTree) -> Result { writeln!(w, r#"
"#)?; write_key_values(w, &[("root", &source_tree.root)])?; let reverse_ids: HashMap<_, _> = source_tree .source_ids .iter() .map(|(id, path)| (path, id)) .collect(); for (path, source) in &source_tree.sources { writeln!(w, r#"
"#)?; let source_id = reverse_ids.get(path).unwrap(); write_key_values(w, &[("path", &path), ("source_id", source_id)])?; let source_escaped = escape_html(source); writeln!( w, r#"{source_escaped}"# )?; writeln!(w, "
")?; // source } writeln!(w, "
") } fn write_repr_lr(w: &mut W, tokens: &lr::Tokens) -> Result { writeln!(w, r#""#)?; for token in &tokens.0 { writeln!(w, r#""#,)?; writeln!(w, "", escape_html(&format!("{:?}", token.kind)))?; writeln!( w, r#""#, token.span.start, token.span.end )?; writeln!(w, "")?; } writeln!(w, "
{}{}:{}
") } fn write_repr_pr(w: &mut W, ast: &pr::ModuleDef) -> Result { writeln!(w, r#"
"#)?; let json = serde_json::to_string(ast).unwrap(); let json_node: serde_json::Value = serde_json::from_str(&json).unwrap(); write_json_ast_node(w, json_node, false)?; writeln!(w, "
") } fn write_repr_pl(w: &mut W, ast: &pl::ModuleDef) -> Result { writeln!(w, r#"
"#)?; let json = serde_json::to_string(ast).unwrap(); let json_node: serde_json::Value = serde_json::from_str(&json).unwrap(); write_json_ast_node(w, json_node, false)?; writeln!(w, "
") } fn write_repr_decl(w: &mut W, root_mod: &decl::RootModule) -> Result { writeln!(w, r#"
"#)?; for (name, decl) in root_mod.module.names.iter().sorted_by_key(|x| x.0.as_str()) { write_decl(w, decl, name, &root_mod.span_map)?; } writeln!(w, "
") } fn write_repr_rq(w: &mut W, ast: &rq::RelationalQuery) -> Result { writeln!(w, r#"
"#)?; let json = serde_json::to_string(ast).unwrap(); let json_node: serde_json::Value = serde_json::from_str(&json).unwrap(); write_json_ast_node(w, json_node, false)?; writeln!(w, "
") } fn write_repr_pq_early(w: &mut W, ast: &[pq_ast::SqlTransform]) -> Result { writeln!(w, r#"
"#)?; let json = serde_json::to_string(ast).unwrap(); let json_node: serde_json::Value = serde_json::from_str(&json).unwrap(); write_json_ast_node(w, json_node, false)?; writeln!(w, "
") } fn write_repr_pq(w: &mut W, ast: &pq_ast::SqlQuery) -> Result { writeln!(w, r#"
"#)?; let json = serde_json::to_string(ast).unwrap(); let json_node: serde_json::Value = serde_json::from_str(&json).unwrap(); write_json_ast_node(w, json_node, false)?; writeln!(w, "
") } fn write_repr_sql_parser(w: &mut W, ast: &sqlparser::ast::Query) -> Result { writeln!(w, r#"
"#)?; let json = serde_json::to_string(ast).unwrap(); let json_node: serde_json::Value = serde_json::from_str(&json).unwrap(); write_json_ast_node(w, json_node, false)?; writeln!(w, "
") } fn write_repr_sql(w: &mut W, query: &str) -> Result { writeln!(w, r#"
"#)?; writeln!(w, "
{query}
")?; writeln!(w, "
") } fn write_key_values(w: &mut W, pairs: &[(&'static str, &dyn Debug)]) -> Result { writeln!(w, r#"
"#)?; for (k, v) in pairs { writeln!(w, r#"
{k}: {v:?}
"#)?; } writeln!(w, "
") } /// A hacky way to reconstruct AST nodes from their JSON serialization. /// Finds structures that look like `{ "BinOp": { ... }, "span": ... }` /// and converts them to `
...
`. fn write_json_ast_node( w: &mut W, node: serde_json::Value, is_node_contents: bool, ) -> Result { match node { serde_json::Value::Null => write!(w, "None"), serde_json::Value::Bool(b) => write!(w, "{b}"), serde_json::Value::Number(n) => write!(w, "{n}"), serde_json::Value::String(s) => write!(w, "{s}"), serde_json::Value::Array(items) => { writeln!(w, r#"
    "#)?; for item in items { write!(w, "
  • ")?; write_json_ast_node(w, item, false)?; write!(w, "
  • ")?; } writeln!(w, "
")?; Ok(()) } serde_json::Value::Object(properties) => { let is_ast_node = properties.contains_key("span") || properties.contains_key("id") || (properties.len() == 1 && !is_node_contents && properties.values().next().unwrap().is_object()); if is_ast_node { return write_ast_node_from_object(w, properties); } writeln!(w, r#"
"#)?; for (key, value) in properties { if key == "ty" || key == "return_ty" { // special case for better type printing let ty_json = value.to_string(); if let Ok(ty) = serde_json::from_str::(&ty_json) { let ty_prql = escape_html(&codegen::write_ty(&ty)); write!(w, r#"{key}: {ty_prql}"#)?; } continue; } write!(w, r#"{key}:
"#)?; write_json_ast_node(w, value, false)?; writeln!(w, "
")?; } writeln!(w, "
") } } } fn write_ast_node_from_object( w: &mut W, mut properties: serde_json::Map, ) -> Result { let id: Option = properties.remove("id").and_then(|s| match s { serde_json::Value::Null => None, serde_json::Value::Number(n) if n.is_i64() => n.as_i64(), _ => unreachable!("expected id to be int, got: {}", s), }); let span: Option = properties.remove("span").and_then(|s| match s { serde_json::Value::Null => None, serde_json::Value::String(s) => Some(s), _ => None, // We previously used to error, but sqlparser now has a type that // doesn't include spans, so we just ignore it. // _ => unreachable!("expected span to be string, got: {}", s), }); let ty: Option = properties.remove("ty"); let first_item = properties.into_iter().next(); let (name, contents) = first_item.unwrap_or_else(|| ("".into(), serde_json::Value::Null)); write!(w, r#"
"#)?; { write!(w, "")?; let h2_id = id.map(|i| format!("id=ast-{i} ")).unwrap_or_default(); write!(w, "

{name}

")?; if let Some(id) = id { write!(w, r#"id={id}"#)?; } if let Some(span) = span { write!(w, r#"{span}"#)?; } if let Some(ty) = ty { let ty_json = ty.to_string(); if let Ok(ty) = serde_json::from_str::(&ty_json) { let ty_prql = codegen::write_ty(&ty); write!(w, r#"{ty_prql}"#)?; } } write!(w, "
")?; } { write!(w, r#""#)?; write_json_ast_node(w, contents, true)?; write!(w, "")?; } write!(w, "
") } fn write_decl( w: &mut W, decl: &decl::Decl, name: &String, span_map: &HashMap, ) -> Result { let open = if name != "std" { " open" } else { "" }; write!(w, r#"
"#)?; // header { write!(w, "")?; write!(w, r#"

{name}

"#)?; let span = decl.declared_at.as_ref().and_then(|id| span_map.get(id)); if let Some(span) = span { write!(w, r#"{span:?}"#)?; } write!(w, "
")?; // header } { write!(w, r#""#)?; match &decl.kind { decl::DeclKind::Module(m) => { for (name, decl) in m.names.iter().sorted_by_key(|x| x.0.as_str()) { write_decl(w, decl, name, span_map)?; } } decl::DeclKind::Expr(expr) => { let json = serde_json::to_string(expr).unwrap(); let json_node: serde_json::Value = serde_json::from_str(&json).unwrap(); write_json_ast_node(w, json_node, false)?; } decl::DeclKind::Ty(ty) => { writeln!(w, r#"{}"#, escape_html(&codegen::write_ty(ty)))?; } decl::DeclKind::TableDecl(table_decl) => { let json = serde_json::to_string(table_decl).unwrap(); let json_node: serde_json::Value = serde_json::from_str(&json).unwrap(); write_json_ast_node(w, json_node, false)?; } _ => { write!(w, r#"
{}
"#, decl.kind)?; } } write!(w, "
")?; } write!(w, "
") } fn escape_html(text: &str) -> String { text.replace('&', "&") .replace('<', "<") .replace('>', ">") .replace('"', """) .replace('\'', "'") } const CSS_STYLES: &str = r#" body { font-size: 12px; font-family: monospace; --background: #1F1F1F; --background-hover: #4D4F51; --background-focus: #623315; --text: #CACACA; --text-blue: #4ebffc; --text-green: #4BBFA7; --text-yellow: #DCDCAA; --text-muted: gray; background-color: var(--background); color: var(--text); } summary::marker { content: ""; } .clickable { cursor: pointer; text-decoration: underline; } .highlight-hover { background-color: var(--background-hover); } .highlight-focus { background-color: var(--background-focus); } .yellow { color: var(--text-yellow); } .blue:not(#fakeId) { color: var(--text-blue); } .muted { color: var(--text-muted); } .key-values { display: flex; gap: 1em; } .tab-container > .tab-panels > .tab-panel { display: none; } .tab-container > input:first-child:checked ~ .tab-panels > .tab-panel:first-child, .tab-container > input:nth-child(3):checked ~ .tab-panels > .tab-panel:nth-child(2), .tab-container > input:nth-child(5):checked ~ .tab-panels > .tab-panel:nth-child(3), .tab-container > input:nth-child(7):checked ~ .tab-panels > .tab-panel:nth-child(4), .tab-container > input:nth-child(9):checked ~ .tab-panels > .tab-panel:nth-child(5), .tab-container > input:nth-child(11):checked ~ .tab-panels > .tab-panel:nth-child(6), .tab-container > input:nth-child(13):checked ~ .tab-panels > .tab-panel:nth-child(7), .tab-container > input:nth-child(15):checked ~ .tab-panels > .tab-panel:nth-child(8), .tab-container > input:nth-child(17):checked ~ .tab-panels > .tab-panel:nth-child(9) { display: block; } .tab-container > input { display: none; } .tab-container > label { display: inline-block; padding: 1em; cursor: pointer; border: 1px solid transparent; border-bottom: 0; } .tab-container > input:checked + label { border-color: var(--text); border-bottom: 1px solid var(--background); margin-bottom: -1px; } section.tab-panel { border-top: 1px solid; padding-top: 1rem; } .entry { &>.entry-label { margin: 0; display: block; } &>.entry-collapse { display: none; } &>.entry-collapse:checked + .entry-label + .entry-content { display: none; } &>.entry-content { display: flex; flex-direction: column; } } .msg-ERROR, .msg-WARN, .msg-INFO, .msg-DEBUG, .msg-TRACE { display: none; } .msg-filter-error .msg-ERROR { display: block; } .msg-filter-warn .msg-WARN, .msg-filter-warn .msg-ERROR { display: block; } .msg-filter-info .msg-INFO, .msg-filter-info .msg-WARN, .msg-filter-info .msg-ERROR { display: block; } .msg-filter-debug .msg-DEBUG, .msg-filter-debug .msg-INFO, .msg-filter-debug .msg-WARN, .msg-filter-debug .msg-ERROR { display: block; } .msg-filter-trace .msg-TRACE, .msg-filter-trace .msg-DEBUG, .msg-filter-trace .msg-INFO, .msg-filter-trace .msg-WARN, .msg-filter-trace .msg-ERROR { display: block; } code { white-space: preserve; word-break: keep-all; } .indent { padding-left: 1em; margin-top: 2px; border-left: 1px solid gray; } .span { color: var(--text-muted); } table.repr.lr { border-collapse: collapse; } .ast-node>.header { display: flex; } .ast-node>.contents { display: block; } .ast-node>.header>h2 { font-size: inherit; color: var(--text-green); margin: 0; } .ast-node>.header>span { display: inline-block; margin-left: 1em; } .ast-node:not([open])>.header::after { content: '...'; margin-left: 1em; } .ast-node>.contents.indent>.json-array, .ast-node>.contents.indent>.json-object { padding-left: 0; } .ast-node:focus { background-color: var(--background-focus); } .json-array { margin: 0; list-style-type: "- "; } .json-array,.json-object,.json-value { padding-left: 1em; } "#; const JS_SCRIPT: &str = r#" const ast_node_mousedown = (event) => { event.stopPropagation(); const ast_node = event.currentTarget; if (document.activeElement == ast_node) { // unfocus after click (and focusing) is finished setTimeout(() => { ast_node.blur(); highlight(null, null, 'highlight-focus'); }, 0) } }; const ast_node_focus = (event) => { event.stopPropagation(); const ast_node = event.currentTarget; const span_element = ast_node.querySelector(':scope > .header > .span'); highlight(span_element, null, 'highlight-focus'); }; const ast_node_mouseover = (event) => { if (document.activeElement != document.body) { // something else has focus, we don't show hover highlight event.stopPropagation(); return; } const ast_node = event.currentTarget; if (ast_node.classList.contains("highlight-hover")) { event.stopPropagation(); return; } // find the span DOM node const span_element = ast_node.querySelector(':scope > .header > .span'); if (!span_element) { // if there is no node, return without stopping propagation return; } event.stopPropagation(); highlight(span_element, ast_node, 'highlight-hover'); } const highlight = (span_element, origin_element, highlight_class) => { // remove all existing highlights document.querySelectorAll("." + highlight_class).forEach(e => { e.classList.remove(highlight_class); }); // highlight origin element if (origin_element) { origin_element.classList.add(highlight_class); } // highlight source if (span_element) { const span = extract_span(span_element); const code_element = document.getElementById(`source-${span.source_id}`); if (!code_element) { console.error(`cannot find source with id=${span.source_id}`); } else { const source_text = code_element.innerText; const before = escape_html(source_text.substring(0, span.start)); const selected = escape_html(source_text.substring(span.start, span.end)); const after = escape_html(source_text.substring(span.end)); code_element.innerHTML = `${before}${selected}${after}`; } } }; const escape_html = (text) => { return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } const extract_span = (span_element) => { const parts = span_element.innerText.split(':'); const source_id = Number.parseInt(parts[0]); const start_end = parts[1].split('-'); const start = Number.parseInt(start_end[0]); const end = Number.parseInt(start_end[1]); return { source_id, start, end }; }; const message_filter_change = (event) => { if (!event.target.checked) { return; } const active_id = document.querySelector(".message-filter > input:checked").id; const main = document.getElementsByTagName('main')[0]; main.classList.forEach(c => { if (c.startsWith("msg-filter-")) { main.classList.remove(c); } }); main.classList.add(active_id); }; document.addEventListener('DOMContentLoaded', () => { const ast_nodes = document.querySelectorAll(".ast-node"); ast_nodes.forEach(ast_node => { ast_node.addEventListener("mouseover", ast_node_mouseover); ast_node.addEventListener("mousedown", ast_node_mousedown); ast_node.addEventListener("focus", ast_node_focus); }); const message_filter_nodes = document.querySelectorAll(".message-filter > input"); message_filter_nodes.forEach(node => { node.addEventListener("change", message_filter_change); }); }); "#; ================================================ FILE: prqlc/prqlc/src/error_message.rs ================================================ use std::collections::HashMap; use std::error::Error as StdError; use std::fmt::{self, Debug, Display, Formatter}; use std::ops::Range; use std::path::PathBuf; use ariadne::{Cache, Config, Label, Report, ReportKind, Source}; use serde::Serialize; use crate::Span; use crate::{Error, Errors, MessageKind, SourceTree}; #[derive(Clone, Serialize)] pub struct ErrorMessage { /// Message kind. Currently only Error is implemented. pub kind: MessageKind, /// Machine-readable identifier of the error pub code: Option, /// Plain text of the error pub reason: String, /// A list of suggestions of how to fix the error pub hints: Vec, /// Character offset of error origin within a source file pub span: Option, /// Annotated code, containing cause and hints. pub display: Option, /// Line and column number of error origin within a source file pub location: Option, } /// Location within the source file. /// Tuples contain: /// - line number (0-based), /// - column number within that line (0-based), #[derive(Debug, Clone, Serialize)] pub struct SourceLocation { pub start: (usize, usize), pub end: (usize, usize), } impl Display for ErrorMessage { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { // https://github.com/zesterer/ariadne/issues/52 if let Some(display) = &self.display { let message_without_trailing_spaces = display .split('\n') .map(str::trim_end) .collect::>() .join("\n"); f.write_str(&message_without_trailing_spaces)?; } else { let code = (self.code.as_ref()) .map(|c| format!("[{c}] ")) .unwrap_or_default(); writeln!(f, "{}Error: {}", code, &self.reason)?; for hint in &self.hints { // TODO: consider alternative formatting for hints. writeln!(f, "↳ Hint: {hint}")?; } } Ok(()) } } impl Debug for ErrorMessage { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { Display::fmt(&self, f) } } impl From for ErrorMessage { fn from(e: Error) -> Self { log::debug!("{e:#?}"); ErrorMessage { code: e.code.map(str::to_string), kind: e.kind, reason: e.reason.to_string(), hints: e.hints, span: e.span, display: None, location: None, } } } impl From> for ErrorMessages { fn from(errors: Vec) -> Self { ErrorMessages { inner: errors } } } #[derive(Debug, Clone, Serialize)] pub struct ErrorMessages { pub inner: Vec, } impl StdError for ErrorMessages {} impl From for ErrorMessages { fn from(e: ErrorMessage) -> Self { ErrorMessages { inner: vec![e] } } } impl From for ErrorMessages { fn from(e: Error) -> Self { ErrorMessages { inner: vec![ErrorMessage::from(e)], } } } impl From for ErrorMessages { fn from(errs: Errors) -> Self { ErrorMessages { inner: errs.0.into_iter().map(ErrorMessage::from).collect(), } } } impl Display for ErrorMessages { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { for e in &self.inner { Display::fmt(&e, f)?; } Ok(()) } } impl ErrorMessages { pub fn to_json(&self) -> String { serde_json::to_string(self).unwrap() } /// Computes message location and builds the pretty display. pub fn composed(mut self, sources: &SourceTree) -> Self { let mut cache = FileTreeCache::new(sources); for e in &mut self.inner { let Some(span) = e.span else { continue; }; let Some(source_path) = sources.source_ids.get(&span.source_id) else { continue; }; let Ok(source) = cache.fetch(source_path) else { continue; }; e.location = e.compose_location(source); assert!( e.location.is_some(), "span {:?} is out of bounds of the source (len = {})", e.span, source.len() ); e.display = e.compose_display(source_path.clone(), &mut cache); } self } } impl ErrorMessage { fn compose_display(&self, source_path: PathBuf, cache: &mut FileTreeCache) -> Option { // We always pass color to ariadne as true, and then (currently) strip later. let config = Config::default().with_color(true); // Create a span tuple with the source path and the error range let span = Range::from(self.span?); let error_span = (source_path.clone(), span.start..span.end); let mut report = Report::build(ReportKind::Error, error_span.clone()) .with_config(config) .with_label(Label::new(error_span).with_message(&self.reason)); if let Some(code) = &self.code { report = report.with_code(code); } // I don't know how to set multiple hints... if !self.hints.is_empty() { report.set_help(&self.hints[0]); } if self.hints.len() > 1 { report.set_note(&self.hints[1]); } if self.hints.len() > 2 { report.set_message(&self.hints[2]); } let mut out = Vec::new(); report.finish().write(cache, &mut out).ok()?; String::from_utf8(out) .ok() .map(|x| crate::utils::maybe_strip_colors(x.as_str())) } fn compose_location(&self, source: &Source) -> Option { let span = self.span?; let start = source.get_offset_line(span.start)?; let end = source.get_offset_line(span.end)?; Some(SourceLocation { start: (start.1, start.2), end: (end.1, end.2), }) } } struct FileTreeCache<'a> { file_tree: &'a SourceTree, cache: HashMap, } impl<'a> FileTreeCache<'a> { fn new(file_tree: &'a SourceTree) -> Self { FileTreeCache { file_tree, cache: HashMap::new(), } } } impl Cache for FileTreeCache<'_> { type Storage = String; fn fetch(&mut self, id: &PathBuf) -> Result<&Source, impl fmt::Debug> { let file_contents = match self.file_tree.sources.get(id) { Some(v) => v, None => return Err(format!("Unknown file `{id:?}`")), }; Ok(self .cache .entry(id.clone()) .or_insert_with(|| Source::from(file_contents.to_string()))) } fn display<'b>(&self, id: &'b PathBuf) -> Option { id.as_os_str().to_str().map(str::to_string) } } ================================================ FILE: prqlc/prqlc/src/ir/decl.rs ================================================ use std::collections::HashMap; use std::fmt::Debug; use enum_as_inner::EnumAsInner; use itertools::Itertools; use serde::{Deserialize, Serialize}; use crate::codegen::write_ty; use crate::ir::pl; use crate::pr::{Span, Ty}; use crate::semantic::write_pl; /// Context of the pipeline. #[derive(Default, Serialize, Deserialize, Clone)] pub struct RootModule { /// Map of all accessible names (for each namespace) pub module: Module, pub span_map: HashMap, } #[derive(Default, PartialEq, Serialize, Deserialize, Clone)] pub struct Module { /// Names declared in this module. This is the important thing. pub names: HashMap, /// List of relative paths to include in search path when doing lookup in /// this module. /// /// Assuming we want to lookup `average`, which is in `std`. The root module /// does not contain the `average`. So instead: /// - look for `average` in root module and find nothing, /// - follow redirects in root module, /// - because of redirect `std`, so we look for `average` in `std`, /// - there is `average` is `std`, /// - result of the lookup is FQ ident `std.average`. pub redirects: Vec, /// A declaration that has been shadowed (overwritten) by this module. pub shadowed: Option>, } /// A struct containing information about a single declaration. #[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)] pub struct Decl { #[serde(skip_serializing_if = "Option::is_none")] pub declared_at: Option, pub kind: DeclKind, /// Some declarations (like relation columns) have an order to them. /// 0 means that the order is irrelevant. #[serde(skip_serializing_if = "is_zero")] pub order: usize, #[serde(skip_serializing_if = "Vec::is_empty")] pub annotations: Vec, } /// The Declaration itself. #[derive(Debug, PartialEq, Serialize, Deserialize, Clone, EnumAsInner)] pub enum DeclKind { /// A nested namespace Module(Module), /// Nested namespaces that do lookup in layers from top to bottom, stopping at first match. LayeredModules(Vec), TableDecl(TableDecl), InstanceOf(pl::Ident, Option), /// A single column. Contains id of target which is either: /// - an input relation that is source of this column or /// - a column expression. Column(usize), /// Contains a default value to be created in parent namespace when NS_INFER is matched. Infer(Box), Expr(Box), Ty(Ty), QueryDef(pl::QueryDef), /// Equivalent to the declaration pointed to by the fully qualified ident Import(pl::Ident), } #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] pub struct TableDecl { /// This will always be `TyKind::Array(TyKind::Tuple)`. /// It is being preparing to be merged with [DeclKind::Expr]. /// It used to keep track of columns. pub ty: Option, pub expr: TableExpr, } #[derive(Debug, PartialEq, Serialize, Deserialize, Clone, EnumAsInner)] pub enum TableExpr { /// In SQL, this is a CTE RelationVar(Box), /// Actual table in a database. In SQL it can be referred to by name. LocalTable, /// No expression (this decl just tracks a relation literal). None, /// A placeholder for a relation that will be provided later. Param(String), } #[derive(Clone, Eq, Debug, PartialEq, Serialize, Deserialize)] pub enum TableColumn { Wildcard, Single(Option), } impl std::fmt::Debug for RootModule { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.module.fmt(f) } } impl std::fmt::Debug for Module { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut ds = f.debug_struct("Module"); if !self.redirects.is_empty() { let redirects = self.redirects.iter().map(|x| x.to_string()).collect_vec(); ds.field("redirects", &redirects); } if self.names.len() < 15 { ds.field("names", &DebugNames(&self.names)); } else { ds.field("names", &format!("... {} entries ...", self.names.len())); } if self.shadowed.is_some() { ds.field("shadowed", &"(hidden)"); } ds.finish() } } struct DebugNames<'a>(&'a HashMap); impl std::fmt::Debug for DebugNames<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut dm = f.debug_map(); for (n, decl) in self.0.iter().sorted_by_key(|x| x.0) { dm.entry(n, decl); } dm.finish() } } impl Default for DeclKind { fn default() -> Self { DeclKind::Module(Module::default()) } } impl From for Decl { fn from(kind: DeclKind) -> Self { Decl { kind, declared_at: None, order: 0, annotations: Vec::new(), } } } impl std::fmt::Display for Decl { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(&self.kind, f) } } impl std::fmt::Display for DeclKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Module(arg0) => f.debug_tuple("Module").field(arg0).finish(), Self::LayeredModules(arg0) => f.debug_tuple("LayeredModules").field(arg0).finish(), Self::TableDecl(TableDecl { ty, expr }) => { write!( f, "TableDecl: {} {expr:?}", ty.as_ref().map(write_ty).unwrap_or_default() ) } Self::InstanceOf(arg0, _) => write!(f, "InstanceOf: {arg0}"), Self::Column(arg0) => write!(f, "Column (target {arg0})"), Self::Infer(arg0) => write!(f, "Infer (default: {arg0})"), Self::Expr(arg0) => write!(f, "Expr: {}", write_pl(*arg0.clone())), Self::Ty(arg0) => write!(f, "Ty: {}", write_ty(arg0)), Self::QueryDef(_) => write!(f, "QueryDef"), Self::Import(arg0) => write!(f, "Import {arg0}"), } } } fn is_zero(x: &usize) -> bool { *x == 0 } ================================================ FILE: prqlc/prqlc/src/ir/generic.rs ================================================ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use prqlc_parser::generic; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct ColumnSort { pub direction: SortDirection, pub column: T, } #[derive(Debug, Clone, Serialize, Default, Deserialize, PartialEq, Eq, JsonSchema)] pub enum SortDirection { #[default] Asc, Desc, } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)] pub struct WindowFrame { pub kind: WindowKind, pub range: generic::Range, } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)] pub enum WindowKind { Rows, Range, } impl WindowFrame { pub(crate) fn is_default(&self) -> bool { matches!( self, WindowFrame { kind: WindowKind::Rows, range: generic::Range { start: None, end: None } } ) } } impl Default for WindowFrame { fn default() -> Self { Self { kind: WindowKind::Rows, range: generic::Range::unbounded(), } } } ================================================ FILE: prqlc/prqlc/src/ir/mod.rs ================================================ //! Intermediate Representations of Abstract Syntax Tree //! pub use prqlc_parser::span::Span; pub mod decl; pub mod generic; pub mod pl; pub mod rq; ================================================ FILE: prqlc/prqlc/src/ir/pl/expr.rs ================================================ use std::collections::HashMap; use enum_as_inner::EnumAsInner; use prqlc_parser::generic; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use super::{Lineage, TransformCall}; use crate::codegen::write_ty; use crate::pr::{Ident, Literal, Span, Ty}; /// Expr is anything that has a value and thus a type. /// Most of these can contain other [Expr] themselves; literals should be [ExprKind::Literal]. #[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct Expr { #[serde(flatten)] pub kind: ExprKind, #[serde(skip_serializing_if = "Option::is_none")] pub span: Option, #[serde(skip_serializing_if = "Option::is_none")] pub alias: Option, /// Unique identifier of the node. Set exactly once during semantic::resolve. #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, /// For [Ident]s, this is id of node referenced by the ident #[serde(skip_serializing_if = "Option::is_none")] pub target_id: Option, /// Type of expression this node represents. /// [None] means that type should be inferred. #[serde(skip_serializing_if = "Option::is_none")] pub ty: Option, /// Information about where data of this expression will come from. /// /// Currently, this is used to infer relational pipeline frames. /// Must always exists if ty is a relation. #[serde(skip_serializing_if = "Option::is_none")] pub lineage: Option, #[serde(skip)] pub needs_window: bool, /// When true on [ExprKind::Tuple], this list will be flattened when placed /// in some other list. // TODO: maybe we should have a special ExprKind instead of this flag? #[serde(skip)] pub flatten: bool, } #[derive( Debug, EnumAsInner, PartialEq, Clone, Serialize, Deserialize, strum::AsRefStr, JsonSchema, )] pub enum ExprKind { Ident(Ident), All { within: Box, except: Box, }, Literal(Literal), Tuple(Vec), Array(Vec), FuncCall(FuncCall), Func(Box), TransformCall(TransformCall), SString(Vec), FString(Vec), Case(Vec), RqOperator { name: String, args: Vec, }, /// placeholder for values provided after query is compiled Param(String), /// When used instead of function body, the function will be translated to a RQ operator. /// Contains ident of the RQ operator. Internal(String), } /// Function call. #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct FuncCall { pub name: Box, pub args: Vec, #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub named_args: HashMap, } /// Function called with possibly missing positional arguments. /// May also contain environment that is needed to evaluate the body. #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct Func { /// Name of the function. Used for user-facing messages only. pub name_hint: Option, /// Type requirement for the function body expression. pub return_ty: Option, /// Expression containing parameter (and environment) references. pub body: Box, /// Positional function parameters. pub params: Vec, /// Named function parameters. pub named_params: Vec, /// Arguments that have already been provided. pub args: Vec, /// Additional variables that the body of the function may need to be /// evaluated. pub env: HashMap, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct FuncParam { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub ty: Option, pub default_value: Option>, } pub type Range = generic::Range>; pub type InterpolateItem = generic::InterpolateItem; pub type SwitchCase = generic::SwitchCase>; impl From for ExprKind { fn from(value: Literal) -> Self { ExprKind::Literal(value) } } impl From for ExprKind { fn from(value: Ident) -> Self { ExprKind::Ident(value) } } impl From for ExprKind { fn from(value: Func) -> Self { ExprKind::Func(Box::new(value)) } } impl std::fmt::Debug for Expr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut ds = f.debug_struct("Expr"); if let Some(x) = &self.span { ds.field("span", x); } ds.field("kind", &self.kind); if let Some(x) = &self.alias { ds.field("alias", x); } if let Some(x) = &self.id { ds.field("id", x); } if let Some(x) = &self.target_id { ds.field("target_id", x); } if self.needs_window { ds.field("needs_window", &self.needs_window); } if self.flatten { ds.field("flatten", &self.flatten); } if let Some(x) = &self.ty { // DebugTy is needed to get around string quotes that // would be printed if we Debug-ed the string directly. struct DebugTy<'a>(&'a Ty); impl std::fmt::Debug for DebugTy<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&write_ty(self.0)) } } ds.field("ty", &DebugTy(x)); } if let Some(x) = &self.lineage { ds.field("lineage", x); } ds.finish() } } ================================================ FILE: prqlc/prqlc/src/ir/pl/extra.rs ================================================ use enum_as_inner::EnumAsInner; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::ir::generic::WindowKind; use crate::ir::pl::{Expr, ExprKind, Func, FuncCall, Ident, Range}; use crate::pr::Ty; impl FuncCall { pub fn new_simple(name: Expr, args: Vec) -> Self { FuncCall { name: Box::new(name), args, named_args: Default::default(), } } } /// An expression that may have already been converted to a type. #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, EnumAsInner, JsonSchema)] pub enum TyOrExpr { Ty(Ty), Expr(Box), } impl Func { pub(crate) fn as_debug_name(&self) -> &str { let ident = self.name_hint.as_ref(); ident.map(|n| n.name.as_str()).unwrap_or("") } } pub type WindowFrame = crate::ir::generic::WindowFrame>; pub type ColumnSort = crate::ir::generic::ColumnSort>; /// FuncCall with better typing. Returns the modified table. #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct TransformCall { pub input: Box, pub kind: Box, /// Grouping of values in columns #[serde(default, skip_serializing_if = "Option::is_none")] pub partition: Option>, /// Windowing frame of columns #[serde(default, skip_serializing_if = "WindowFrame::is_default")] pub frame: WindowFrame, /// Windowing order of columns #[serde(default, skip_serializing_if = "Vec::is_empty")] pub sort: Vec, } #[derive( Debug, PartialEq, Clone, Serialize, Deserialize, strum::AsRefStr, EnumAsInner, JsonSchema, )] pub enum TransformKind { Derive { assigns: Box, }, Select { assigns: Box, }, Filter { filter: Box, }, Aggregate { assigns: Box, }, Sort { by: Vec, }, Take { range: Range, }, Join { side: JoinSide, with: Box, filter: Box, }, Group { by: Box, pipeline: Box, }, Window { kind: WindowKind, range: Range, pipeline: Box, }, Append(Box), Loop(Box), } /// A reference to a table that is not in scope of this query. #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)] pub enum TableExternRef { /// Actual table in a database, that we can refer to by name in SQL LocalTable(Ident), /// Placeholder for a relation that will be provided later. /// This is very similar to relational s-strings and may not even be needed for now, so /// it's not documented anywhere. But it will be used in the future. Param(String), // TODO: add other sources such as files, URLs } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)] pub enum JoinSide { Inner, Left, Right, Full, } impl Expr { pub fn new(kind: impl Into) -> Self { Expr { id: None, kind: kind.into(), span: None, target_id: None, ty: None, lineage: None, needs_window: false, alias: None, flatten: false, } } } ================================================ FILE: prqlc/prqlc/src/ir/pl/fold.rs ================================================ /// A trait to "fold" a PRQL AST (similar to a visitor), so we can transitively /// apply some logic to a whole tree by just defining how we want to handle each /// type. use itertools::Itertools; use super::*; use crate::pr::{Ty, TyFunc, TyKind, TyTupleField}; use crate::Result; // Fold pattern: // - https://rust-unofficial.github.io/patterns/patterns/creational/fold.html // Good discussions on the visitor / fold pattern: // - https://github.com/rust-unofficial/patterns/discussions/236 (within this, // this comment looked interesting: https://github.com/rust-unofficial/patterns/discussions/236#discussioncomment-393517) // - https://news.ycombinator.com/item?id=25620110 // For some functions, we want to call a default impl, because copying & // pasting everything apart from a specific match is lots of repetition. So // we define a function outside the trait, by default call it, and let // implementors override the default while calling the function directly for // some cases. Ref https://stackoverflow.com/a/66077767/3064736 pub trait PlFold { fn fold_stmt(&mut self, mut stmt: Stmt) -> Result { stmt.kind = fold_stmt_kind(self, stmt.kind)?; Ok(stmt) } fn fold_stmts(&mut self, stmts: Vec) -> Result> { stmts.into_iter().map(|stmt| self.fold_stmt(stmt)).collect() } fn fold_expr(&mut self, mut expr: Expr) -> Result { expr.kind = self.fold_expr_kind(expr.kind)?; Ok(expr) } fn fold_expr_kind(&mut self, expr_kind: ExprKind) -> Result { fold_expr_kind(self, expr_kind) } fn fold_exprs(&mut self, exprs: Vec) -> Result> { exprs.into_iter().map(|node| self.fold_expr(node)).collect() } fn fold_var_def(&mut self, var_def: VarDef) -> Result { fold_var_def(self, var_def) } fn fold_type_def(&mut self, ty_def: TypeDef) -> Result { Ok(TypeDef { name: ty_def.name, value: self.fold_type(ty_def.value)?, }) } fn fold_module_def(&mut self, module_def: ModuleDef) -> Result { fold_module_def(self, module_def) } fn fold_func_call(&mut self, func_call: FuncCall) -> Result { fold_func_call(self, func_call) } fn fold_transform_call(&mut self, transform_call: TransformCall) -> Result { fold_transform_call(self, transform_call) } fn fold_func(&mut self, func: Func) -> Result { fold_func(self, func) } fn fold_interpolate_item(&mut self, sstring_item: InterpolateItem) -> Result { fold_interpolate_item(self, sstring_item) } fn fold_type(&mut self, t: Ty) -> Result { fold_type(self, t) } fn fold_window(&mut self, window: WindowFrame) -> Result { fold_window(self, window) } } pub fn fold_expr_kind(fold: &mut T, expr_kind: ExprKind) -> Result { use ExprKind::*; Ok(match expr_kind { Ident(ident) => Ident(ident), All { within, except } => All { within: Box::new(fold.fold_expr(*within)?), except: Box::new(fold.fold_expr(*except)?), }, Tuple(items) => Tuple(fold.fold_exprs(items)?), Array(items) => Array(fold.fold_exprs(items)?), SString(items) => SString( items .into_iter() .map(|x| fold.fold_interpolate_item(x)) .try_collect()?, ), FString(items) => FString( items .into_iter() .map(|x| fold.fold_interpolate_item(x)) .try_collect()?, ), Case(cases) => Case(fold_cases(fold, cases)?), FuncCall(func_call) => FuncCall(fold.fold_func_call(func_call)?), Func(closure) => Func(Box::new(fold.fold_func(*closure)?)), TransformCall(transform) => TransformCall(fold.fold_transform_call(transform)?), RqOperator { name, args } => RqOperator { name, args: fold.fold_exprs(args)?, }, // None of these capture variables, so we don't need to fold them. Param(_) | Internal(_) | Literal(_) => expr_kind, }) } pub fn fold_stmt_kind(fold: &mut T, stmt_kind: StmtKind) -> Result { use StmtKind::*; Ok(match stmt_kind { VarDef(var_def) => VarDef(fold.fold_var_def(var_def)?), TypeDef(type_def) => TypeDef(fold.fold_type_def(type_def)?), ModuleDef(module_def) => ModuleDef(fold.fold_module_def(module_def)?), QueryDef(_) | ImportDef(_) => stmt_kind, }) } fn fold_module_def(fold: &mut F, module_def: ModuleDef) -> Result { Ok(ModuleDef { name: module_def.name, stmts: fold.fold_stmts(module_def.stmts)?, }) } pub fn fold_var_def(fold: &mut F, var_def: VarDef) -> Result { Ok(VarDef { name: var_def.name, value: fold_optional_box(fold, var_def.value)?, ty: var_def.ty.map(|x| fold.fold_type(x)).transpose()?, }) } pub fn fold_window(fold: &mut F, window: WindowFrame) -> Result { Ok(WindowFrame { kind: window.kind, range: fold_range(fold, window.range)?, }) } pub fn fold_range(fold: &mut F, Range { start, end }: Range) -> Result { Ok(Range { start: fold_optional_box(fold, start)?, end: fold_optional_box(fold, end)?, }) } // This aren't strictly in the hierarchy, so we don't need to // have an assoc. function for `fold_optional_box` — we just // call out to the function in this module pub fn fold_optional_box( fold: &mut F, opt: Option>, ) -> Result>> { Ok(opt.map(|n| fold.fold_expr(*n)).transpose()?.map(Box::from)) } pub fn fold_interpolate_item( fold: &mut F, interpolate_item: InterpolateItem, ) -> Result { Ok(match interpolate_item { InterpolateItem::String(string) => InterpolateItem::String(string), InterpolateItem::Expr { expr, format } => InterpolateItem::Expr { expr: Box::new(fold.fold_expr(*expr)?), format, }, }) } fn fold_cases(fold: &mut F, cases: Vec) -> Result> { cases .into_iter() .map(|c| fold_switch_case(fold, c)) .try_collect() } pub fn fold_switch_case(fold: &mut F, case: SwitchCase) -> Result { Ok(SwitchCase { condition: Box::new(fold.fold_expr(*case.condition)?), value: Box::new(fold.fold_expr(*case.value)?), }) } pub fn fold_column_sorts( fold: &mut F, sort: Vec, ) -> Result> { sort.into_iter() .map(|s| fold_column_sort(fold, s)) .try_collect() } pub fn fold_column_sort( fold: &mut T, sort_column: ColumnSort, ) -> Result { Ok(ColumnSort { direction: sort_column.direction, column: Box::new(fold.fold_expr(*sort_column.column)?), }) } pub fn fold_func_call(fold: &mut T, func_call: FuncCall) -> Result { Ok(FuncCall { name: Box::new(fold.fold_expr(*func_call.name)?), args: fold.fold_exprs(func_call.args)?, named_args: func_call .named_args .into_iter() .map(|(name, expr)| fold.fold_expr(expr).map(|e| (name, e))) .try_collect()?, }) } pub fn fold_transform_call( fold: &mut T, t: TransformCall, ) -> Result { Ok(TransformCall { kind: Box::new(fold_transform_kind(fold, *t.kind)?), input: Box::new(fold.fold_expr(*t.input)?), partition: fold_optional_box(fold, t.partition)?, frame: fold.fold_window(t.frame)?, sort: fold_column_sorts(fold, t.sort)?, }) } pub fn fold_transform_kind( fold: &mut T, t: TransformKind, ) -> Result { use TransformKind::*; Ok(match t { Derive { assigns } => Derive { assigns: Box::new(fold.fold_expr(*assigns)?), }, Select { assigns } => Select { assigns: Box::new(fold.fold_expr(*assigns)?), }, Filter { filter } => Filter { filter: Box::new(fold.fold_expr(*filter)?), }, Aggregate { assigns } => Aggregate { assigns: Box::new(fold.fold_expr(*assigns)?), }, Sort { by } => Sort { by: fold_column_sorts(fold, by)?, }, Take { range } => Take { range: fold_range(fold, range)?, }, Join { side, with, filter } => Join { side, with: Box::new(fold.fold_expr(*with)?), filter: Box::new(fold.fold_expr(*filter)?), }, Append(bottom) => Append(Box::new(fold.fold_expr(*bottom)?)), Group { by, pipeline } => Group { by: Box::new(fold.fold_expr(*by)?), pipeline: Box::new(fold.fold_expr(*pipeline)?), }, Window { kind, range, pipeline, } => Window { kind, range: fold_range(fold, range)?, pipeline: Box::new(fold.fold_expr(*pipeline)?), }, Loop(pipeline) => Loop(Box::new(fold.fold_expr(*pipeline)?)), }) } pub fn fold_func(fold: &mut T, func: Func) -> Result { Ok(Func { body: Box::new(fold.fold_expr(*func.body)?), args: func .args .into_iter() .map(|item| fold.fold_expr(item)) .try_collect()?, ..func }) } pub fn fold_func_param( fold: &mut T, nodes: Vec, ) -> Result> { nodes .into_iter() .map(|param| { Ok(FuncParam { default_value: fold_optional_box(fold, param.default_value)?, ..param }) }) .try_collect() } #[inline] pub fn fold_type_opt(fold: &mut T, ty: Option) -> Result> { ty.map(|t| fold.fold_type(t)).transpose() } #[inline] pub fn fold_type_opt_box( fold: &mut T, ty: Option>, ) -> Result>> { ty.map(|t| fold.fold_type(*t).map(Box::new)).transpose() } pub fn fold_type(fold: &mut T, ty: Ty) -> Result { Ok(Ty { kind: match ty.kind { TyKind::Tuple(fields) => TyKind::Tuple( fields .into_iter() .map(|field| -> Result<_> { Ok(match field { TyTupleField::Single(name, ty) => { TyTupleField::Single(name, fold_type_opt(fold, ty)?) } TyTupleField::Wildcard(ty) => { TyTupleField::Wildcard(fold_type_opt(fold, ty)?) } }) }) .try_collect()?, ), TyKind::Array(ty) => TyKind::Array(fold_type_opt_box(fold, ty)?), TyKind::Function(func) => TyKind::Function( func.map(|f| -> Result<_> { Ok(TyFunc { params: f .params .into_iter() .map(|a| fold_type_opt(fold, a)) .try_collect()?, return_ty: fold_type_opt(fold, f.return_ty.map(|x| *x))?.map(Box::new), name_hint: f.name_hint, }) }) .transpose()?, ), TyKind::Ident(_) | TyKind::Primitive(_) => ty.kind, }, span: ty.span, name: ty.name, }) } ================================================ FILE: prqlc/prqlc/src/ir/pl/lineage.rs ================================================ use std::collections::HashSet; use std::fmt::{Debug, Display, Formatter}; use enum_as_inner::EnumAsInner; use itertools::{Itertools, Position}; use schemars::JsonSchema; use serde::{Deserialize, Serialize, Serializer}; use super::Ident; /// Represents the object that is manipulated by the pipeline transforms. /// Similar to a view in a database or a data frame. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct Lineage { pub columns: Vec, pub inputs: Vec, // A hack that allows name retention when applying `ExprKind::All { except }` #[serde(skip)] pub prev_columns: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] pub struct LineageInput { /// Id of the node in AST that declares this input. pub id: usize, /// Local name of this input within a query. pub name: String, /// Fully qualified name of the table that provides the data for this input. pub table: Ident, } #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, EnumAsInner, JsonSchema)] pub enum LineageColumn { Single { name: Option, // id of the defining expr (which can be actual expr or lineage input expr) target_id: usize, // if target is a relation, this is the name within the relation target_name: Option, }, /// All columns (including unknown ones) from an input (i.e. `foo_table.*`) All { input_id: usize, #[serde(serialize_with = "sorted_set")] except: HashSet, }, } pub fn sorted_set( value: &HashSet, serializer: S, ) -> Result { value .iter() .sorted() .collect::>() .serialize(serializer) } impl Display for Lineage { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { display_lineage(self, f, false) } } fn display_lineage(lineage: &Lineage, f: &mut Formatter, display_ids: bool) -> std::fmt::Result { write!(f, "[")?; for (pos, col) in lineage.columns.iter().with_position() { let is_last = matches!(pos, Position::Last | Position::Only); display_lineage_column(col, f, display_ids)?; if !is_last { write!(f, ", ")?; } } write!(f, "]") } fn display_lineage_column( col: &LineageColumn, f: &mut Formatter, display_ids: bool, ) -> std::fmt::Result { match col { LineageColumn::All { input_id, .. } => { write!(f, "{input_id}.*")?; } LineageColumn::Single { name, target_id, .. } => { if let Some(name) = name { write!(f, "{name}")? } else { write!(f, "?")? } if display_ids { write!(f, ":{target_id}")? } } } Ok(()) } ================================================ FILE: prqlc/prqlc/src/ir/pl/mod.rs ================================================ //! Pipelined Language AST //! //! Abstract Syntax Tree for the first part of PRQL compiler. //! It can represent basic expressions, lists, pipelines, function calls & //! definitions, variable declarations and more. //! //! The central struct here is [Expr] and its [ExprKind]. //! //! Top-level construct is a list of statements [`Vec`]. pub use crate::pr::Literal; pub use crate::pr::QueryDef; pub use crate::pr::{BinOp, BinaryExpr, Ident, UnOp, UnaryExpr}; pub use self::expr::*; pub use self::extra::*; pub use self::fold::*; pub use self::lineage::*; pub use self::stmt::*; pub use self::utils::*; mod expr; mod extra; mod fold; mod lineage; mod stmt; mod utils; pub fn print_mem_sizes() { use std::mem::size_of; use crate::ir::{decl, generic, pl, rq}; use crate::pr::{PrimitiveSet, Ty, TyFunc, TyKind, TyTupleField}; use crate::{ErrorMessage, ErrorMessages, SourceTree, Span}; println!("{:16}= {}", "Annotation", size_of::()); println!("{:16}= {}", "BinaryExpr", size_of::()); println!("{:16}= {}", "BinOp", size_of::()); println!("{:16}= {}", "ColumnSort", size_of::()); println!("{:16}= {}", "decl::Decl", size_of::()); println!("{:16}= {}", "decl::DeclKind", size_of::()); println!("{:16}= {}", "decl::Module", size_of::()); println!("{:16}= {}", "decl::TableDecl", size_of::()); println!("{:16}= {}", "decl::TableExpr", size_of::()); println!("{:16}= {}", "ErrorMessage", size_of::()); println!("{:16}= {}", "ErrorMessages", size_of::()); println!("{:16}= {}", "ExprKind", size_of::()); println!("{:16}= {}", "Func", size_of::()); println!("{:16}= {}", "FuncCall", size_of::()); println!("{:16}= {}", "FuncParam", size_of::()); println!( "{:16}= {}", "generic::SortDirection", size_of::() ); println!( "{:16}= {}", "generic::WindowKind", size_of::() ); println!("{:16}= {}", "InterpolateItem", size_of::()); println!("{:16}= {}", "JoinSide", size_of::()); println!("{:16}= {}", "Lineage", size_of::()); println!("{:16}= {}", "LineageColumn", size_of::()); println!("{:16}= {}", "LineageInput", size_of::()); println!("{:16}= {}", "ModuleDef", size_of::()); println!("{:16}= {}", "pl::Expr", size_of::()); println!("{:16}= {}", "PrimitiveSet", size_of::()); println!("{:16}= {}", "QueryDef", size_of::()); println!("{:16}= {}", "Range", size_of::()); println!("{:16}= {}", "rq::Expr", size_of::()); println!( "{:16}= {}", "rq::RelationalQuery", size_of::() ); println!("{:16}= {}", "rq::TableRef", size_of::()); println!("{:16}= {}", "SourceTree", size_of::()); println!("{:16}= {}", "Span", size_of::()); println!("{:16}= {}", "Stmt", size_of::()); println!("{:16}= {}", "StmtKind", size_of::()); println!("{:16}= {}", "SwitchCase", size_of::()); println!("{:16}= {}", "TableExternRef", size_of::()); println!("{:16}= {}", "TransformCall", size_of::()); println!("{:16}= {}", "TransformKind", size_of::()); println!("{:16}= {}", "TupleField", size_of::()); println!("{:16}= {}", "Ty", size_of::()); println!("{:16}= {}", "TyFunc", size_of::()); println!("{:16}= {}", "TyKind", size_of::()); println!("{:16}= {}", "TyOrExpr", size_of::()); println!("{:16}= {}", "TypeDef", size_of::()); println!("{:16}= {}", "UnaryExpr", size_of::()); println!("{:16}= {}", "UnOp", size_of::()); println!("{:16}= {}", "VarDef", size_of::()); println!("{:16}= {}", "WindowFrame", size_of::()); } ================================================ FILE: prqlc/prqlc/src/ir/pl/stmt.rs ================================================ use enum_as_inner::EnumAsInner; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::pr::Ident; use crate::pr::QueryDef; use crate::pr::{Span, Ty}; use super::expr::Expr; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct Stmt { #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, #[serde(flatten)] pub kind: StmtKind, #[serde(skip_serializing_if = "Option::is_none")] pub span: Option, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub annotations: Vec, } #[derive(Debug, EnumAsInner, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub enum StmtKind { QueryDef(Box), VarDef(VarDef), TypeDef(TypeDef), ModuleDef(ModuleDef), ImportDef(ImportDef), } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct VarDef { pub name: String, pub value: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub ty: Option, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct TypeDef { pub name: String, pub value: Ty, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct ModuleDef { pub name: String, pub stmts: Vec, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct ImportDef { pub alias: Option, pub name: Ident, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct Annotation { pub expr: Box, } ================================================ FILE: prqlc/prqlc/src/ir/pl/utils.rs ================================================ use super::{Expr, ExprKind, FuncCall}; use crate::pr::Ident; pub fn maybe_binop(left: Option, op_name: &[&str], right: Option) -> Option { match (left, right) { (Some(left), Some(right)) => Some(new_binop(left, op_name, right)), (left, right) => left.or(right), } } pub fn new_binop(left: Expr, op_name: &[&str], right: Expr) -> Expr { Expr::new(ExprKind::FuncCall(FuncCall { name: Box::new(Expr::new(Ident::from_path(op_name.to_vec()))), args: vec![left, right], named_args: Default::default(), })) } ================================================ FILE: prqlc/prqlc/src/ir/rq/expr.rs ================================================ use enum_as_inner::EnumAsInner; use prqlc_parser::generic; use prqlc_parser::lexer::lr::Literal; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use super::CId; use crate::Span; /// Analogous to [crate::ir::pl::Expr], but with fewer kinds. #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct Expr { pub kind: ExprKind, pub span: Option, } pub(super) type Range = generic::Range; pub(super) type InterpolateItem = generic::InterpolateItem; pub(super) type SwitchCase = generic::SwitchCase; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, EnumAsInner, JsonSchema)] pub enum ExprKind { ColumnRef(CId), // https://github.com/dtolnay/serde-yaml/issues/363 // We should repeat this if we encounter any other nested enums. #[cfg_attr( feature = "serde_yaml", serde(with = "serde_yaml::with::singleton_map"), schemars(with = "Literal") )] Literal(Literal), SString(Vec), Case(Vec), Operator { name: String, args: Vec, }, /// Placeholder for expressions provided after compilation. Param(String), Array(Vec), } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)] pub enum UnOp { Neg, Not, } ================================================ FILE: prqlc/prqlc/src/ir/rq/fold.rs ================================================ /// A trait to "fold" a PRQL AST (similar to a visitor), so we can transitively /// apply some logic to a whole tree by just defining how we want to handle each /// type. use itertools::Itertools; use super::*; use crate::ir::generic::{ColumnSort, WindowFrame}; use crate::Result; // Fold pattern: // - https://rust-unofficial.github.io/patterns/patterns/creational/fold.html // Good discussions on the visitor / fold pattern: // - https://github.com/rust-unofficial/patterns/discussions/236 (within this, // this comment looked interesting: https://github.com/rust-unofficial/patterns/discussions/236#discussioncomment-393517) // - https://news.ycombinator.com/item?id=25620110 // For some functions, we want to call a default impl, because copying & // pasting everything apart from a specific match is lots of repetition. So // we define a function outside the trait, by default call it, and let // implementors override the default while calling the function directly for // some cases. Ref https://stackoverflow.com/a/66077767/3064736 pub trait RqFold { fn fold_transform(&mut self, transform: Transform) -> Result { fold_transform(self, transform) } fn fold_transforms(&mut self, transforms: Vec) -> Result> { fold_transforms(self, transforms) } fn fold_table(&mut self, table: TableDecl) -> Result { fold_table(self, table) } fn fold_relation(&mut self, relation: Relation) -> Result { fold_relation(self, relation) } fn fold_relation_kind(&mut self, rel_kind: RelationKind) -> Result { fold_relation_kind(self, rel_kind) } fn fold_table_ref(&mut self, table_ref: TableRef) -> Result { fold_table_ref(self, table_ref) } fn fold_query(&mut self, query: RelationalQuery) -> Result { fold_query(self, query) } fn fold_expr(&mut self, mut expr: Expr) -> Result { expr.kind = self.fold_expr_kind(expr.kind)?; Ok(expr) } fn fold_expr_kind(&mut self, kind: ExprKind) -> Result { fold_expr_kind(self, kind) } fn fold_relation_column(&mut self, col: RelationColumn) -> Result { Ok(col) } fn fold_cid(&mut self, cid: CId) -> Result { Ok(cid) } fn fold_cids(&mut self, cids: Vec) -> Result> { cids.into_iter().map(|i| self.fold_cid(i)).try_collect() } fn fold_compute(&mut self, compute: Compute) -> Result { fold_compute(self, compute) } } fn fold_compute(fold: &mut F, compute: Compute) -> Result { Ok(Compute { id: fold.fold_cid(compute.id)?, expr: fold.fold_expr(compute.expr)?, window: compute.window.map(|w| fold_window(fold, w)).transpose()?, is_aggregation: compute.is_aggregation, }) } fn fold_window(fold: &mut F, w: Window) -> Result { Ok(Window { frame: WindowFrame { kind: w.frame.kind, range: Range { start: w.frame.range.start.map(|x| fold.fold_expr(x)).transpose()?, end: w.frame.range.end.map(|x| fold.fold_expr(x)).transpose()?, }, }, partition: fold.fold_cids(w.partition)?, sort: fold_column_sorts(fold, w.sort)?, }) } pub fn fold_table(fold: &mut F, t: TableDecl) -> Result { Ok(TableDecl { id: t.id, name: t.name, relation: fold.fold_relation(t.relation)?, }) } pub fn fold_relation(fold: &mut F, relation: Relation) -> Result { Ok(Relation { kind: fold.fold_relation_kind(relation.kind)?, columns: relation.columns, }) } pub fn fold_relation_kind( fold: &mut F, rel: RelationKind, ) -> Result { Ok(match rel { RelationKind::ExternRef(table_ref) => RelationKind::ExternRef(table_ref), RelationKind::Pipeline(transforms) => { RelationKind::Pipeline(fold.fold_transforms(transforms)?) } RelationKind::Literal(lit) => RelationKind::Literal(lit), RelationKind::SString(items) => RelationKind::SString(fold_interpolate_items(fold, items)?), RelationKind::BuiltInFunction { name, args } => RelationKind::BuiltInFunction { name, args: args.into_iter().map(|a| fold.fold_expr(a)).try_collect()?, }, }) } pub fn fold_table_ref(fold: &mut F, table_ref: TableRef) -> Result { Ok(TableRef { name: table_ref.name, source: table_ref.source, columns: table_ref .columns .into_iter() .map(|(col, cid)| -> Result<_> { Ok((fold.fold_relation_column(col)?, fold.fold_cid(cid)?)) }) .try_collect()?, prefer_cte: table_ref.prefer_cte, }) } pub fn fold_query( fold: &mut F, query: RelationalQuery, ) -> Result { Ok(RelationalQuery { def: query.def, relation: fold.fold_relation(query.relation)?, tables: query .tables .into_iter() .map(|t| fold.fold_table(t)) .try_collect()?, }) } pub fn fold_transforms( fold: &mut F, transforms: Vec, ) -> Result> { transforms .into_iter() .map(|t| fold.fold_transform(t)) .try_collect() } pub fn fold_transform( fold: &mut T, mut transform: Transform, ) -> Result { use Transform::*; transform = match transform { From(tid) => From(fold.fold_table_ref(tid)?), Compute(compute) => Compute(fold.fold_compute(compute)?), Aggregate { partition, compute } => Aggregate { partition: fold.fold_cids(partition)?, compute: fold.fold_cids(compute)?, }, Select(ids) => Select(fold.fold_cids(ids)?), Filter(i) => Filter(fold.fold_expr(i)?), Sort(sorts) => Sort(fold_column_sorts(fold, sorts)?), Take(take) => Take(super::Take { partition: fold.fold_cids(take.partition)?, sort: fold_column_sorts(fold, take.sort)?, range: take.range, }), Join { side, with, filter } => Join { side, with: fold.fold_table_ref(with)?, filter: fold.fold_expr(filter)?, }, Append(bottom) => Append(fold.fold_table_ref(bottom)?), Loop(transforms) => Loop(fold_transforms(fold, transforms)?), }; Ok(transform) } pub fn fold_column_sorts( fold: &mut T, sorts: Vec>, ) -> Result>> { sorts .into_iter() .map(|s| -> Result> { Ok(ColumnSort { column: fold.fold_cid(s.column)?, direction: s.direction, }) }) .try_collect() } pub fn fold_expr_kind(fold: &mut F, kind: ExprKind) -> Result { Ok(match kind { ExprKind::ColumnRef(cid) => ExprKind::ColumnRef(fold.fold_cid(cid)?), ExprKind::SString(items) => ExprKind::SString(fold_interpolate_items(fold, items)?), ExprKind::Case(cases) => ExprKind::Case( cases .into_iter() .map(|c| fold_switch_case(fold, c)) .try_collect()?, ), ExprKind::Operator { name, args } => ExprKind::Operator { name, args: args.into_iter().map(|a| fold.fold_expr(a)).try_collect()?, }, ExprKind::Param(id) => ExprKind::Param(id), ExprKind::Literal(_) => kind, ExprKind::Array(exprs) => { ExprKind::Array(exprs.into_iter().map(|e| fold.fold_expr(e)).try_collect()?) } }) } /// Helper pub fn fold_optional_box( fold: &mut F, opt: Option>, ) -> Result>> { Ok(match opt { Some(e) => Some(Box::new(fold.fold_expr(*e)?)), None => None, }) } pub fn fold_interpolate_items( fold: &mut T, items: Vec, ) -> Result> { items .into_iter() .map(|i| fold_interpolate_item(fold, i)) .try_collect() } pub fn fold_interpolate_item( fold: &mut T, item: InterpolateItem, ) -> Result { Ok(match item { InterpolateItem::String(string) => InterpolateItem::String(string), InterpolateItem::Expr { expr, format } => InterpolateItem::Expr { expr: Box::new(fold.fold_expr(*expr)?), format, }, }) } pub fn fold_switch_case(fold: &mut F, case: SwitchCase) -> Result { Ok(SwitchCase { condition: fold.fold_expr(case.condition)?, value: fold.fold_expr(case.value)?, }) } ================================================ FILE: prqlc/prqlc/src/ir/rq/ids.rs ================================================ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; /// Column id #[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Ord, PartialOrd, JsonSchema)] pub struct CId(usize); impl CId { pub fn get(&self) -> usize { self.0 } } impl From for CId { fn from(id: usize) -> Self { CId(id) } } impl std::fmt::Debug for CId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "column-{}", self.0) } } /// Table id #[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)] pub struct TId(usize); impl TId { pub fn get(&self) -> usize { self.0 } } impl From for TId { fn from(id: usize) -> Self { TId(id) } } impl std::fmt::Debug for TId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "table-{}", self.0) } } ================================================ FILE: prqlc/prqlc/src/ir/rq/mod.rs ================================================ //! Relational Query AST //! //! Strictly typed AST for describing relational queries. use enum_as_inner::EnumAsInner; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; pub use expr::{Expr, ExprKind, UnOp}; use expr::{InterpolateItem, Range, SwitchCase}; pub use fold::*; pub use ids::*; use prqlc_parser::lexer::lr; pub use transform::*; pub use utils::*; use super::pl::QueryDef; use super::pl::TableExternRef; mod expr; mod fold; mod ids; mod transform; mod utils; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct RelationalQuery { pub def: QueryDef, pub tables: Vec, pub relation: Relation, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct Relation { pub kind: RelationKind, /// Column definitions. /// This is the interface of the table that can be referenced from other tables. pub columns: Vec, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, EnumAsInner, JsonSchema)] pub enum RelationKind { #[cfg_attr( feature = "serde_yaml", serde(with = "serde_yaml::with::singleton_map"), schemars(with = "TableExternRef") )] ExternRef(TableExternRef), Pipeline(Vec), Literal(RelationLiteral), SString(Vec), BuiltInFunction { name: String, args: Vec, }, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct RelationLiteral { /// Column names pub columns: Vec, /// Row-oriented data pub rows: Vec>, } #[derive(Debug, PartialEq, Clone, Eq, Hash, Serialize, Deserialize, EnumAsInner, JsonSchema)] pub enum RelationColumn { /// A single column that may have a name. /// Unnamed columns cannot be referenced. Single(Option), /// Means "and other unmentioned columns". Does not mean "all columns". Wildcard, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct TableDecl { /// An id for this table, unique within all tables in this query. pub id: TId, /// Name hint for this declaration (name of the CTE) pub name: Option, /// Table's contents. pub relation: Relation, } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)] pub struct TableRef { /// Referenced table pub source: TId, /// New column definitions are required because there may be multiple instances /// of this table in the same query pub columns: Vec<(RelationColumn, CId)>, /// Name hint for relation within this pipeline (table alias) pub name: Option, /// We prefer CTEs for most syntaxes but some like UNION works best with subqueries. pub prefer_cte: bool, } ================================================ FILE: prqlc/prqlc/src/ir/rq/transform.rs ================================================ use enum_as_inner::EnumAsInner; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use super::*; use crate::ir::generic::ColumnSort; use crate::ir::generic::WindowFrame; use crate::ir::pl::JoinSide; /// Transformation of a table. #[derive( Debug, PartialEq, Clone, Serialize, Deserialize, strum::AsRefStr, EnumAsInner, JsonSchema, )] pub enum Transform { From(TableRef), Compute(Compute), Select(Vec), Filter(Expr), Aggregate { partition: Vec, compute: Vec, }, Sort(Vec>), Take(Take), Join { side: JoinSide, with: TableRef, filter: Expr, }, Append(TableRef), Loop(Vec), } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct Take { pub range: Range, pub partition: Vec, pub sort: Vec>, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct Compute { pub id: CId, pub expr: Expr, /// Parameters for window functions (or expressions). #[serde(skip_serializing_if = "Option::is_none", default)] pub window: Option, /// Must be set exactly on columns used in [Transform::Aggregate]. #[serde(skip_serializing_if = "is_false", default)] pub is_aggregation: bool, } /// Transformation of a table. #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Default, JsonSchema)] pub struct Window { pub frame: WindowFrame, pub partition: Vec, pub sort: Vec>, } fn is_false(b: &bool) -> bool { !b } ================================================ FILE: prqlc/prqlc/src/ir/rq/utils.rs ================================================ use super::Expr; use super::ExprKind; pub fn new_binop(left: Expr, operator_name: &str, right: Expr) -> Expr { Expr { kind: ExprKind::Operator { name: operator_name.to_string(), args: vec![left, right], }, span: None, } } pub fn maybe_binop(left: Option, operator_name: &str, right: Option) -> Option { match (left, right) { (Some(left), Some(right)) => Some(Expr { kind: ExprKind::Operator { name: operator_name.to_string(), args: vec![left, right], }, span: None, }), (left, right) => left.or(right), } } ================================================ FILE: prqlc/prqlc/src/lib.rs ================================================ //! # prqlc //! //! Compiler for PRQL language. Targets SQL and exposes PL and RQ abstract //! syntax trees. //! //! You probably want to start with [compile] wrapper function. //! //! For more granular access, refer to this diagram: //! ```ascii //! PRQL //! //! (parse) │ ▲ //! prql_to_pl │ │ pl_to_prql //! │ │ //! ▼ │ json::from_pl //! ────────► //! PL AST PL JSON //! ◄──────── //! │ json::to_pl //! │ //! (resolve) │ //! pl_to_rq │ //! │ //! │ //! ▼ json::from_rq //! ────────► //! RQ AST RQ JSON //! ◄──────── //! │ json::to_rq //! │ //! rq_to_sql │ //! ▼ //! //! SQL //! ``` //! #![doc = include_str!("../ARCHITECTURE.md")] // TODO: remove when enum-as-inner is updated with the fix from // https://github.com/bluejekyll/enum-as-inner/pull/108 // This suppresses false positive warnings from Rust 1.92+: // https://github.com/rust-lang/rust/issues/147648 #![allow(unused_assignments)] //! //! ## Common use-cases //! //! - Compile PRQL queries to SQL at run time. //! //! ``` //! # fn main() -> Result<(), prqlc::ErrorMessages> { //! let sql = prqlc::compile( //! "from albums | select {title, artist_id}", //! &prqlc::Options::default().no_format() //! )?; //! assert_eq!(&sql[..35], "SELECT title, artist_id FROM albums"); //! # Ok(()) //! # } //! ``` //! //! - Compile PRQL queries to SQL at build time. //! //! For inline strings, use the `prqlc-macros` crate; for example: //! ```ignore //! let sql: &str = prql_to_sql!("from albums | select {title, artist_id}"); //! ``` //! //! For compiling whole files (`.prql` to `.sql`), call `prqlc` from //! `build.rs`. See [this example //! project](https://github.com/PRQL/prql/tree/main/prqlc/prqlc/examples/compile-files). //! //! - Compile, format & debug PRQL from command line. //! //! ```sh //! $ cargo install --locked prqlc //! $ prqlc compile query.prql //! ``` //! //! ## Feature flags //! //! The following feature flags are available: //! //! * `cli`: enables the `prqlc` CLI binary. This is enabled by default. When //! consuming this crate from another rust library, it can be disabled. //! * `test-dbs`: enables the `prqlc` in-process test databases as part of the //! crate's tests. This significantly increases compile times so is not //! enabled by default. //! * `test-dbs-external`: enables the `prqlc` external test databases, //! requiring a docker container with the test databases to be running. Check //! out the [integration tests](https://github.com/PRQL/prql/tree/main/prqlc/prqlc/tests/integration/dbs) //! for more details. //! * `serde_yaml`: Enables serialization and deserialization of ASTs to YAML. //! //! ## Large binary sizes //! //! For Linux users, the binary size contributed by this crate will probably be //! quite large (>20MB) by default. That is because it includes a lot of //! debuginfo symbols from our parser. They can be removed by adding the //! following to `Cargo.toml`, reducing the contribution to around 7MB: //! ```toml //! [profile.release.package.prqlc] //! strip = "debuginfo" //! ``` use std::sync::OnceLock; use std::{collections::HashMap, path::PathBuf, str::FromStr}; use anstream::adapter::strip_str; use semver::Version; use serde::{Deserialize, Serialize}; use strum::VariantNames; pub use error_message::{ErrorMessage, ErrorMessages, SourceLocation}; pub use prqlc_parser::error::{Error, ErrorSource, Errors, MessageKind, Reason, WithErrorInfo}; pub use prqlc_parser::lexer::lr; pub use prqlc_parser::parser::pr; pub use prqlc_parser::span::Span; mod codegen; pub mod debug; mod error_message; pub mod ir; pub mod parser; pub mod semantic; pub mod sql; #[cfg(feature = "cli")] pub mod utils; #[cfg(not(feature = "cli"))] pub(crate) mod utils; pub type Result = core::result::Result; /// Get the version of the compiler. This is determined by the first of: /// - An optional environment variable `PRQL_VERSION_OVERRIDE`; primarily useful /// for internal testing. /// - Note that this env var is checked on every call of this function. /// Without checking each read, we found some internal tests were flaky. If /// this caused any perf issues, we could adjust the tests that rely on /// versions to run in a more encapsulated way (for example, use `prqlc` /// binary tests, which we can guarantee won't have anything call this /// before setting up the env var). /// - The version returned by `git describe --tags` /// - The version in the cargo manifest pub fn compiler_version() -> Version { if let Ok(prql_version_override) = std::env::var("PRQL_VERSION_OVERRIDE") { return Version::parse(&prql_version_override).unwrap_or_else(|e| { panic!("Could not parse PRQL version {prql_version_override}\n{e}") }); }; static COMPILER_VERSION: OnceLock = OnceLock::new(); COMPILER_VERSION .get_or_init(|| { if let Ok(prql_version_override) = std::env::var("PRQL_VERSION_OVERRIDE") { return Version::parse(&prql_version_override).unwrap_or_else(|e| { panic!("Could not parse PRQL version {prql_version_override}\n{e}") }); } let git_version = env!("VERGEN_GIT_DESCRIBE"); let cargo_version = env!("CARGO_PKG_VERSION"); Version::parse(git_version) .or_else(|e| { log::info!("Could not parse git version number {git_version}\n{e}"); Version::parse(cargo_version) }) .unwrap_or_else(|e| { panic!("Could not parse prqlc version number {cargo_version}\n{e}") }) }) .clone() } /// Compile a PRQL string into a SQL string. /// /// This is a wrapper for: /// - [prql_to_pl] — Build PL AST from a PRQL string /// - [pl_to_rq] — Finds variable references, validates functions calls, /// determines frames and converts PL to RQ. /// - [rq_to_sql] — Convert RQ AST into an SQL string. /// # Example /// Use the prql compiler to convert a PRQL string to SQLite dialect /// /// ``` /// use prqlc::{compile, Options, Target, sql::Dialect}; /// /// let prql = "from employees | select {name,age}"; /// let opts = Options::default().with_target(Target::Sql(Some(Dialect::SQLite))).with_signature_comment(false).with_format(false); /// let sql = compile(&prql, &opts).unwrap(); /// println!("PRQL: {}\nSQLite: {}", prql, &sql); /// assert_eq!("SELECT name, age FROM employees", sql) /// /// ``` /// See [`sql::Options`](sql/struct.Options.html) and /// [`sql::Dialect`](sql/enum.Dialect.html) for options and supported SQL /// dialects. pub fn compile(prql: &str, options: &Options) -> Result { let sources = SourceTree::from(prql); Ok(&sources) .and_then(parser::parse) .and_then(|ast| { semantic::resolve_and_lower(ast, &[], None) .map_err(|e| e.with_source(ErrorSource::NameResolver).into()) }) .and_then(|rq| { sql::compile(rq, options).map_err(|e| e.with_source(ErrorSource::SQL).into()) }) .map_err(|e| { let error_messages = ErrorMessages::from(e).composed(&sources); match options.display { DisplayOptions::AnsiColor => error_messages, DisplayOptions::Plain => ErrorMessages { inner: error_messages .inner .into_iter() .map(|e| ErrorMessage { display: e.display.map(|s| strip_str(&s).to_string()), ..e }) .collect(), }, } }) } #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Target { /// If `None` is used, dialect is extracted from `target` query header. Sql(Option), } impl Default for Target { fn default() -> Self { Self::Sql(None) } } impl Target { pub fn names() -> Vec { let mut names = vec!["sql.any".to_string()]; let dialects = sql::Dialect::VARIANTS; names.extend(dialects.iter().map(|d| format!("sql.{d}"))); names } } impl FromStr for Target { type Err = Error; fn from_str(s: &str) -> Result { if let Some(dialect) = s.strip_prefix("sql.") { if dialect == "any" { return Ok(Target::Sql(None)); } if let Ok(dialect) = sql::Dialect::from_str(dialect) { return Ok(Target::Sql(Some(dialect))); } } Err(Error::new(Reason::NotFound { name: format!("{s:?}"), namespace: "target".to_string(), })) } } /// Compilation options for SQL backend of the compiler. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Options { /// Pass generated SQL string through a formatter that splits it /// into multiple lines and prettifies indentation and spacing. /// /// Defaults to true. pub format: bool, /// Target and dialect to compile to. pub target: Target, /// Emits the compiler signature as a comment after generated SQL /// /// Defaults to true. pub signature_comment: bool, /// Deprecated: use `display` instead. pub color: bool, /// Whether to use ANSI colors in error messages. This may be extended to /// other formats in the future. /// /// Note that we don't generally recommend threading a `color` option /// through an entire application. Instead, in order of preferences: /// - Use a library such as `anstream` to encapsulate presentation logic and /// automatically disable colors when not connected to a TTY. /// - Set an environment variable such as `CLI_COLOR=0` to disable any /// colors coming back from this library. /// - Strip colors from the output (possibly also with a library such as /// `anstream`). pub display: DisplayOptions, } impl Default for Options { fn default() -> Self { Self { format: true, target: Target::Sql(None), signature_comment: true, color: true, display: DisplayOptions::AnsiColor, } } } impl Options { pub fn with_format(mut self, format: bool) -> Self { self.format = format; self } pub fn no_format(self) -> Self { self.with_format(false) } pub fn with_signature_comment(mut self, signature_comment: bool) -> Self { self.signature_comment = signature_comment; self } pub fn no_signature(self) -> Self { self.with_signature_comment(false) } pub fn with_target(mut self, target: Target) -> Self { self.target = target; self } #[deprecated(note = "`color` is replaced by `display`; see `Options` docs for more details")] pub fn with_color(mut self, color: bool) -> Self { self.color = color; self } pub fn with_display(mut self, display: DisplayOptions) -> Self { self.display = display; self } } #[derive(Debug, Clone, Serialize, Deserialize, strum::EnumString)] #[strum(serialize_all = "snake_case")] #[non_exhaustive] pub enum DisplayOptions { /// Plain text Plain, /// With ANSI colors AnsiColor, } #[doc = include_str!("../README.md")] #[cfg(doctest)] pub struct ReadmeDoctests; /// Lex PRQL source into Lexer Representation. pub fn prql_to_tokens(prql: &str) -> Result { prqlc_parser::lexer::lex_source(prql).map_err(|e| { e.into_iter() .map(|e| e.into()) .collect::>() .into() }) } /// Parse PRQL into a PL AST // TODO: rename this to `prql_to_pl_simple` pub fn prql_to_pl(prql: &str) -> Result { let source_tree = SourceTree::from(prql); prql_to_pl_tree(&source_tree) } /// Parse PRQL into a PL AST pub fn prql_to_pl_tree(prql: &SourceTree) -> Result { parser::parse(prql).map_err(|e| ErrorMessages::from(e).composed(prql)) } /// Perform semantic analysis and convert PL to RQ. // TODO: rename this to `pl_to_rq_simple` pub fn pl_to_rq(pl: pr::ModuleDef) -> Result { semantic::resolve_and_lower(pl, &[], None) .map_err(|e| e.with_source(ErrorSource::NameResolver).into()) } /// Perform semantic analysis and convert PL to RQ. pub fn pl_to_rq_tree( pl: pr::ModuleDef, main_path: &[String], database_module_path: &[String], ) -> Result { semantic::resolve_and_lower(pl, main_path, Some(database_module_path)) .map_err(|e| e.with_source(ErrorSource::NameResolver).into()) } /// Generate SQL from RQ. pub fn rq_to_sql(rq: ir::rq::RelationalQuery, options: &Options) -> Result { sql::compile(rq, options).map_err(|e| e.with_source(ErrorSource::SQL).into()) } /// Generate PRQL code from PL AST pub fn pl_to_prql(pl: &pr::ModuleDef) -> Result { Ok(codegen::WriteSource::write(&pl.stmts, codegen::WriteOpt::default()).unwrap()) } /// JSON serialization and deserialization functions pub mod json { use super::*; /// JSON serialization pub fn from_pl(pl: &pr::ModuleDef) -> Result { serde_json::to_string(pl).map_err(convert_json_err) } /// JSON deserialization pub fn to_pl(json: &str) -> Result { serde_json::from_str(json).map_err(convert_json_err) } /// JSON serialization pub fn from_rq(rq: &ir::rq::RelationalQuery) -> Result { serde_json::to_string(rq).map_err(convert_json_err) } /// JSON deserialization pub fn to_rq(json: &str) -> Result { serde_json::from_str(json).map_err(convert_json_err) } fn convert_json_err(err: serde_json::Error) -> ErrorMessages { ErrorMessages::from(Error::new_simple(err.to_string())) } } /// All paths are relative to the project root. // We use `SourceTree` to represent both a single file (including a "file" piped // from stdin), and a collection of files. (Possibly this could be implemented // as a Trait with a Struct for each type, which would use structure over values // (i.e. `Option` below signifies whether it's a project or not). But // waiting until it's necessary before splitting it out.) #[derive(Debug, Clone, Default, Serialize)] pub struct SourceTree { /// Path to the root of the source tree. pub root: Option, /// Mapping from file paths into into their contents. /// Paths are relative to the root. pub sources: HashMap, /// Index of source ids to paths. Used to keep [error::Span] lean. source_ids: HashMap, } impl SourceTree { pub fn single(path: PathBuf, content: String) -> Self { SourceTree { sources: [(path.clone(), content)].into(), source_ids: [(1, path)].into(), root: None, } } pub fn new(iter: I, root: Option) -> Self where I: IntoIterator, { let mut res = SourceTree { sources: HashMap::new(), source_ids: HashMap::new(), root, }; for (index, (path, content)) in iter.into_iter().enumerate() { res.sources.insert(path.clone(), content); res.source_ids.insert((index + 1) as u16, path); } res } pub fn insert(&mut self, path: PathBuf, content: String) { let last_id = self.source_ids.keys().max().cloned().unwrap_or(0); self.sources.insert(path.clone(), content); self.source_ids.insert(last_id + 1, path); } pub fn get_path(&self, source_id: u16) -> Option<&PathBuf> { self.source_ids.get(&source_id) } } impl From for SourceTree { fn from(source: S) -> Self { SourceTree::single(PathBuf::from(""), source.to_string()) } } /// Debugging and unstable API functions pub mod internal { use super::*; /// Create column-level lineage graph pub fn pl_to_lineage( pl: pr::ModuleDef, ) -> Result { let ast = Some(pl.clone()); let root_module = semantic::resolve(pl).map_err(ErrorMessages::from)?; let (main, _) = root_module.find_main_rel(&[]).unwrap(); let mut fc = semantic::reporting::collect_frames(*main.clone().into_relation_var().unwrap()); fc.ast = ast; Ok(fc) } pub mod json { use super::*; /// JSON serialization of FrameCollector lineage pub fn from_lineage( fc: &semantic::reporting::FrameCollector, ) -> Result { serde_json::to_string(fc).map_err(convert_json_err) } fn convert_json_err(err: serde_json::Error) -> ErrorMessages { ErrorMessages::from(Error::new_simple(err.to_string())) } } } #[cfg(test)] mod tests { use std::str::FromStr; use insta::assert_debug_snapshot; use crate::pr::Ident; use crate::Target; pub fn compile(prql: &str) -> Result { anstream::ColorChoice::Never.write_global(); super::compile(prql, &super::Options::default().no_signature()) } #[test] fn test_starts_with() { // Over-testing, from co-pilot, can remove some of them. let a = Ident::from_path(vec!["a", "b", "c"]); let b = Ident::from_path(vec!["a", "b"]); let c = Ident::from_path(vec!["a", "b", "c", "d"]); let d = Ident::from_path(vec!["a", "b", "d"]); let e = Ident::from_path(vec!["a", "c"]); let f = Ident::from_path(vec!["b", "c"]); assert!(a.starts_with(&b)); assert!(a.starts_with(&a)); assert!(!a.starts_with(&c)); assert!(!a.starts_with(&d)); assert!(!a.starts_with(&e)); assert!(!a.starts_with(&f)); } #[test] fn test_target_from_str() { assert_debug_snapshot!(Target::from_str("sql.postgres"), @r" Ok( Sql( Some( Postgres, ), ), ) "); assert_debug_snapshot!(Target::from_str("sql.poostgres"), @r#" Err( Error { kind: Error, span: None, reason: NotFound { name: "\"sql.poostgres\"", namespace: "target", }, hints: [], code: None, }, ) "#); assert_debug_snapshot!(Target::from_str("postgres"), @r#" Err( Error { kind: Error, span: None, reason: NotFound { name: "\"postgres\"", namespace: "target", }, hints: [], code: None, }, ) "#); } /// Confirm that all target names can be parsed. #[test] fn test_target_names() { let _: Vec<_> = Target::names() .into_iter() .map(|name| Target::from_str(&name)) .collect(); } /// Regression test for #4633: sort inside group should not leak to outer query after join. /// /// Per PRQL spec, `group` resets the order. The `sort` inside a group is for /// row selection (which row to keep), not output ordering. After the group, /// there is no defined order, so it should not appear in the outer query. #[test] fn test_sort_not_propagated_after_join() { use insta::assert_snapshot; // DISTINCT ON (postgres) uses the sort for row selection within the CTE. // This internal sorting must not leak to the outer query after a join. assert_snapshot!( super::compile( r#" prql target:sql.postgres from tracks group media_type_id ( sort name take 1 ) join media_types (== media_type_id) select { tracks.track_id, media_types.name } "#, &super::Options::default().no_signature() ).unwrap(), @" WITH table_0 AS ( SELECT DISTINCT ON (media_type_id) track_id, media_type_id, name FROM tracks ORDER BY media_type_id, name ) SELECT table_0.track_id, media_types.name FROM table_0 INNER JOIN media_types ON table_0.media_type_id = media_types.media_type_id " ); } /// Verify that explicit sorts after group are preserved past joins. /// /// Per PRQL spec, `sort` introduces a new order. When the user explicitly /// sorts AFTER a group, that becomes the new output order and should /// propagate through subsequent transforms including joins. #[test] fn test_explicit_sort_after_distinct_on_preserved() { use insta::assert_snapshot; // Explicit `sort media_type_id` after the group introduces a new order. // This user-requested ordering should propagate past the join. assert_snapshot!( super::compile( r#" prql target:sql.postgres from tracks group media_type_id ( sort name take 1 ) sort media_type_id join media_types (== media_type_id) select { tracks.track_id, media_types.name } "#, &super::Options::default().no_signature() ).unwrap(), @" WITH table_0 AS ( SELECT DISTINCT ON (media_type_id) track_id, media_type_id, name FROM tracks ORDER BY media_type_id, name ), table_1 AS ( SELECT table_0.track_id, media_types.name, table_0.media_type_id FROM table_0 INNER JOIN media_types ON table_0.media_type_id = media_types.media_type_id ) SELECT track_id, name FROM table_1 ORDER BY media_type_id " ); } } ================================================ FILE: prqlc/prqlc/src/main.rs ================================================ #[cfg(all(not(target_family = "wasm"), feature = "cli"))] mod cli; #[cfg(all(not(target_family = "wasm"), feature = "cli"))] fn main() -> color_eyre::eyre::Result<()> { // Use a larger stack size (8 MiB) to avoid stack overflows on Windows, // where the default stack is only 1 MiB. const STACK_SIZE: usize = 8 * 1024 * 1024; let thread = std::thread::Builder::new() .stack_size(STACK_SIZE) .spawn(cli::main) .expect("failed to spawn main thread"); thread.join().expect("main thread panicked")?; Ok(()) } #[cfg(any(target_family = "wasm", not(feature = "cli")))] fn main() { panic!("Crate is not built with the `cli` feature enabled, or was built for a wasm target."); } ================================================ FILE: prqlc/prqlc/src/parser.rs ================================================ use std::path::PathBuf; use std::{collections::HashMap, path::Path}; use itertools::Itertools; use crate::debug; use crate::lr; use crate::pr; use crate::{Error, Errors, Result, SourceTree, WithErrorInfo}; pub fn parse(file_tree: &SourceTree) -> Result { // register a new stage of the compiler // (here should register lexer stage first, but that all happens in a single call to prqlc_parser) debug::log_entry(|| debug::DebugEntryKind::ReprPrql(file_tree.clone())); debug::log_stage(debug::Stage::Parsing); let source_files = linearize_tree(file_tree)?; // reverse the id->file_path map let ids: HashMap<_, _> = file_tree .source_ids .iter() .map(|(a, b)| (b.as_path(), a)) .collect(); // init the root module def let mut root = pr::ModuleDef { name: "Project".to_string(), stmts: Vec::new(), }; // parse and insert into the root let mut errors = Vec::new(); for source_file in source_files { let id = ids .get(&source_file.file_path) .map(|x| **x) .expect("source tree has malformed ids"); match parse_source(source_file.content, id) { Ok(stmts) => { insert_stmts_at_path(&mut root, source_file.module_path, stmts); } Err(errs) => errors.extend(errs), } } if errors.is_empty() { debug::log_entry(|| debug::DebugEntryKind::ReprPr(root.clone())); Ok(root) } else { Err(Errors(errors)) } } /// Build PR AST from a PRQL query string. // We have this function in `prqlc` rather than in `prqlc-parser` crate since // our logging is in `prqlc` and we want to log the LR. (We could split the logging // out into a separate crate, but it has dependencies on `prqlc` internals and // would be an effort) pub(crate) fn parse_source(source: &str, source_id: u16) -> Result, Vec> { let (tokens, mut errors) = prqlc_parser::lexer::lex_source_recovery(source, source_id); let ast = if let Some(tokens) = tokens { debug::log_entry(|| debug::DebugEntryKind::ReprLr(lr::Tokens(tokens.clone()))); let (ast, parse_errors) = prqlc_parser::parser::parse_lr_to_pr(source_id, tokens); errors.extend(parse_errors); ast } else { None }; if errors.is_empty() { Ok(ast.unwrap_or_default()) } else { Err(errors) } } struct SourceFile<'a> { file_path: &'a Path, module_path: Vec, content: &'a str, } fn linearize_tree(tree: &SourceTree) -> Result>> { // find root let root_path; if tree.sources.len() == 1 { // if there is only one file, use that as the root root_path = tree.sources.keys().next().unwrap(); } else if let Some(root) = tree.sources.get_key_value(&PathBuf::from("")) { // if there is an empty path, that's the root root_path = root.0; } else if let Some(root) = tree.sources.keys().find(path_starts_with_uppercase) { root_path = root; } else { if tree.sources.is_empty() { // TODO: should we allow non `.prql` files? We could require `.prql` // for modules but then allow any file if a single file is passed // (python allows this, for example) return Err(Error::new_simple( "No `.prql` files found in the source tree", )); } let file_names = tree .sources .keys() .map(|p| format!(" - {}", p.to_str().unwrap_or_default())) .sorted() .join("\n"); return Err(Error::new_simple(format!( "Cannot find the root module within the following files:\n{file_names}" )) .push_hint("add a file that starts with uppercase letter to the root directory") .with_code("E0002")); } let mut sources: Vec<_> = Vec::with_capacity(tree.sources.len()); // prepare paths for (path, source) in &tree.sources { if path == root_path { continue; } let module_path = os_path_to_prql_path(path)?; sources.push(SourceFile { file_path: path, module_path, content: source, }); } // sort to make this deterministic sources.sort_by(|a, b| a.module_path.cmp(&b.module_path)); // add root let root_content = tree.sources.get(root_path).unwrap(); sources.push(SourceFile { file_path: root_path, module_path: Vec::new(), content: root_content, }); Ok(sources) } fn insert_stmts_at_path(module: &mut pr::ModuleDef, mut path: Vec, stmts: Vec) { if path.is_empty() { module.stmts.extend(stmts); return; } let step = path.remove(0); // find submodule def let submodule = module.stmts.iter_mut().find(|x| is_mod_def_for(x, &step)); let submodule = if let Some(sm) = submodule { sm } else { // insert new module def let new_stmt = pr::Stmt::new(pr::StmtKind::ModuleDef(pr::ModuleDef { name: step, stmts: Vec::new(), })); module.stmts.push(new_stmt); module.stmts.last_mut().unwrap() }; let submodule = submodule.kind.as_module_def_mut().unwrap(); insert_stmts_at_path(submodule, path, stmts); } pub(crate) fn is_mod_def_for(stmt: &pr::Stmt, name: &str) -> bool { stmt.kind.as_module_def().is_some_and(|x| x.name == name) } fn path_starts_with_uppercase(p: &&PathBuf) -> bool { p.components() .next() .and_then(|x| x.as_os_str().to_str()) .and_then(|x| x.chars().next()) .is_some_and(|x| x.is_uppercase()) } pub fn os_path_to_prql_path(path: &Path) -> Result> { // remove file format extension let path = path.with_extension(""); // split by / path.components() .map(|x| { x.as_os_str() .to_str() .map(str::to_string) .ok_or_else(|| Error::new_simple(format!("Invalid file path: {path:?}"))) }) .try_collect() } ================================================ FILE: prqlc/prqlc/src/semantic/ast_expand.rs ================================================ use std::collections::HashMap; use itertools::Itertools; use prqlc_parser::generic; use crate::ir::decl; use crate::ir::pl::{self, new_binop}; use crate::pr; use crate::semantic::{NS_THAT, NS_THIS}; use crate::{Error, Result}; /// An AST pass that maps AST to PL. pub fn expand_expr(expr: pr::Expr) -> Result { let kind = match expr.kind { pr::ExprKind::Ident(v) => pl::ExprKind::Ident(v), pr::ExprKind::Literal(v) => pl::ExprKind::Literal(v), pr::ExprKind::Pipeline(v) => { let mut e = desugar_pipeline(v)?; e.alias = expr.alias.or(e.alias); return Ok(e); } pr::ExprKind::Tuple(v) => pl::ExprKind::Tuple(expand_exprs(v)?), pr::ExprKind::Array(v) => pl::ExprKind::Array(expand_exprs(v)?), pr::ExprKind::Range(v) => expands_range(v)?, pr::ExprKind::Unary(unary) => expand_unary(unary)?, pr::ExprKind::Binary(binary) => expand_binary(binary)?, pr::ExprKind::FuncCall(v) => pl::ExprKind::FuncCall(pl::FuncCall { name: expand_expr_box(v.name)?, args: expand_exprs(v.args)?, named_args: v .named_args .into_iter() .map(|(k, v)| -> Result<_> { Ok((k, expand_expr(v)?)) }) .try_collect()?, }), pr::ExprKind::Func(v) => pl::ExprKind::Func( pl::Func { return_ty: v.return_ty, body: expand_expr_box(v.body)?, params: expand_func_params(v.params)?, named_params: expand_func_params(v.named_params)?, name_hint: None, args: Vec::new(), env: HashMap::new(), } .into(), ), pr::ExprKind::SString(v) => pl::ExprKind::SString( v.into_iter() .map(|v| v.try_map(expand_expr)) .try_collect()?, ), pr::ExprKind::FString(v) => pl::ExprKind::FString( v.into_iter() .map(|v| v.try_map(expand_expr)) .try_collect()?, ), pr::ExprKind::Case(v) => pl::ExprKind::Case( v.into_iter() .map(|case| -> Result<_> { Ok(pl::SwitchCase { condition: expand_expr_box(case.condition)?, value: expand_expr_box(case.value)?, }) }) .try_collect()?, ), pr::ExprKind::Param(v) => pl::ExprKind::Param(v), pr::ExprKind::Internal(v) => pl::ExprKind::Internal(v), }; Ok(pl::Expr { kind, span: expr.span, alias: expr.alias, id: None, target_id: None, ty: None, lineage: None, needs_window: false, flatten: false, }) } /// De-sugars range `a..b` into `{start=a, end=b}`. Open bounds are mapped into `null`. fn expands_range(v: generic::Range>) -> Result { let mut start = v .start .map(|e| expand_expr(*e)) .transpose()? .unwrap_or_else(|| pl::Expr::new(pr::Literal::Null)); start.alias = Some("start".into()); let mut end = v .end .map(|e| expand_expr(*e)) .transpose()? .unwrap_or_else(|| pl::Expr::new(pr::Literal::Null)); end.alias = Some("end".into()); Ok(pl::ExprKind::Tuple(vec![start, end])) } fn expand_exprs(exprs: Vec) -> Result> { exprs.into_iter().map(expand_expr).collect() } #[allow(clippy::boxed_local)] fn expand_expr_box(expr: Box) -> Result> { Ok(Box::new(expand_expr(*expr)?)) } fn desugar_pipeline(mut pipeline: pr::Pipeline) -> Result { let value = pipeline.exprs.remove(0); let mut value = expand_expr(value)?; for expr in pipeline.exprs { let expr = expand_expr(expr)?; let span = expr.span; value = pl::Expr::new(pl::ExprKind::FuncCall(pl::FuncCall::new_simple( expr, vec![value], ))); value.span = span; } Ok(value) } /// Desugar unary operators into function calls. fn expand_unary(pr::UnaryExpr { op, expr }: pr::UnaryExpr) -> Result { use pr::UnOp::*; let expr = expand_expr(*expr)?; let func_name = match op { Neg => ["std", "neg"], Not => ["std", "not"], Add => return Ok(expr.kind), EqSelf => { let pl::ExprKind::Ident(ident) = expr.kind else { return Err(Error::new_simple( "self-equality operator requires a column name", )); }; if !ident.path.is_empty() { return Err(Error::new_simple( "self-equality operator does not support namespace prefix", )); } let left = pl::Expr { span: expr.span, ..pl::Expr::new(pr::Ident { path: vec![NS_THIS.to_string()], name: ident.name.clone(), }) }; let right = pl::Expr { span: expr.span, ..pl::Expr::new(pr::Ident { path: vec![NS_THAT.to_string()], name: ident.name, }) }; return Ok(new_binop(left, &["std", "eq"], right).kind); } }; Ok(pl::ExprKind::FuncCall(pl::FuncCall::new_simple( pl::Expr::new(pr::Ident::from_path(func_name.to_vec())), vec![expr], ))) } /// Desugar binary operators into function calls. fn expand_binary(pr::BinaryExpr { op, left, right }: pr::BinaryExpr) -> Result { let left = expand_expr(*left)?; let right = expand_expr(*right)?; let func_name: Vec<&str> = match op { pr::BinOp::Mul => vec!["std", "mul"], pr::BinOp::DivInt => vec!["std", "div_i"], pr::BinOp::DivFloat => vec!["std", "div_f"], pr::BinOp::Mod => vec!["std", "mod"], pr::BinOp::Pow => vec!["std", "math", "pow"], pr::BinOp::Add => vec!["std", "add"], pr::BinOp::Sub => vec!["std", "sub"], pr::BinOp::Eq => vec!["std", "eq"], pr::BinOp::Ne => vec!["std", "ne"], pr::BinOp::Gt => vec!["std", "gt"], pr::BinOp::Lt => vec!["std", "lt"], pr::BinOp::Gte => vec!["std", "gte"], pr::BinOp::Lte => vec!["std", "lte"], pr::BinOp::RegexSearch => vec!["std", "regex_search"], pr::BinOp::And => vec!["std", "and"], pr::BinOp::Or => vec!["std", "or"], pr::BinOp::Coalesce => vec!["std", "coalesce"], }; // For the power operator, we need to reverse the order, since `math.pow a // b` is equivalent to `b ** a`. (but for example `sub a b` is equivalent to // `a - b`). // // (I think this is the most globally consistent approach, since final // arguments should be the "data", which in the case of `pow` would be the // base; but it's not perfect, we could change it...) let (left, right) = match op { pr::BinOp::Pow => (right, left), _ => (left, right), }; Ok(new_binop(left, &func_name, right).kind) } fn expand_func_param(value: pr::FuncParam) -> Result { Ok(pl::FuncParam { name: value.name, ty: value.ty, default_value: value.default_value.map(expand_expr_box).transpose()?, }) } fn expand_func_params(value: Vec) -> Result> { value.into_iter().map(expand_func_param).collect() } fn expand_stmt(value: pr::Stmt) -> Result { Ok(pl::Stmt { id: None, kind: expand_stmt_kind(value.kind)?, span: value.span, annotations: value .annotations .into_iter() .map(expand_annotation) .try_collect()?, }) } pub fn expand_module_def(v: pr::ModuleDef) -> Result { Ok(pl::ModuleDef { name: v.name, stmts: expand_stmts(v.stmts)?, }) } fn expand_stmts(value: Vec) -> Result> { value.into_iter().map(expand_stmt).collect() } fn expand_stmt_kind(value: pr::StmtKind) -> Result { Ok(match value { pr::StmtKind::QueryDef(v) => pl::StmtKind::QueryDef(v), pr::StmtKind::VarDef(v) => pl::StmtKind::VarDef(pl::VarDef { name: v.name, value: v.value.map(expand_expr_box).transpose()?, ty: v.ty, }), pr::StmtKind::TypeDef(v) => pl::StmtKind::TypeDef(pl::TypeDef { name: v.name, value: v.value, }), pr::StmtKind::ModuleDef(v) => pl::StmtKind::ModuleDef(expand_module_def(v)?), pr::StmtKind::ImportDef(v) => pl::StmtKind::ImportDef(pl::ImportDef { alias: v.alias, name: v.name, }), }) } fn expand_annotation(value: pr::Annotation) -> Result { Ok(pl::Annotation { expr: expand_expr_box(value.expr)?, }) } /// An AST pass that tries to revert the mapping from AST to PL pub fn restrict_expr(expr: pl::Expr) -> pr::Expr { pr::Expr { kind: restrict_expr_kind(expr.kind), span: expr.span, alias: expr.alias, doc_comment: None, } } #[allow(clippy::boxed_local)] fn restrict_expr_box(expr: Box) -> Box { Box::new(restrict_expr(*expr)) } fn restrict_exprs(exprs: Vec) -> Vec { exprs.into_iter().map(restrict_expr).collect() } fn restrict_expr_kind(value: pl::ExprKind) -> pr::ExprKind { match value { pl::ExprKind::Ident(v) => pr::ExprKind::Ident(v), pl::ExprKind::Literal(v) => pr::ExprKind::Literal(v), pl::ExprKind::Tuple(v) => pr::ExprKind::Tuple(restrict_exprs(v)), pl::ExprKind::Array(v) => pr::ExprKind::Array(restrict_exprs(v)), pl::ExprKind::FuncCall(v) => pr::ExprKind::FuncCall(pr::FuncCall { name: restrict_expr_box(v.name), args: restrict_exprs(v.args), named_args: v .named_args .into_iter() .map(|(k, v)| (k, restrict_expr(v))) .collect(), }), pl::ExprKind::Func(v) => { let func = pr::ExprKind::Func( pr::Func { return_ty: v.return_ty, body: restrict_expr_box(v.body), params: restrict_func_params(v.params), named_params: restrict_func_params(v.named_params), } .into(), ); if v.args.is_empty() { func } else { pr::ExprKind::FuncCall(pr::FuncCall { name: Box::new(pr::Expr::new(func)), args: restrict_exprs(v.args), named_args: Default::default(), }) } } pl::ExprKind::SString(v) => { pr::ExprKind::SString(v.into_iter().map(|v| v.map(restrict_expr)).collect()) } pl::ExprKind::FString(v) => { pr::ExprKind::FString(v.into_iter().map(|v| v.map(restrict_expr)).collect()) } pl::ExprKind::Case(v) => pr::ExprKind::Case( v.into_iter() .map(|case| pr::SwitchCase { condition: restrict_expr_box(case.condition), value: restrict_expr_box(case.value), }) .collect(), ), pl::ExprKind::Param(v) => pr::ExprKind::Param(v), pl::ExprKind::Internal(v) => pr::ExprKind::Internal(v), // TODO: these are not correct, they are producing invalid PRQL pl::ExprKind::All { within, .. } => restrict_expr(*within).kind, pl::ExprKind::TransformCall(tc) => pr::ExprKind::Ident(pr::Ident::from_name(format!( "({} ...)", tc.kind.as_ref().as_ref() ))), pl::ExprKind::RqOperator { name, .. } => { pr::ExprKind::Ident(pr::Ident::from_name(format!("({name} ...)"))) } } } fn restrict_func_params(value: Vec) -> Vec { value.into_iter().map(restrict_func_param).collect() } fn restrict_func_param(value: pl::FuncParam) -> pr::FuncParam { pr::FuncParam { name: value.name, ty: value.ty, default_value: value.default_value.map(restrict_expr_box), } } /// Restricts a tuple of form `{start=a, end=b}` into a range `a..b`. pub fn try_restrict_range(expr: pl::Expr) -> Result<(pl::Expr, pl::Expr), pl::Expr> { let pl::ExprKind::Tuple(fields) = expr.kind else { return Err(expr); }; if fields.len() != 2 || fields[0].alias.as_deref() != Some("start") || fields[1].alias.as_deref() != Some("end") { return Err(pl::Expr { kind: pl::ExprKind::Tuple(fields), ..expr }); } let [start, end]: [pl::Expr; 2] = fields.try_into().unwrap(); Ok((start, end)) } /// Returns None if the Expr is a null literal and Some(expr) otherwise. pub fn restrict_null_literal(expr: pl::Expr) -> Option { if let pl::ExprKind::Literal(pr::Literal::Null) = expr.kind { None } else { Some(expr) } } pub fn restrict_module_def(def: pl::ModuleDef) -> pr::ModuleDef { pr::ModuleDef { name: def.name, stmts: restrict_stmts(def.stmts), } } fn restrict_stmts(stmts: Vec) -> Vec { stmts.into_iter().map(restrict_stmt).collect() } fn restrict_stmt(stmt: pl::Stmt) -> pr::Stmt { let kind = match stmt.kind { pl::StmtKind::QueryDef(def) => pr::StmtKind::QueryDef(def), pl::StmtKind::VarDef(def) => pr::StmtKind::VarDef(pr::VarDef { kind: pr::VarDefKind::Let, name: def.name, value: def.value.map(restrict_expr_box), ty: def.ty, }), pl::StmtKind::TypeDef(def) => pr::StmtKind::TypeDef(pr::TypeDef { name: def.name, value: def.value, }), pl::StmtKind::ModuleDef(def) => pr::StmtKind::ModuleDef(restrict_module_def(def)), pl::StmtKind::ImportDef(def) => pr::StmtKind::ImportDef(pr::ImportDef { name: def.name, alias: def.alias, }), }; pr::Stmt { kind, span: stmt.span, annotations: stmt .annotations .into_iter() .map(restrict_annotation) .collect(), doc_comment: None, } } pub fn restrict_annotation(value: pl::Annotation) -> pr::Annotation { pr::Annotation { expr: restrict_expr_box(value.expr), } } pub fn restrict_module(value: decl::Module) -> pr::ModuleDef { let mut stmts = Vec::new(); for (name, decl) in value.names.into_iter().sorted_by_key(|x| x.0.clone()) { stmts.extend(restrict_decl(name, decl)) } pr::ModuleDef { name: "".to_string(), stmts, } } fn restrict_decl(name: String, value: decl::Decl) -> Option { let kind = match value.kind { decl::DeclKind::Module(module) => pr::StmtKind::ModuleDef(pr::ModuleDef { name, stmts: restrict_module(module).stmts, }), decl::DeclKind::LayeredModules(mut stack) => { let module = stack.pop()?; pr::StmtKind::ModuleDef(pr::ModuleDef { name, stmts: restrict_module(module).stmts, }) } decl::DeclKind::TableDecl(table_decl) => pr::StmtKind::VarDef(pr::VarDef { kind: pr::VarDefKind::Let, name: name.clone(), value: Some(Box::new(match table_decl.expr { decl::TableExpr::RelationVar(expr) => restrict_expr(*expr), decl::TableExpr::LocalTable => { pr::Expr::new(pr::ExprKind::Internal("local_table".into())) } decl::TableExpr::None => { pr::Expr::new(pr::ExprKind::Internal("literal_tracker".to_string())) } decl::TableExpr::Param(id) => pr::Expr::new(pr::ExprKind::Param(id)), })), ty: table_decl.ty, }), decl::DeclKind::InstanceOf(ident, _) => { new_internal_stmt(name, format!("instance_of.{ident}")) } decl::DeclKind::Column(id) => new_internal_stmt(name, format!("column.{id}")), decl::DeclKind::Infer(_) => new_internal_stmt(name, "infer".to_string()), decl::DeclKind::Expr(mut expr) => pr::StmtKind::VarDef(pr::VarDef { kind: pr::VarDefKind::Let, name, ty: expr.ty.take(), value: Some(restrict_expr_box(expr)), }), decl::DeclKind::Ty(ty) => pr::StmtKind::TypeDef(pr::TypeDef { name, value: ty }), decl::DeclKind::QueryDef(query_def) => pr::StmtKind::QueryDef(Box::new(query_def)), decl::DeclKind::Import(ident) => pr::StmtKind::ImportDef(pr::ImportDef { alias: Some(name), name: ident, }), }; Some(pr::Stmt::new(kind)) } fn new_internal_stmt(name: String, internal: String) -> pr::StmtKind { pr::StmtKind::VarDef(pr::VarDef { kind: pr::VarDefKind::Let, name, value: Some(Box::new(pr::Expr::new(pr::ExprKind::Internal(internal)))), ty: None, }) } ================================================ FILE: prqlc/prqlc/src/semantic/lowering.rs ================================================ use std::collections::hash_map::RandomState; use std::collections::{BTreeSet, HashMap, HashSet}; use std::iter::zip; use enum_as_inner::EnumAsInner; use itertools::Itertools; use prqlc_parser::generic::{InterpolateItem, Range, SwitchCase}; use prqlc_parser::lexer::lr::Literal; use semver::{Prerelease, Version}; use crate::compiler_version; use crate::ir::decl::{self, DeclKind, Module, RootModule, TableExpr}; use crate::ir::generic::{ColumnSort, WindowFrame}; use crate::ir::pl::TableExternRef::LocalTable; use crate::ir::pl::{self, Ident, Lineage, LineageColumn, PlFold, QueryDef}; use crate::ir::rq::{ self, CId, RelationColumn, RelationLiteral, RelationalQuery, TId, TableDecl, Transform, }; use crate::pr::TyTupleField; use crate::semantic::write_pl; use crate::utils::{toposort, IdGenerator}; use crate::{Error, Reason, Result, Span, WithErrorInfo}; /// Convert a resolved expression at path `main_path` relative to `root_mod` /// into RQ and make sure that: /// - transforms are not nested, /// - transforms have correct partition, window and sort set, /// - make sure there are no unresolved expressions. /// /// All table references must reside within module at `database_module_path`. /// They are compiled to table identifiers, using their path relative to the database module. /// For example, with `database_module_path=my_database`: /// - `my_database.my_table` will compile to `"my_table"`, /// - `my_database.my_schema.my_table` will compile to `"my_schema.my_table"`, /// - `my_table` will error out saying that this table does not reside in current database. pub fn lower_to_ir( root_mod: RootModule, main_path: &[String], database_module_path: &[String], ) -> Result<(RelationalQuery, RootModule)> { // find main log::debug!("lookup for main pipeline in {main_path:?}"); let (_, main_ident) = root_mod.find_main_rel(main_path).map_err(|(_hint, span)| { // Provide better error messages based on what's in the module let user_declared_names: Vec<_> = root_mod .module .names .keys() .filter(|name| *name != "std" && *name != "default_db") .collect(); let error = if user_declared_names.is_empty() { // No user declarations - empty query or only comments // Message is self-explanatory, no hint needed Error::new_simple("No PRQL query entered").with_code("E0001") } else { // Has declarations but no pipeline starting with 'from' Error::new_simple("PRQL queries must begin with 'from'") .with_code("E0001") .push_hint("A query must start with a 'from' statement to define the main pipeline") }; error.with_span(span) })?; // find & validate query def let def = root_mod.find_query_def(&main_ident); let def = def.cloned().unwrap_or_default(); validate_query_def(&def)?; // find all tables in the root module let tables = TableExtractor::extract(&root_mod.module); // prune and toposort let tables = toposort_tables(tables, &main_ident); // lower tables let mut l = Lowerer::new(root_mod, database_module_path); let mut main_relation = None; for (fq_ident, (table, declared_at)) in tables { let is_main = fq_ident == main_ident; l.lower_table_decl(table, fq_ident) .map_err(with_span_if_not_exists(|| get_span_of_id(&l, declared_at)))?; if is_main { let main_table = l.table_buffer.pop().unwrap(); main_relation = Some(main_table.relation); } } let query = RelationalQuery { def, tables: l.table_buffer, relation: main_relation.unwrap(), }; Ok((query, l.root_mod)) } fn extern_ref_to_relation( mut columns: Vec, fq_ident: &Ident, database_module_path: &[String], ) -> Result<(rq::Relation, Option), Error> { let extern_name = if fq_ident.starts_with_path(database_module_path) { let relative_to_database: Vec<&String> = fq_ident.iter().skip(database_module_path.len()).collect(); if relative_to_database.is_empty() { None } else { Some(Ident::from_path(relative_to_database)) } } else { None }; let Some(extern_name) = extern_name else { let database_module = Ident::from_path(database_module_path.to_vec()); return Err(Error::new_simple("this table is not in the current database") .push_hint(format!("If this is a table in the current database, move its declaration into module {database_module}"))); }; // put wildcards last columns.sort_by_key(|a| matches!(a, TyTupleField::Wildcard(_))); let relation = rq::Relation { kind: rq::RelationKind::ExternRef(LocalTable(extern_name)), columns: tuple_fields_to_relation_columns(columns), }; Ok((relation, None)) } fn tuple_fields_to_relation_columns(columns: Vec) -> Vec { columns .into_iter() .map(|field| match field { TyTupleField::Single(name, _) => RelationColumn::Single(name), TyTupleField::Wildcard(_) => RelationColumn::Wildcard, }) .collect_vec() } fn validate_query_def(query_def: &QueryDef) -> Result<()> { if let Some(requirement) = &query_def.version { let current_version = compiler_version(); // We need to remove the pre-release part of the version, because // otherwise those will fail the match. let clean_version = Version { pre: Prerelease::EMPTY, ..current_version.clone() }; if !requirement.matches(&clean_version) { return Err(Error::new_simple(format!( "This query requires version {requirement} of PRQL that is not supported by prqlc version {clean_version} (shortened from {current_version}). Please upgrade the compiler." ))); } } Ok(()) } #[derive(Debug)] struct Lowerer { cid: IdGenerator, tid: IdGenerator, root_mod: RootModule, database_module_path: Vec, /// describes what has certain id has been lowered to node_mapping: HashMap, /// mapping from [Ident] of [crate::pr::TableDef] into [TId]s table_mapping: HashMap, // current window for any new column defs window: Option, /// A buffer to be added into current pipeline pipeline: Vec, /// A buffer to be added into query tables table_buffer: Vec, } #[derive(Clone, EnumAsInner, Debug)] enum LoweredTarget { /// Lowered node was a computed expression. Compute(CId), /// Lowered node was a pipeline input. /// Contains mapping from column names to CIds, along with order in frame. Input(HashMap), } impl Lowerer { fn new(root_mod: RootModule, database_module_path: &[String]) -> Self { Lowerer { root_mod, database_module_path: database_module_path.to_vec(), cid: IdGenerator::new(), tid: IdGenerator::new(), node_mapping: HashMap::new(), table_mapping: HashMap::new(), window: None, pipeline: Vec::new(), table_buffer: Vec::new(), } } fn lower_table_decl(&mut self, table: decl::TableDecl, fq_ident: Ident) -> Result<()> { let decl::TableDecl { ty, expr } = table; // TODO: can this panic? let columns = ty.unwrap().into_relation().unwrap(); let (relation, name) = match expr { TableExpr::RelationVar(expr) => { // a CTE (self.lower_relation(*expr)?, Some(fq_ident.name.clone())) } TableExpr::LocalTable => { extern_ref_to_relation(columns, &fq_ident, &self.database_module_path)? } TableExpr::Param(_) => unreachable!(), TableExpr::None => return Ok(()), }; let id = *self .table_mapping .entry(fq_ident) .or_insert_with(|| self.tid.gen()); log::debug!("lowering table {name:?}, columns = {:?}", relation.columns); let table = TableDecl { id, name, relation }; self.table_buffer.push(table); Ok(()) } /// Lower an expression into a instance of a table in the query fn lower_table_ref(&mut self, expr: pl::Expr) -> Result { let mut expr = expr; if expr.lineage.is_none() { // make sure that type of this expr has been inferred to be a table expr.lineage = Some(Lineage::default()); } Ok(match expr.kind { pl::ExprKind::Ident(fq_table_name) => { // ident that refer to table: create an instance of the table let id = expr.id.unwrap(); let tid = *self .table_mapping .get(&fq_table_name) .ok_or_else(|| Error::new_bug(4474))?; log::debug!("lowering an instance of table {fq_table_name} (id={id})..."); let input_name = expr .lineage .as_ref() .and_then(|f| f.inputs.first()) .map(|i| i.name.clone()); let name = input_name.or(Some(fq_table_name.name)); self.create_a_table_instance(id, name, tid) } pl::ExprKind::TransformCall(_) => { // pipeline that has to be pulled out into a table let id = expr.id.unwrap(); // create a new table let tid = self.tid.gen(); let relation = self.lower_relation(expr)?; let last_transform = &relation.kind.as_pipeline().unwrap().last().unwrap(); let cids = last_transform.as_select().unwrap().clone(); log::debug!("lowering inline table, columns = {:?}", relation.columns); self.table_buffer.push(TableDecl { id: tid, name: None, relation, }); // return an instance of this new table let table_ref = self.create_a_table_instance(id, None, tid); let redirects = zip(cids, table_ref.columns.iter().map(|(_, c)| *c)).collect(); self.redirect_mappings(redirects); table_ref } pl::ExprKind::SString(items) => { let id = expr.id.unwrap(); // create a new table let tid = self.tid.gen(); // pull columns from the table decl let frame = expr.lineage.as_ref().unwrap(); let input = frame.inputs.first().unwrap(); let table_decl = self.root_mod.module.get(&input.table).unwrap(); let table_decl = table_decl.kind.as_table_decl().unwrap(); let ty = table_decl.ty.as_ref(); // TODO: can this panic? let columns = ty.unwrap().as_relation().unwrap().clone(); log::debug!("lowering sstring table, columns = {columns:?}"); // lower the expr let items = self.lower_interpolations(items)?; let columns = tuple_fields_to_relation_columns(columns); let columns = try_extract_sql_columns(columns, &items); let relation = rq::Relation { kind: rq::RelationKind::SString(items), columns, }; self.table_buffer.push(TableDecl { id: tid, name: None, relation, }); // return an instance of this new table self.create_a_table_instance(id, None, tid) } pl::ExprKind::RqOperator { name, args } => { let id = expr.id.unwrap(); // create a new table let tid = self.tid.gen(); // pull columns from the table decl let frame = expr.lineage.as_ref().unwrap(); let input = frame.inputs.first().unwrap(); let table_decl = self.root_mod.module.get(&input.table).unwrap(); let table_decl = table_decl.kind.as_table_decl().unwrap(); let ty = table_decl.ty.as_ref(); // TODO: can this panic? let columns = ty.unwrap().as_relation().unwrap().clone(); log::debug!("lowering function table, columns = {columns:?}"); // lower the expr let args = args.into_iter().map(|a| self.lower_expr(a)).try_collect()?; let relation = rq::Relation { kind: rq::RelationKind::BuiltInFunction { name, args }, columns: tuple_fields_to_relation_columns(columns), }; self.table_buffer.push(TableDecl { id: tid, name: None, relation, }); // return an instance of this new table self.create_a_table_instance(id, None, tid) } pl::ExprKind::Array(elements) => { let id = expr.id.unwrap(); // create a new table let tid = self.tid.gen(); // pull columns from the table decl let lineage = expr.lineage.as_ref().unwrap(); let columns: Vec<_> = (lineage.columns.iter()) .map(|c| match c { LineageColumn::Single { name, .. } => Ok(RelationColumn::Single( name.as_ref().map(|i| i.name.clone()), )), LineageColumn::All { .. } => Err(Error::new_bug(4317)), }) .try_collect()?; let lit = RelationLiteral { columns: columns .iter() .map(|c| c.as_single().unwrap().clone().unwrap()) .collect_vec(), rows: elements .into_iter() .map(|row| { row.kind .into_tuple() .unwrap() .into_iter() .map(|element| { element.try_cast( |x| x.into_literal(), Some("relation literal"), "literals", ) }) .try_collect() }) .try_collect()?, }; log::debug!("lowering literal relation table, columns = {columns:?}"); let relation = rq::Relation { kind: rq::RelationKind::Literal(lit), columns, }; self.table_buffer.push(TableDecl { id: tid, name: None, relation, }); // return an instance of this new table self.create_a_table_instance(id, None, tid) } _ => { let found_str = write_pl(expr.clone()); let mut error = Error::new(Reason::Expected { who: None, expected: "a pipeline that resolves to a table".to_string(), found: format!("`{}`", found_str), }); // Provide better hints for common mistakes if found_str.starts_with("internal std.sub") { // This is likely a negative number or expression that should be wrapped in parentheses error = error.push_hint( "wrap negative numbers in parentheses, e.g. `sort (-column_name)`", ); } else { error = error.push_hint("`from` statement might be missing?"); } return Err(error.with_span(expr.span)); } }) } fn redirect_mappings(&mut self, redirects: HashMap) { for target in self.node_mapping.values_mut() { match target { LoweredTarget::Compute(cid) => { if let Some(new) = redirects.get(cid) { *cid = *new; } } LoweredTarget::Input(mapping) => { for (cid, _) in mapping.values_mut() { if let Some(new) = redirects.get(cid) { *cid = *new; } } } } } } fn create_a_table_instance( &mut self, id: usize, name: Option, tid: TId, ) -> rq::TableRef { // create instance columns from table columns let table = self.table_buffer.iter().find(|t| t.id == tid).unwrap(); let columns = (table.relation.columns.iter()) .cloned() .unique() .map(|col| (col, self.cid.gen())) .collect_vec(); log::debug!("... columns = {columns:?}"); let input_cids: HashMap<_, _> = columns .iter() .cloned() .enumerate() .map(|(index, (col, cid))| (col, (cid, index))) .collect(); self.node_mapping .insert(id, LoweredTarget::Input(input_cids)); rq::TableRef { source: tid, name, columns, prefer_cte: true, } } fn lower_relation(&mut self, expr: pl::Expr) -> Result { let span = expr.span; let lineage = expr.lineage.clone(); let prev_pipeline = self.pipeline.drain(..).collect_vec(); self.lower_pipeline(expr, None)?; let mut transforms = self.pipeline.drain(..).collect_vec(); let columns = self.push_select(lineage, &mut transforms).with_span(span)?; self.pipeline = prev_pipeline; let relation = rq::Relation { kind: rq::RelationKind::Pipeline(transforms), columns, }; Ok(relation) } // Result is stored in self.pipeline fn lower_pipeline(&mut self, ast: pl::Expr, closure_param: Option) -> Result<()> { let transform_call = match ast.kind { pl::ExprKind::TransformCall(transform) => transform, pl::ExprKind::Func(closure) => { let param = closure.params.first(); let param = param.and_then(|p| p.name.parse::().ok()); return self.lower_pipeline(*closure.body, param); } _ => { if let Some(target) = ast.target_id { if Some(target) == closure_param { // ast is a closure param, so we can skip pushing From return Ok(()); } } let table_ref = self.lower_table_ref(ast)?; self.pipeline.push(Transform::From(table_ref)); return Ok(()); } }; // lower input table self.lower_pipeline(*transform_call.input, closure_param)?; // ... and continues with transforms created in this function let window = rq::Window { frame: WindowFrame { kind: transform_call.frame.kind, range: self.lower_range(transform_call.frame.range)?, }, partition: if let Some(partition) = transform_call.partition { self.declare_as_columns(*partition, false)? } else { vec![] }, sort: self.lower_sorts(transform_call.sort)?, }; self.window = Some(window); match *transform_call.kind { pl::TransformKind::Derive { assigns, .. } => { self.declare_as_columns(*assigns, false)?; } pl::TransformKind::Select { assigns, .. } => { let cids = self.declare_as_columns(*assigns, false)?; self.pipeline.push(Transform::Select(cids)); } pl::TransformKind::Filter { filter, .. } => { let filter = self.lower_expr(*filter)?; self.pipeline.push(Transform::Filter(filter)); } pl::TransformKind::Aggregate { assigns, .. } => { let window = self.window.take(); let compute = self.declare_as_columns(*assigns, true)?; let partition = window.unwrap().partition; self.pipeline .push(Transform::Aggregate { partition, compute }); } pl::TransformKind::Sort { by, .. } => { let sorts = self.lower_sorts(by)?; self.pipeline.push(Transform::Sort(sorts)); } pl::TransformKind::Take { range, .. } => { let window = self.window.take().unwrap_or_default(); let range = self.lower_range(range)?; validate_take_range(&range, ast.span)?; self.pipeline.push(Transform::Take(rq::Take { range, partition: window.partition, sort: window.sort, })); } pl::TransformKind::Join { side, with, filter, .. } => { let with = self.lower_table_ref(*with)?; let transform = Transform::Join { side, with, filter: self.lower_expr(*filter)?, }; self.pipeline.push(transform); } pl::TransformKind::Append(bottom) => { let mut bottom = self.lower_table_ref(*bottom)?; bottom.prefer_cte = false; self.pipeline.push(Transform::Append(bottom)); } pl::TransformKind::Loop(pipeline) => { let relation = self.lower_relation(*pipeline)?; let mut pipeline = relation.kind.into_pipeline().unwrap(); // last select is not needed here pipeline.pop(); self.pipeline.push(Transform::Loop(pipeline)); } pl::TransformKind::Group { .. } | pl::TransformKind::Window { .. } => unreachable!( "transform `{}` cannot be lowered.", (*transform_call.kind).as_ref() ), } self.window = None; // result is stored in self.pipeline Ok(()) } fn lower_range(&mut self, range: Range>) -> Result> { Ok(Range { start: range.start.map(|x| self.lower_expr(*x)).transpose()?, end: range.end.map(|x| self.lower_expr(*x)).transpose()?, }) } fn lower_sorts(&mut self, by: Vec>>) -> Result>> { by.into_iter() .map(|ColumnSort { column, direction }| { let column = self.declare_as_column(*column, false)?; Ok(ColumnSort { direction, column }) }) .try_collect() } /// Append a Select of final table columns derived from frame fn push_select( &mut self, lineage: Option, transforms: &mut Vec, ) -> Result> { let lineage = lineage.unwrap_or_default(); log::debug!("push_select of a frame: {lineage:?}"); let mut columns = Vec::new(); // normal columns for col in &lineage.columns { match col { LineageColumn::Single { name, target_id, target_name, } => { let cid = self.lookup_cid(*target_id, target_name.as_ref())?; let name = name.as_ref().map(|i| i.name.clone()); columns.push((RelationColumn::Single(name), cid)); } LineageColumn::All { input_id, except } => { let Some(input) = lineage.find_input(*input_id) else { return Err(Error::new_simple( "column references a table not accessible in this context", ) .push_hint("join is not supported inside group")); }; match &self.node_mapping[&input.id] { LoweredTarget::Compute(_cid) => unreachable!(), LoweredTarget::Input(input_cols) => { let mut input_cols = input_cols .iter() .filter(|(c, _)| match c { RelationColumn::Single(Some(name)) => !except.contains(name), _ => true, }) .collect_vec(); input_cols.sort_by_key(|e| e.1 .1); for (col, (cid, _)) in input_cols { columns.push((col.clone(), *cid)); } } } } } } let (cols, cids) = columns.into_iter().unzip(); log::debug!("... cids={cids:?}"); transforms.push(Transform::Select(cids)); Ok(cols) } fn declare_as_columns(&mut self, exprs: pl::Expr, is_aggregation: bool) -> Result> { // special case: reference to a tuple that is a relational input if exprs.ty.as_ref().is_some_and(|x| x.kind.is_tuple()) && exprs.kind.is_ident() { // return all contained columns let input_id = exprs.target_id.as_ref().unwrap(); let id_mapping = self.node_mapping.get(input_id).unwrap(); let input_columns = id_mapping.as_input().unwrap(); return Ok(input_columns .iter() .sorted_by_key(|c| c.1 .1) .map(|(_, (cid, _))| *cid) .collect_vec()); } let mut r = Vec::new(); match exprs.kind { pl::ExprKind::All { within, except } => { // special case: ExprKind::All r.extend(self.find_selected_all(*within, Some(*except))?); } pl::ExprKind::Tuple(fields) => { // tuple unpacking for expr in fields { r.extend(self.declare_as_columns(expr, is_aggregation)?); } } _ => { // base case r.push(self.declare_as_column(exprs, is_aggregation)?); } } Ok(r) } fn find_selected_all( &mut self, within: pl::Expr, except: Option, ) -> Result> { let mut selected = self.declare_as_columns(within, false)?; if let Some(except) = except { let except: HashSet<_> = self.find_except_ids(except)?; selected.retain(|t| !except.contains(t)); } Ok(selected) } fn find_except_ids(&mut self, except: pl::Expr) -> Result> { let pl::ExprKind::Tuple(fields) = except.kind else { return Ok(HashSet::new()); }; let mut res = HashSet::new(); for e in fields { if e.target_id.is_none() { continue; } let id = e.target_id.unwrap(); match e.kind { pl::ExprKind::Ident(_) if e.ty.as_ref().is_some_and(|x| x.kind.is_tuple()) => { res.extend(self.find_selected_all(e, None).with_span(except.span)?); } pl::ExprKind::Ident(ident) => { res.insert( self.lookup_cid(id, Some(&ident.name)) .with_span(except.span)?, ); } pl::ExprKind::All { within, except } => { res.extend(self.find_selected_all(*within, Some(*except))?) } _ => { return Err(Error::new(Reason::Expected { who: None, expected: "an identifier".to_string(), found: write_pl(e), })); } } } Ok(res) } fn declare_as_column( &mut self, mut expr_ast: pl::Expr, is_aggregation: bool, ) -> Result { // short-circuit if this node has already been lowered if let Some(LoweredTarget::Compute(lowered)) = self.node_mapping.get(&expr_ast.id.unwrap()) { return Ok(*lowered); } // copy metadata before lowering let alias = expr_ast.alias.clone(); let has_alias = alias.is_some(); let needs_window = expr_ast.needs_window; expr_ast.needs_window = false; let alias_for = if has_alias { expr_ast.kind.as_ident().map(|x| x.name.clone()) } else { None }; let id = expr_ast.id.unwrap(); // lower let expr = self.lower_expr(expr_ast)?; // don't create new ColumnDef if expr is just a ColumnRef with no renaming if let rq::ExprKind::ColumnRef(cid) = &expr.kind { if !needs_window && (!has_alias || alias == alias_for) { self.node_mapping.insert(id, LoweredTarget::Compute(*cid)); return Ok(*cid); } } // determine window let window = if needs_window { self.window.clone() } else { None }; // construct ColumnDef let cid = self.cid.gen(); let compute = rq::Compute { id: cid, expr, window, is_aggregation, }; self.node_mapping.insert(id, LoweredTarget::Compute(cid)); self.pipeline.push(Transform::Compute(compute)); Ok(cid) } fn lower_expr(&mut self, expr: pl::Expr) -> Result { let span = expr.span; if expr.needs_window { let span = expr.span; let cid = self.declare_as_column(expr, false)?; let kind = rq::ExprKind::ColumnRef(cid); return Ok(rq::Expr { kind, span }); } let kind = match expr.kind { pl::ExprKind::Ident(ident) => { log::debug!("lowering ident {ident} (target {:?})", expr.target_id); if expr.ty.as_ref().is_some_and(|x| x.kind.is_tuple()) { // special case: tuple ref let expr = pl::Expr { kind: pl::ExprKind::Ident(ident), ..expr }; let selected = self.find_selected_all(expr, None)?; if selected.len() == 1 { rq::ExprKind::ColumnRef(selected[0]) } else { return Err( Error::new_simple("This wildcard usage is not yet supported.") .with_span(span), ); } } else if let Some(id) = expr.target_id { // base case: column ref let cid = self.lookup_cid(id, Some(&ident.name)).with_span(span)?; rq::ExprKind::ColumnRef(cid) } else { // fallback: unresolved ident // Let's hope that the database engine can resolve it. rq::ExprKind::SString(vec![InterpolateItem::String(ident.name)]) } } pl::ExprKind::All { within, except } => { let selected = self.find_selected_all(*within, Some(*except))?; if selected.len() == 1 { rq::ExprKind::ColumnRef(selected[0]) } else { return Err( Error::new_simple("This wildcard usage is not yet supported.") .with_span(span), ); } } pl::ExprKind::Literal(literal) => rq::ExprKind::Literal(literal), pl::ExprKind::SString(items) => { rq::ExprKind::SString(self.lower_interpolations(items)?) } pl::ExprKind::FString(items) => { let mut res = None; for item in items { let item = Some(match item { pl::InterpolateItem::String(string) => str_lit(string), pl::InterpolateItem::Expr { expr, .. } => self.lower_expr(*expr)?, }); res = rq::maybe_binop(res, "std.concat", item); } res.unwrap_or_else(|| str_lit("".to_string())).kind } pl::ExprKind::Case(cases) => rq::ExprKind::Case( cases .into_iter() .map(|case| -> Result<_> { Ok(SwitchCase { condition: self.lower_expr(*case.condition)?, value: self.lower_expr(*case.value)?, }) }) .try_collect()?, ), pl::ExprKind::RqOperator { name, args } => { // Check for relation types used as operator arguments for arg in &args { if arg.ty.as_ref().is_some_and(|x| x.is_relation()) { return Err(Error::new_simple( "table variable cannot be used as a scalar value", ) .push_hint("use a join instead, or inline the subquery") .with_span(arg.span)); } } let args = args.into_iter().map(|x| self.lower_expr(x)).try_collect()?; rq::ExprKind::Operator { name, args } } pl::ExprKind::Param(id) => rq::ExprKind::Param(id), pl::ExprKind::Tuple(_) => { return Err( Error::new_simple("table instance cannot be referenced directly") .push_hint("column name might be missing?") .with_span(span), ); } pl::ExprKind::Array(exprs) => rq::ExprKind::Array( exprs .into_iter() .map(|x| self.lower_expr(x)) .try_collect()?, ), pl::ExprKind::FuncCall(_) | pl::ExprKind::Func(_) | pl::ExprKind::TransformCall(_) => { log::debug!("cannot lower {expr:?}"); return Err(Error::new(Reason::Unexpected { found: format!("`{}`", write_pl(expr.clone())), }) .push_hint("this is probably a 'bad type' error (we are working on that)") .with_span(expr.span)); } pl::ExprKind::Internal(_) => { return Err(Error::new_assert(format!( "Unresolved lowering: {}", write_pl(expr) ))) } }; Ok(rq::Expr { kind, span }) } fn lower_interpolations( &mut self, items: Vec>, ) -> Result>> { items .into_iter() .map(|i| { Ok(match i { InterpolateItem::String(s) => InterpolateItem::String(s), InterpolateItem::Expr { expr, .. } => InterpolateItem::Expr { expr: Box::new(self.lower_expr(*expr)?), format: None, }, }) }) .try_collect() } fn lookup_cid(&mut self, id: usize, name: Option<&String>) -> Result { let cid = match self.node_mapping.get(&id) { Some(LoweredTarget::Compute(cid)) => *cid, Some(LoweredTarget::Input(input_columns)) => { let name = match name { Some(v) => RelationColumn::Single(Some(v.clone())), None => return Err(Error::new_simple( "This table contains unnamed columns that need to be referenced by name", ) .with_span(self.root_mod.span_map.get(&id).cloned()) .push_hint("the name may have been overridden later in the pipeline.")), }; log::trace!("lookup cid of name={name:?} in input {input_columns:?}"); if let Some((cid, _)) = input_columns.get(&name) { *cid } else { panic!("cannot find cid by id={id} and name={name:?}"); } } None => { return Err(Error::new_bug(3870))?; } }; Ok(cid) } } /// Attempts to extract column names from an S-String to avoid wildcards when possible fn try_extract_sql_columns( columns: Vec, items: &[InterpolateItem], ) -> Vec { use sqlparser::ast; let mut has_wildcard = false; let sql_columns = items .iter() .map(|item| match item { InterpolateItem::String(s) => { let sql_ast = sqlparser::parser::Parser::parse_sql(&sqlparser::dialect::GenericDialect {}, s) .map_err(|err| format!("could not parse {item:?}: {err:?}"))?; if sql_ast.len() != 1 { return Err(format!( "expected exactly one statement, got {}", sql_ast.len() )); } let statement = sql_ast.into_iter().next().unwrap(); if let sqlparser::ast::Statement::Query(query) = statement { if let sqlparser::ast::SetExpr::Select(select_stmt) = *query.body { select_stmt .projection .into_iter() .map(|expr| match expr { ast::SelectItem::UnnamedExpr(expr) => { if let ast::Expr::Identifier(ast::Ident { value, .. }) = expr { Ok(value) } else { Err(format!("Only Idents are supported, got {expr:?}")) } } ast::SelectItem::ExprWithAlias { alias, .. } => Ok(alias.value), // Store alias ast::SelectItem::QualifiedWildcard(_, _) | ast::SelectItem::Wildcard(_) => { has_wildcard = true; Err("columns contain a wildcard".into()) } }) .collect::, String>>() } else { Err(format!("not a SELECT statement: {query:?}")) } } else { Err(format!("not a Query: {statement:?}")) } } InterpolateItem::Expr { .. } => Err(format!( "could not extract columns from item {item:?}: not a string" )), }) .collect::>, _>>(); let sql_columns = match sql_columns { Ok(sql_columns) => sql_columns, Err(cause) => { log::warn!("Could not extract SQL columns: {cause}"); return columns; } } .into_iter() .flatten() // deduplicate extracted columns, but preserve their order .collect::>(); if has_wildcard { log::debug!("s-string contains a wildcard, skipping column extraction"); return columns; } columns .into_iter() .filter(|column| matches!(column, RelationColumn::Single(_))) .chain( sql_columns .into_iter() .map(|col| RelationColumn::Single(Some(col))), ) .collect() } fn str_lit(string: String) -> rq::Expr { rq::Expr { kind: rq::ExprKind::Literal(Literal::String(string)), span: None, } } fn validate_take_range(range: &Range, span: Option) -> Result<()> { fn bound_as_int(bound: &Option) -> Option> { bound .as_ref() .map(|e| e.kind.as_literal().and_then(|l| l.as_integer())) } fn bound_display(bound: Option>) -> String { bound .map(|x| x.map(|l| l.to_string()).unwrap_or_else(|| "?".to_string())) .unwrap_or_default() } let start = bound_as_int(&range.start); let end = bound_as_int(&range.end); let start_ok = if let Some(start) = start { start.map(|s| *s >= 1).unwrap_or(false) } else { true }; let end_ok = if let Some(end) = end { end.map(|e| *e >= 1).unwrap_or(false) } else { true }; if !start_ok || !end_ok { let range_display = format!("{}..{}", bound_display(start), bound_display(end)); Err(Error::new(Reason::Expected { who: Some("take".to_string()), expected: "a positive int range".to_string(), found: range_display, }) .with_span(span)) } else { Ok(()) } } #[derive(Default)] struct TableExtractor { path: Vec, tables: Vec<(Ident, (decl::TableDecl, Option))>, } impl TableExtractor { /// Finds table declarations in a module, recursively. fn extract(root_module: &Module) -> Vec<(Ident, (decl::TableDecl, Option))> { let mut te = TableExtractor::default(); te.extract_from_module(root_module); te.tables } /// Finds table declarations in a module, recursively. fn extract_from_module(&mut self, namespace: &Module) { for (name, entry) in &namespace.names { self.path.push(name.clone()); match &entry.kind { DeclKind::Module(ns) => { self.extract_from_module(ns); } DeclKind::TableDecl(table) => { let fq_ident = Ident::from_path(self.path.clone()); self.tables .push((fq_ident, (table.clone(), entry.declared_at))); } _ => {} } self.path.pop(); } } } /// Does a topological sort of the pipeline definitions and prunes all definitions that /// are not needed for the main pipeline. To do this, it needs to collect references /// between pipelines. fn toposort_tables( tables: Vec<(Ident, (decl::TableDecl, Option))>, main_table: &Ident, ) -> Vec<(Ident, (decl::TableDecl, Option))> { let tables: HashMap<_, _, RandomState> = HashMap::from_iter(tables); let mut dependencies: Vec<(Ident, Vec)> = Vec::new(); for (ident, table) in &tables { let deps = if let TableExpr::RelationVar(e) = &table.0.expr { TableDepsCollector::collect(*e.clone()) } else { vec![] }; dependencies.push((ident.clone(), deps)); } // sort just to make sure lowering is stable dependencies.sort_by(|a, b| a.0.cmp(&b.0)); let sort = toposort(&dependencies, Some(main_table)).unwrap(); let mut tables = tables; sort.into_iter() .map(|ident| tables.remove_entry(ident).unwrap()) .collect_vec() } #[derive(Default)] struct TableDepsCollector { deps: Vec, } impl TableDepsCollector { fn collect(expr: pl::Expr) -> Vec { let mut c = TableDepsCollector::default(); c.fold_expr(expr).unwrap(); c.deps } } impl PlFold for TableDepsCollector { fn fold_expr(&mut self, mut expr: pl::Expr) -> Result { expr.kind = match expr.kind { pl::ExprKind::Ident(ref ident) => { if let Some(ty) = &expr.ty { if ty.is_relation() { self.deps.push(ident.clone()); } } expr.kind } pl::ExprKind::TransformCall(tc) => { pl::ExprKind::TransformCall(self.fold_transform_call(tc)?) } pl::ExprKind::Func(func) => pl::ExprKind::Func(Box::new(self.fold_func(*func)?)), // optimization: don't recurse into anything else than TransformCalls and Func _ => expr.kind, }; Ok(expr) } } fn get_span_of_id(l: &Lowerer, id: Option) -> Option { id.and_then(|id| l.root_mod.span_map.get(&id)).cloned() } fn with_span_if_not_exists<'a, F>(get_span: F) -> impl FnOnce(Error) -> Error + 'a where F: FnOnce() -> Option + 'a, { move |e| { if e.span.is_some() { return e; } e.with_span(get_span()) } } ================================================ FILE: prqlc/prqlc/src/semantic/mod.rs ================================================ //! Semantic resolver (name resolution, type checking and lowering to RQ) pub mod ast_expand; mod lowering; mod module; pub mod reporting; mod resolver; pub use lowering::lower_to_ir; use self::resolver::Resolver; pub use self::resolver::ResolverOptions; use crate::ir::decl::{Module, RootModule}; use crate::ir::pl::{self, ImportDef, ModuleDef, Stmt, StmtKind, TypeDef, VarDef}; use crate::ir::rq::RelationalQuery; use crate::parser::is_mod_def_for; use crate::pr; use crate::WithErrorInfo; use crate::{debug, parser}; use crate::{Error, Reason, Result}; /// Runs semantic analysis on the query and lowers PL to RQ. pub fn resolve_and_lower( file_tree: pr::ModuleDef, main_path: &[String], database_module_path: Option<&[String]>, ) -> Result { let root_mod = resolve(file_tree)?; debug::log_stage(debug::Stage::Semantic(debug::StageSemantic::Lowering)); let default_db = [NS_DEFAULT_DB.to_string()]; let database_module_path = database_module_path.unwrap_or(&default_db); let (query, _) = lowering::lower_to_ir(root_mod, main_path, database_module_path)?; debug::log_entry(|| debug::DebugEntryKind::ReprRq(query.clone())); Ok(query) } /// Runs semantic analysis on the query. pub fn resolve(mut module_tree: pr::ModuleDef) -> Result { load_std_lib(&mut module_tree); // expand AST into PL debug::log_stage(debug::Stage::Semantic(debug::StageSemantic::AstExpand)); let root_module_def = ast_expand::expand_module_def(module_tree)?; debug::log_entry(|| debug::DebugEntryKind::ReprPl(root_module_def.clone())); // init new root module let mut root_module = RootModule { module: Module::new_root(), ..Default::default() }; let mut resolver = Resolver::new(&mut root_module); // resolve the module def into the root module debug::log_stage(debug::Stage::Semantic(debug::StageSemantic::Resolver)); resolver.fold_statements(root_module_def.stmts)?; debug::log_entry(|| debug::DebugEntryKind::ReprDecl(root_module.clone())); Ok(root_module) } /// Preferred way of injecting std module. pub fn load_std_lib(module_tree: &mut pr::ModuleDef) { if !module_tree.stmts.iter().any(|s| is_mod_def_for(s, NS_STD)) { log::debug!("loading std.prql"); let _suppressed = debug::log_suppress(); let std_source = include_str!("std.prql"); match parser::parse_source(std_source, 0) { Ok(stmts) => { let stmt = pr::Stmt::new(pr::StmtKind::ModuleDef(pr::ModuleDef { name: "std".to_string(), stmts, })); module_tree.stmts.insert(0, stmt); } Err(errs) => { panic!("std.prql failed to compile:\n{errs:?}"); } } } } pub fn is_ident_or_func_call(expr: &pl::Expr, name: &pr::Ident) -> bool { match &expr.kind { pl::ExprKind::Ident(i) if i == name => true, pl::ExprKind::FuncCall(pl::FuncCall { name: n_expr, .. }) if n_expr.kind.as_ident() == Some(name) => { true } _ => false, } } pub const NS_STD: &str = "std"; pub const NS_THIS: &str = "this"; pub const NS_THAT: &str = "that"; pub const NS_PARAM: &str = "_param"; pub const NS_DEFAULT_DB: &str = "default_db"; pub const NS_QUERY_DEF: &str = "prql"; pub const NS_MAIN: &str = "main"; // refers to the containing module (direct parent) pub const NS_SELF: &str = "_self"; // implies we can infer new non-module declarations in the containing module pub const NS_INFER: &str = "_infer"; // implies we can infer new module declarations in the containing module pub const NS_INFER_MODULE: &str = "_infer_module"; impl Stmt { pub fn new(kind: StmtKind) -> Stmt { Stmt { id: None, kind, span: None, annotations: Vec::new(), } } pub(crate) fn name(&self) -> &str { match &self.kind { StmtKind::QueryDef(_) => NS_QUERY_DEF, StmtKind::VarDef(VarDef { name, .. }) => name, StmtKind::TypeDef(TypeDef { name, .. }) => name, StmtKind::ModuleDef(ModuleDef { name, .. }) => name, StmtKind::ImportDef(ImportDef { name, alias }) => alias.as_ref().unwrap_or(&name.name), } } } impl pl::Expr { fn try_cast(self, f: F, who: Option<&str>, expected: S2) -> Result where F: FnOnce(pl::ExprKind) -> Result, { f(self.kind).map_err(|i| { Error::new(Reason::Expected { who: who.map(|s| s.to_string()), expected: expected.to_string(), found: format!("`{}`", write_pl(pl::Expr::new(i))), }) .with_span(self.span) }) } } /// Write a PL IR to string. /// /// Because PL needs to be restricted back to AST, ownerships of expr is required. pub fn write_pl(expr: pl::Expr) -> String { let expr = ast_expand::restrict_expr(expr); crate::codegen::write_expr(&expr) } #[cfg(test)] pub mod test { use insta::assert_yaml_snapshot; use super::{resolve, resolve_and_lower, RootModule}; use crate::ir::rq::RelationalQuery; use crate::parser::parse; use crate::Errors; pub fn parse_resolve_and_lower(query: &str) -> Result { let source_tree = query.into(); Ok(resolve_and_lower(parse(&source_tree)?, &[], None)?) } pub fn parse_and_resolve(query: &str) -> Result { let source_tree = query.into(); Ok(resolve(parse(&source_tree)?)?) } #[test] fn test_resolve_01() { assert_yaml_snapshot!(parse_resolve_and_lower(r###" from employees select !{foo} "###).unwrap().relation.columns, @"- Wildcard") } #[test] fn test_resolve_02() { assert_yaml_snapshot!(parse_resolve_and_lower(r###" from foo sort day window range:-4..4 ( derive {next_four_days = sum b} ) "###).unwrap().relation.columns, @r" - Single: day - Single: b - Wildcard - Single: next_four_days ") } #[test] fn test_resolve_03() { assert_yaml_snapshot!(parse_resolve_and_lower(r###" from a=albums filter is_sponsored select {a.*} "###).unwrap().relation.columns, @r" - Single: is_sponsored - Wildcard ") } #[test] fn test_resolve_04() { assert_yaml_snapshot!(parse_resolve_and_lower(r###" from x select {a, a, a = a + 1} "###).unwrap().relation.columns, @r" - Single: ~ - Single: ~ - Single: a ") } #[test] fn test_header() { assert_yaml_snapshot!(parse_resolve_and_lower(r#" prql target:sql.mssql version:"0" from employees "#).unwrap(), @r" def: version: ^0 other: target: sql.mssql tables: - id: 0 name: ~ relation: kind: ExternRef: LocalTable: - employees columns: - Wildcard relation: kind: Pipeline: - From: source: 0 columns: - - Wildcard - 0 name: employees prefer_cte: true - Select: - 0 columns: - Wildcard " ); assert!(parse_resolve_and_lower( r###" prql target:sql.bigquery version:foo from employees "###, ) .is_err()); assert!(parse_resolve_and_lower( r#" prql target:sql.bigquery version:"25" from employees "#, ) .is_err()); assert!(parse_resolve_and_lower( r###" prql target:sql.yah version:foo from employees "###, ) .is_err()); } } ================================================ FILE: prqlc/prqlc/src/semantic/module.rs ================================================ use std::collections::{HashMap, HashSet}; use super::{ NS_DEFAULT_DB, NS_INFER, NS_INFER_MODULE, NS_MAIN, NS_PARAM, NS_QUERY_DEF, NS_SELF, NS_STD, NS_THAT, NS_THIS, }; use crate::ir::decl::{Decl, DeclKind, Module, RootModule, TableDecl, TableExpr}; use crate::ir::pl::{Annotation, Expr, Ident, Lineage, LineageColumn}; use crate::pr::QueryDef; use crate::pr::{Span, Ty, TyKind, TyTupleField}; use crate::Error; use crate::Result; impl Module { pub fn singleton(name: S, entry: Decl) -> Module { Module { names: HashMap::from([(name.to_string(), entry)]), ..Default::default() } } pub fn new_root() -> Module { // Each module starts with a default namespace that contains a wildcard // and the standard library. Module { names: HashMap::from([ ( NS_DEFAULT_DB.to_string(), Decl::from(DeclKind::Module(Module::new_database())), ), (NS_STD.to_string(), Decl::from(DeclKind::default())), ]), shadowed: None, redirects: vec![ Ident::from_name(NS_THIS), Ident::from_name(NS_THAT), Ident::from_name(NS_PARAM), Ident::from_name(NS_STD), ], } } pub fn new_database() -> Module { let names = HashMap::from([ ( NS_INFER.to_string(), Decl::from(DeclKind::Infer(Box::new(DeclKind::TableDecl(TableDecl { ty: Some(Ty::relation(vec![TyTupleField::Wildcard(None)])), expr: TableExpr::LocalTable, })))), ), ( NS_INFER_MODULE.to_string(), Decl::from(DeclKind::Infer(Box::new(DeclKind::Module(Module { names: HashMap::new(), redirects: vec![], shadowed: None, })))), ), ]); Module { names, shadowed: None, redirects: vec![], } } pub fn insert(&mut self, fq_ident: Ident, decl: Decl) -> Result, Error> { if fq_ident.path.is_empty() { Ok(self.names.insert(fq_ident.name, decl)) } else { let (top_level, remaining) = fq_ident.pop_front(); let entry = self.names.entry(top_level).or_default(); if let DeclKind::Module(inner) = &mut entry.kind { inner.insert(remaining.unwrap(), decl) } else { Err(Error::new_simple( "path does not resolve to a module or a table", )) } } } pub fn get_mut(&mut self, ident: &Ident) -> Option<&mut Decl> { let mut ns = self; for part in &ident.path { let entry = ns.names.get_mut(part); match entry { Some(Decl { kind: DeclKind::Module(inner), .. }) => { ns = inner; } _ => return None, } } ns.names.get_mut(&ident.name) } /// Get namespace entry using a fully qualified ident. pub fn get(&self, fq_ident: &Ident) -> Option<&Decl> { let mut ns = self; for (index, part) in fq_ident.path.iter().enumerate() { let decl = ns.names.get(part)?; match &decl.kind { DeclKind::Module(inner) => { ns = inner; } DeclKind::LayeredModules(stack) => { let next = fq_ident.path.get(index + 1).unwrap_or(&fq_ident.name); let mut found = false; for n in stack.iter().rev() { if n.names.contains_key(next) { ns = n; found = true; break; } } if !found { return None; } } _ => return None, } } ns.names.get(&fq_ident.name) } pub fn lookup(&self, ident: &Ident) -> HashSet { fn lookup_in(module: &Module, ident: Ident) -> HashSet { let (prefix, ident) = ident.pop_front(); if let Some(ident) = ident { if let Some(entry) = module.names.get(&prefix) { let redirected = match &entry.kind { DeclKind::Module(ns) => ns.lookup(&ident), DeclKind::LayeredModules(stack) => { let mut r = HashSet::new(); for ns in stack.iter().rev() { r = ns.lookup(&ident); if !r.is_empty() { break; } } r } _ => HashSet::new(), }; return redirected .into_iter() .map(|i| Ident::from_name(&prefix) + i) .collect(); } } else if let Some(decl) = module.names.get(&prefix) { if let DeclKind::Module(inner) = &decl.kind { if inner.names.contains_key(NS_SELF) { return HashSet::from([Ident::from_path(vec![ prefix, NS_SELF.to_string(), ])]); } } return HashSet::from([Ident::from_name(prefix)]); } HashSet::new() } log::trace!("lookup: {ident}"); let mut res = HashSet::new(); res.extend(lookup_in(self, ident.clone())); for redirect in &self.redirects { log::trace!("... following redirect {redirect}"); let r = lookup_in(self, redirect.clone() + ident.clone()); log::trace!("... result of redirect {redirect}: {r:?}"); res.extend(r); } res } pub(super) fn insert_frame(&mut self, lineage: &Lineage, namespace: &str) { let namespace = self.names.entry(namespace.to_string()).or_default(); let namespace = namespace.kind.as_module_mut().unwrap(); let lin_ty = *ty_of_lineage(lineage).kind.into_array().unwrap().unwrap(); for (col_index, column) in lineage.columns.iter().enumerate() { // determine input name let input_name = match column { LineageColumn::All { input_id, .. } => { lineage.find_input(*input_id).map(|i| &i.name) } LineageColumn::Single { name, .. } => name.as_ref().and_then(|n| n.path.first()), }; // get or create input namespace let ns; if let Some(input_name) = input_name { let entry = match namespace.names.get_mut(input_name) { Some(x) => x, None => { namespace.redirects.push(Ident::from_name(input_name)); let input = lineage.find_input_by_name(input_name).unwrap(); let order = lineage.inputs.iter().position(|i| i.id == input.id); let order = order.unwrap(); let mut sub_ns = Module::default(); let self_ty = lin_ty.clone().kind.into_tuple().unwrap(); let self_ty = self_ty .into_iter() .flat_map(|x| x.into_single()) .find(|(name, _)| name.as_ref() == Some(input_name)) .and_then(|(_, ty)| ty) .or(Some(Ty::new(TyKind::Tuple(vec![TyTupleField::Wildcard( None, )])))); let self_decl = Decl { declared_at: Some(input.id), kind: DeclKind::InstanceOf(input.table.clone(), self_ty), ..Default::default() }; sub_ns.names.insert(NS_SELF.to_string(), self_decl); let sub_ns = Decl { declared_at: Some(input.id), order, kind: DeclKind::Module(sub_ns), ..Default::default() }; namespace.names.entry(input_name.clone()).or_insert(sub_ns) } }; ns = entry.kind.as_module_mut().unwrap() } else { ns = namespace; } // insert column decl match column { LineageColumn::All { input_id, .. } => { // Input might not exist if lineage references an outer scope // (e.g., join inside group). This is an error caught during // lowering - skip here to avoid panic during resolution. if let Some(input) = lineage.find_input(*input_id) { let kind = DeclKind::Infer(Box::new(DeclKind::Column(input.id))); let declared_at = Some(input.id); let decl = Decl { kind, declared_at, order: col_index + 1, ..Default::default() }; ns.names.insert(NS_INFER.to_string(), decl); } } LineageColumn::Single { name: Some(name), target_id, .. } => { let decl = Decl { kind: DeclKind::Column(*target_id), declared_at: None, order: col_index + 1, ..Default::default() }; ns.names.insert(name.name.clone(), decl); } _ => {} } } // insert namespace._self with correct type namespace.names.insert( NS_SELF.to_string(), Decl::from(DeclKind::InstanceOf(Ident::from_name(""), Some(lin_ty))), ); } pub(super) fn insert_frame_col(&mut self, namespace: &str, name: String, id: usize) { let namespace = self.names.entry(namespace.to_string()).or_default(); let namespace = namespace.kind.as_module_mut().unwrap(); namespace.names.insert(name, DeclKind::Column(id).into()); } pub fn shadow(&mut self, ident: &str) { let shadowed = self.names.remove(ident).map(Box::new); let entry = DeclKind::Module(Module { shadowed, ..Default::default() }); self.names.insert(ident.to_string(), entry.into()); } pub fn unshadow(&mut self, ident: &str) { if let Some(entry) = self.names.remove(ident) { let ns = entry.kind.into_module().unwrap(); if let Some(shadowed) = ns.shadowed { self.names.insert(ident.to_string(), *shadowed); } } } pub fn stack_push(&mut self, ident: &str, namespace: Module) { let entry = self .names .entry(ident.to_string()) .or_insert_with(|| DeclKind::LayeredModules(Vec::new()).into()); let stack = entry.kind.as_layered_modules_mut().unwrap(); stack.push(namespace); } pub fn stack_pop(&mut self, ident: &str) -> Option { (self.names.get_mut(ident)) .and_then(|e| e.kind.as_layered_modules_mut()) .and_then(|stack| stack.pop()) } pub(crate) fn into_exprs(self) -> HashMap { self.names .into_iter() .map(|(k, v)| (k, *v.kind.into_expr().unwrap())) .collect() } pub(crate) fn from_exprs(exprs: HashMap) -> Module { Module { names: exprs .into_iter() .map(|(key, expr)| { let decl = Decl { kind: DeclKind::Expr(Box::new(expr)), ..Default::default() }; (key, decl) }) .collect(), ..Default::default() } } pub fn as_decls(&self) -> Vec<(Ident, &Decl)> { let mut r = Vec::new(); for (name, decl) in &self.names { match &decl.kind { DeclKind::Module(module) => r.extend( module .as_decls() .into_iter() .map(|(inner, decl)| (Ident::from_name(name) + inner, decl)), ), _ => r.push((Ident::from_name(name), decl)), } } r } } type HintAndSpan = (Option, Option); impl RootModule { pub(super) fn declare( &mut self, ident: Ident, decl: DeclKind, id: Option, annotations: Vec, ) -> Result<()> { let existing = self.module.get(&ident); if existing.is_some() { return Err(Error::new_simple(format!( "duplicate declarations of {ident}" ))); } let decl = Decl { kind: decl, declared_at: id, order: 0, annotations, }; self.module.insert(ident, decl).unwrap(); Ok(()) } /// Finds that main pipeline given a path to either main itself or its parent module. /// Returns main expr and fq ident of the decl. pub fn find_main_rel(&self, path: &[String]) -> Result<(&TableExpr, Ident), HintAndSpan> { let (decl, ident) = self.find_main(path).map_err(|x| (x, None))?; let span = decl .declared_at .and_then(|id| self.span_map.get(&id)) .cloned(); let decl = (decl.kind.as_table_decl()) .ok_or((Some(format!("{ident} is not a relational variable")), span))?; Ok((&decl.expr, ident)) } pub fn find_main(&self, path: &[String]) -> Result<(&Decl, Ident), Option> { let mut tried_idents = Vec::new(); // is path referencing the relational var directly? if !path.is_empty() { let ident = Ident::from_path(path.to_vec()); let decl = self.module.get(&ident); if let Some(decl) = decl { return Ok((decl, ident)); } else { tried_idents.push(ident.to_string()); } } // is path referencing the parent module? { let mut path = path.to_vec(); path.push(NS_MAIN.to_string()); let ident = Ident::from_path(path); let decl = self.module.get(&ident); if let Some(decl) = decl { return Ok((decl, ident)); } else { tried_idents.push(ident.to_string()); } } Err(Some(format!( "Expected a declaration at {}", tried_idents.join(" or ") ))) } pub fn find_query_def(&self, main: &Ident) -> Option<&QueryDef> { let ident = Ident { path: main.path.clone(), name: NS_QUERY_DEF.to_string(), }; let decl = self.module.get(&ident)?; decl.kind.as_query_def() } } pub fn ty_of_lineage(lineage: &Lineage) -> Ty { Ty::relation( lineage .columns .iter() .map(|col| match col { LineageColumn::All { .. } => TyTupleField::Wildcard(None), LineageColumn::Single { name, .. } => { TyTupleField::Single(name.as_ref().map(|i| i.name.clone()), None) } }) .collect(), ) } #[cfg(test)] mod tests { use prqlc_parser::lexer::lr::Literal; use super::*; use crate::ir::pl::ExprKind; // TODO: tests / docstrings for `stack_pop` & `stack_push` & `insert_frame` #[test] fn test_module() { let mut module = Module::default(); let ident = Ident::from_name("test_name"); let expr: Expr = Expr::new(ExprKind::Literal(Literal::Integer(42))); let decl: Decl = DeclKind::Expr(Box::new(expr)).into(); assert!(module.insert(ident.clone(), decl.clone()).is_ok()); assert_eq!(module.get(&ident).unwrap(), &decl); assert_eq!(module.get_mut(&ident).unwrap(), &decl); // Lookup let lookup_result = module.lookup(&ident); assert_eq!(lookup_result.len(), 1); assert!(lookup_result.contains(&ident)); } #[test] fn test_module_shadow_unshadow() { let mut module = Module::default(); let ident = Ident::from_name("test_name"); let expr: Expr = Expr::new(ExprKind::Literal(Literal::Integer(42))); let decl: Decl = DeclKind::Expr(Box::new(expr)).into(); module.insert(ident.clone(), decl.clone()).unwrap(); module.shadow("test_name"); assert!(module.get(&ident) != Some(&decl)); module.unshadow("test_name"); assert_eq!(module.get(&ident).unwrap(), &decl); } } ================================================ FILE: prqlc/prqlc/src/semantic/reporting.rs ================================================ use std::collections::HashMap; use std::ops::Range; use ariadne::{Color, Label, Report, ReportBuilder, ReportKind, Source}; use schemars::JsonSchema; use serde::Serialize; use crate::ir::decl::{DeclKind, Module, RootModule, TableDecl, TableExpr}; use crate::ir::pl; use crate::ir::pl::PlFold; use crate::pr; use crate::{Result, Span}; pub fn label_references(root_mod: &RootModule, source_id: String, source: String) -> Vec { let report_span = (source_id.clone(), 0..source.len()); let mut report = Report::build(ReportKind::Custom("Info", Color::Blue), report_span); let source = Source::from(source); // label all idents and function calls let mut labeler = Labeler { root_mod, source: &source, source_id: &source_id, report: &mut report, }; labeler.label_module(&labeler.root_mod.module); let mut out = Vec::new(); report .finish() .write((source_id, source), &mut out) .unwrap(); out } /// Traverses AST and add labels for each of the idents and function calls struct Labeler<'a> { root_mod: &'a RootModule, source: &'a Source, source_id: &'a str, report: &'a mut ReportBuilder<'static, (String, Range)>, } impl Labeler<'_> { fn label_module(&mut self, module: &Module) { for (_, decl) in module.names.iter() { if let DeclKind::TableDecl(TableDecl { expr: TableExpr::RelationVar(expr), .. }) = &decl.kind { self.fold_expr(*expr.clone()).unwrap(); } } } fn get_span_lines(&mut self, id: usize) -> Option { let decl_span = self.root_mod.span_map.get(&id); decl_span.map(|decl_span| { let line_span = self.source.get_line_range(&Range::from(*decl_span)); if line_span.len() <= 1 { format!(" at line {}", line_span.start + 1) } else { format!(" at lines {}-{}", line_span.start + 1, line_span.end) } }) } } impl pl::PlFold for Labeler<'_> { fn fold_expr(&mut self, node: pl::Expr) -> Result { if let Some(ident) = node.kind.as_ident() { if let Some(span) = node.span { let decl = self.root_mod.module.get(ident); let ident = format!("[{ident}]"); let (decl, color) = if let Some(decl) = decl { let color = match &decl.kind { DeclKind::Expr(_) => Color::Blue, DeclKind::Ty(_) => Color::Green, DeclKind::Column { .. } => Color::Yellow, DeclKind::InstanceOf(_, _) => Color::Yellow, DeclKind::TableDecl { .. } => Color::Red, DeclKind::Module(module) => { self.label_module(module); Color::Cyan } DeclKind::LayeredModules(_) => Color::Cyan, DeclKind::Infer(_) => Color::White, DeclKind::QueryDef(_) => Color::White, DeclKind::Import(_) => Color::White, }; let location = decl .declared_at .and_then(|id| self.get_span_lines(id)) .unwrap_or_default(); let decl = match &decl.kind { DeclKind::TableDecl(TableDecl { ty, .. }) => { format!( "table {}", ty.as_ref().and_then(|t| t.name.clone()).unwrap_or_default() ) } _ => decl.to_string(), }; (format!("{decl}{location}"), color) } else if let Some(decl_id) = node.target_id { let lines = self.get_span_lines(decl_id).unwrap_or_default(); (format!("variable{lines}"), Color::Yellow) } else { ("".to_string(), Color::White) }; let label_span = (self.source_id.to_string(), span.start..span.end); self.report.add_label( Label::new(label_span) .with_message(format!("{ident} {decl}")) .with_color(color), ); } } Ok(pl::Expr { kind: self.fold_expr_kind(node.kind)?, ..node }) } } /// Traverses AST and collects all node.frame pub fn collect_frames(expr: pl::Expr) -> FrameCollector { let mut collector = FrameCollector { frames: vec![], nodes: vec![], ast: None, }; collector.fold_expr(expr).unwrap(); collector.frames.reverse(); let mut parent_updates = Vec::new(); let mut node_pos = HashMap::new(); for (i, node) in collector.nodes.iter().enumerate() { node_pos.insert(node.id, i); for &child in &node.children { parent_updates.push((child, node.id)); } } for (child, parent) in parent_updates { if let Some(child_pos) = node_pos.get(&child) { if let Some(child_node) = collector.nodes.get_mut(*child_pos) { child_node.parent = Some(parent); } } } collector } #[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)] pub struct ExprGraphNode { /// Node unique ID pub id: usize, /// Descriptive text about the node pub kind: String, /// Position of this expr in the original source query #[serde(default, skip_serializing_if = "Option::is_none")] pub span: Option, /// When this node is part of a Tuple, this holds the alias name #[serde(default, skip_serializing_if = "Option::is_none")] pub alias: Option, /// When kind is Ident, this holds the referenced name #[serde(default, skip_serializing_if = "Option::is_none")] pub ident: Option, /// Upstream sources of data for this expr as node IDs #[serde(default, skip_serializing_if = "Vec::is_empty")] pub targets: Vec, /// If this expr holds other exprs, these are their node IDs #[serde(default, skip_serializing_if = "Vec::is_empty")] pub children: Vec, /// If this expr is inside of another expr, this is its parent node ID #[serde(default, skip_serializing_if = "Option::is_none")] pub parent: Option, } #[derive(Serialize, JsonSchema)] pub struct FrameCollector { /// Each transformation step in the main pipeline corresponds to a single /// frame. This holds the output columns at each frame, as well as the span /// position of the frame. pub frames: Vec<(Option, pl::Lineage)>, /// A mapping of expression graph node IDs to their node definitions. pub nodes: Vec, /// The parsed AST from the provided query. pub ast: Option, } impl PlFold for FrameCollector { fn fold_expr(&mut self, expr: pl::Expr) -> Result { if let Some(id) = expr.id { let targets = match &expr.kind { pl::ExprKind::Ident(_) => { if let Some(target_id) = expr.target_id { vec![target_id] } else { vec![] } } pl::ExprKind::RqOperator { args, .. } => args.iter().filter_map(|e| e.id).collect(), pl::ExprKind::Case(switch) => switch .iter() .flat_map(|c| vec![c.condition.id.unwrap(), c.value.id.unwrap()]) .collect(), pl::ExprKind::SString(iv) | pl::ExprKind::FString(iv) => iv .iter() .filter_map(|i| match i { pl::InterpolateItem::Expr { expr: e, .. } => e.id, _ => None, }) .collect(), _ => vec![], }; let ident = if matches!(&expr.kind, pl::ExprKind::Ident(_)) { Some(expr.kind.clone()) } else { None }; let children = match &expr.kind { pl::ExprKind::Tuple(args) | pl::ExprKind::Array(args) => { args.iter().filter_map(|e| e.id).collect() } pl::ExprKind::TransformCall(tc) => { let mut tcc = vec![tc.input.id.unwrap()]; match *tc.kind { pl::TransformKind::Derive { assigns: ref e } | pl::TransformKind::Select { assigns: ref e } | pl::TransformKind::Filter { filter: ref e } | pl::TransformKind::Append(ref e) | pl::TransformKind::Loop(ref e) | pl::TransformKind::Group { pipeline: ref e, .. } | pl::TransformKind::Window { pipeline: ref e, .. } => { tcc.push(e.id.unwrap()); } pl::TransformKind::Aggregate { assigns: ref e } => { tcc.push(e.id.unwrap()); if let Some(p) = &tc.partition { tcc.push(p.id.unwrap()) } } pl::TransformKind::Join { ref with, ref filter, .. } => { tcc.push(with.id.unwrap()); tcc.push(filter.id.unwrap()); } pl::TransformKind::Take { ref range } => { if let Some(e) = &range.start { tcc.push(e.id.unwrap()); } if let Some(e) = &range.end { tcc.push(e.id.unwrap()); } } pl::TransformKind::Sort { ref by } => { for c in by { tcc.push(c.column.id.unwrap()); } } }; tcc } _ => vec![], }; let kind = match &expr.kind { pl::ExprKind::TransformCall(tc) => { let tc_kind = tc.kind.as_ref().as_ref().to_string(); format!("TransformCall: {tc_kind}") } _ => expr.kind.as_ref().to_string(), }; self.nodes.push(ExprGraphNode { id, kind, span: expr.span, alias: expr.alias.clone(), ident, targets, children, parent: None, }); } self.nodes.sort_by(|a, b| a.id.cmp(&b.id)); self.nodes.dedup(); if matches!(expr.kind, pl::ExprKind::TransformCall(_)) { let lineage = expr.lineage.clone(); if let Some(lineage) = lineage { self.frames.push((expr.span, lineage)); } } Ok(pl::Expr { kind: self.fold_expr_kind(expr.kind)?, ..expr }) } } ================================================ FILE: prqlc/prqlc/src/semantic/resolver/expr.rs ================================================ use itertools::Itertools; use crate::ir::decl::{DeclKind, Module}; use crate::ir::pl; use crate::ir::pl::PlFold; use crate::pr::{Ty, TyKind, TyTupleField}; use crate::semantic::resolver::{flatten, types, Resolver}; use crate::semantic::{NS_INFER, NS_SELF, NS_THAT, NS_THIS}; use crate::utils::IdGenerator; use crate::Result; use crate::{Error, Reason, Span, WithErrorInfo}; impl pl::PlFold for Resolver<'_> { fn fold_stmts(&mut self, _: Vec) -> Result> { unreachable!() } fn fold_type(&mut self, ty: Ty) -> Result { Ok(match ty.kind { TyKind::Ident(ident) => { self.root_mod.module.shadow(NS_THIS); self.root_mod.module.shadow(NS_THAT); let fq_ident = self.resolve_ident(&ident)?; let decl = self.root_mod.module.get(&fq_ident).unwrap(); let decl_ty = decl.kind.as_ty().ok_or_else(|| { Error::new(Reason::Expected { who: None, expected: "a type".to_string(), found: decl.to_string(), }) })?; let mut ty = decl_ty.clone(); ty.name = ty.name.or(Some(fq_ident.name)); self.root_mod.module.unshadow(NS_THIS); self.root_mod.module.unshadow(NS_THAT); ty } _ => pl::fold_type(self, ty)?, }) } fn fold_var_def(&mut self, var_def: pl::VarDef) -> Result { let value = match var_def.value { Some(value) if matches!(value.kind, pl::ExprKind::Func(_)) => Some(value), Some(value) => Some(Box::new(flatten::Flattener::fold(self.fold_expr(*value)?))), None => None, }; Ok(pl::VarDef { name: var_def.name, value, ty: var_def.ty.map(|x| self.fold_type(x)).transpose()?, }) } fn fold_expr(&mut self, node: pl::Expr) -> Result { if node.id.is_some() && !matches!(node.kind, pl::ExprKind::Func(_)) { return Ok(node); } let id = self.id.gen(); let alias = Box::new(node.alias.clone()); let span = Box::new(node.span); if let Some(span) = *span { self.root_mod.span_map.insert(id, span); } log::trace!("folding expr [{id:?}] {node:?}"); let r = match node.kind { pl::ExprKind::Ident(ident) => { log::debug!("resolving ident {ident}..."); let fq_ident = self .resolve_ident(&ident) .map_err(|e| e.with_span(node.span))?; log::debug!("... resolved to {fq_ident}"); let entry = self.root_mod.module.get(&fq_ident).unwrap(); log::debug!("... which is {entry}"); match &entry.kind { DeclKind::Infer(_) => pl::Expr { kind: pl::ExprKind::Ident(fq_ident), target_id: entry.declared_at, ..node }, DeclKind::Column(target_id) => pl::Expr { kind: pl::ExprKind::Ident(fq_ident), target_id: Some(*target_id), ..node }, DeclKind::TableDecl(_) => { let input_name = ident.name.clone(); let lineage = self.lineage_of_table_decl(&fq_ident, input_name, id); pl::Expr { kind: pl::ExprKind::Ident(fq_ident), ty: Some(ty_of_lineage(&lineage)), lineage: Some(lineage), alias: None, ..node } } DeclKind::Expr(expr) => match &expr.kind { pl::ExprKind::Func(closure) => { let closure = self.fold_function_types(closure.clone())?; let expr = pl::Expr::new(pl::ExprKind::Func(closure)); if self.in_func_call_name { expr } else { self.fold_expr(expr)? } } _ => self.fold_expr(expr.as_ref().clone())?, }, DeclKind::InstanceOf(_, ty) => { let ty = ty.clone(); let fields = self.construct_wildcard_include(&fq_ident); pl::Expr { kind: pl::ExprKind::Tuple(fields), ty, ..node } } DeclKind::Ty(_) => { return Err(Error::new(Reason::Expected { who: None, expected: "a value".to_string(), found: "a type".to_string(), }) .with_span(*span)); } _ => pl::Expr { kind: pl::ExprKind::Ident(fq_ident), ..node }, } } pl::ExprKind::FuncCall(pl::FuncCall { name, args, .. }) if (name.kind.as_ident()).is_some_and(|i| i.to_string() == "std.not") && matches!(args[0].kind, pl::ExprKind::Tuple(_)) => { let arg = args.into_iter().exactly_one().unwrap(); self.resolve_column_exclusion(arg)? } pl::ExprKind::FuncCall(pl::FuncCall { name, args, named_args, }) => { // fold function name self.default_namespace = None; let old = self.in_func_call_name; self.in_func_call_name = true; let name = Box::new(self.fold_expr(*name)?); self.in_func_call_name = old; let func = name.try_cast(|n| n.into_func(), None, "a function")?; // fold function let func = self.apply_args_to_closure(func, args, named_args)?; self.fold_function(func, *span)? } pl::ExprKind::Func(closure) => self.fold_function(closure, *span)?, pl::ExprKind::Tuple(exprs) => { let exprs = self.fold_exprs(exprs)?; // flatten let exprs = exprs .into_iter() .flat_map(|e| match e.kind { pl::ExprKind::Tuple(items) if e.flatten => items, _ => vec![e], }) .collect_vec(); pl::Expr { kind: pl::ExprKind::Tuple(exprs), ..node } } item => pl::Expr { kind: pl::fold_expr_kind(self, item)?, ..node }, }; self.finish_expr_resolve(r, id, *alias, *span) } } impl Resolver<'_> { fn finish_expr_resolve( &mut self, expr: pl::Expr, id: usize, alias: Option, span: Option, ) -> Result { let mut r = Box::new(self.maybe_static_eval(expr)?); r.id = r.id.or(Some(id)); r.alias = r.alias.or(alias); r.span = r.span.or(span); if r.ty.is_none() { r.ty = Resolver::infer_type(&r)?; } if r.lineage.is_none() { if let pl::ExprKind::TransformCall(call) = &r.kind { r.lineage = Some(call.infer_lineage()?); } else if let Some(relation_columns) = r.ty.as_ref().and_then(|t| t.as_relation()) { log::debug!("found a relational type without lineage: declaring a new table for it: {relation_columns:?}"); // lineage from ty let columns = Some(relation_columns.clone()); let name = r.alias.clone(); let frame = self.declare_table_for_literal(id, columns, name); r.lineage = Some(frame); } } if let Some(lineage) = &mut r.lineage { if let Some(alias) = r.alias.take() { lineage.rename(alias.clone()); if let Some(ty) = &mut r.ty { types::rename_relation(&mut ty.kind, alias); } } } Ok(*r) } pub fn resolve_column_exclusion(&mut self, expr: pl::Expr) -> Result { let expr = self.fold_expr(expr)?; let except = self.coerce_into_tuple(expr)?; self.fold_expr(pl::Expr::new(pl::ExprKind::All { within: Box::new(pl::Expr::new(pl::Ident::from_name(NS_THIS))), except: Box::new(except), })) } pub fn construct_wildcard_include(&mut self, module_fq_self: &pl::Ident) -> Vec { let module_fq = module_fq_self.clone().pop().unwrap(); let decl = self.root_mod.module.get(&module_fq).unwrap(); let module = decl.kind.as_module().unwrap(); let prefix = module_fq.iter().collect_vec(); Self::construct_tuple_from_module(&mut self.id, &prefix, module) } pub fn construct_tuple_from_module( id: &mut IdGenerator, prefix: &[&String], module: &Module, ) -> Vec { let mut res = Vec::new(); if let Some(decl) = module.names.get(NS_INFER) { let wildcard_field = pl::Expr { id: Some(id.gen()), target_id: decl.declared_at, flatten: true, ty: Some(Ty::new(TyKind::Tuple(vec![TyTupleField::Wildcard(None)]))), ..pl::Expr::new(pl::Ident::from_name(NS_SELF)) }; return vec![wildcard_field]; } for (name, decl) in module.names.iter().sorted_by_key(|(_, d)| d.order) { res.push(match &decl.kind { DeclKind::Module(submodule) => { let prefix = [prefix.to_vec(), vec![name]].concat(); let sub_fields = Self::construct_tuple_from_module(id, &prefix, submodule); pl::Expr { id: Some(id.gen()), alias: Some(name.clone()), ..pl::Expr::new(pl::ExprKind::Tuple(sub_fields)) } } DeclKind::Column(target_id) => pl::Expr { id: Some(id.gen()), target_id: Some(*target_id), // alias: Some(name.clone()), ..pl::Expr::new(pl::Ident::from_path([prefix.to_vec(), vec![name]].concat())) }, _ => continue, }); } res } } fn ty_of_lineage(lineage: &pl::Lineage) -> Ty { Ty::relation( lineage .columns .iter() .map(|col| match col { pl::LineageColumn::All { .. } => TyTupleField::Wildcard(None), pl::LineageColumn::Single { name, .. } => { TyTupleField::Single(name.as_ref().map(|i| i.name.clone()), None) } }) .collect(), ) } ================================================ FILE: prqlc/prqlc/src/semantic/resolver/flatten.rs ================================================ use std::collections::HashMap; use crate::ir::pl::{ fold_column_sorts, fold_transform_kind, ColumnSort, Expr, ExprKind, PlFold, TransformCall, TransformKind, WindowFrame, }; use crate::Result; /// Flattens group and window [TransformCall]s into a single pipeline. /// Sets partition, window and sort of [TransformCall]. #[derive(Default, Debug)] pub struct Flattener { /// Sort affects downstream transforms in a pipeline. /// Because transform pipelines are represented by nested [TransformCall]s, /// affected transforms are all ancestor nodes of sort [TransformCall]. /// This means that this field has to be set after folding inner table, /// so it's passed to parent call of `fold_transform_call` sort: Vec, sort_undone: bool, /// Group affects transforms in it's inner pipeline. /// This means that this field has to be set before folding inner pipeline, /// and unset after the folding. partition: Option>, /// Window affects transforms in it's inner pipeline. /// This means that this field has to be set before folding inner pipeline, /// and unset after the folding. window: WindowFrame, /// Window and group contain Closures in their inner pipelines. /// These closures have form similar to this function: /// ```prql /// let closure = tbl_chunk -> (derive ... (sort ... (tbl_chunk))) /// ``` /// To flatten a window or group, we need to replace group/window transform /// with their closure's body and replace `tbl_chunk` with pipeline /// preceding the group/window transform. /// /// That's what `replace_map` is for. replace_map: HashMap, } impl Flattener { pub fn fold(expr: Expr) -> Expr { let mut f = Flattener::default(); f.fold_expr(expr).unwrap() } } impl PlFold for Flattener { fn fold_expr(&mut self, mut expr: Expr) -> Result { if let Some(target) = &expr.target_id { if let Some(replacement) = self.replace_map.remove(target) { return Ok(replacement); } } expr.kind = match expr.kind { ExprKind::TransformCall(t) => { log::debug!("flattening {}", (*t.kind).as_ref()); let (input, kind) = match *t.kind { TransformKind::Sort { by } => { // fold let by = fold_column_sorts(self, by)?; let input = self.fold_expr(*t.input)?; self.sort.clone_from(&by); if self.sort_undone { return Ok(input); } else { (input, TransformKind::Sort { by }) } } TransformKind::Group { by, pipeline } => { let sort_undone = self.sort_undone; // Only mark sort as undone if there's an actual partition. // Empty group {} should preserve sort (fixes #5100). if !matches!(by.kind, ExprKind::Tuple(ref fields) if fields.is_empty()) { self.sort_undone = true; } let input = self.fold_expr(*t.input)?; let pipeline = pipeline.kind.into_func().unwrap(); let table_param = &pipeline.params[0]; let param_id = table_param.name.parse::().unwrap(); self.replace_map.insert(param_id, input); self.partition = Some(by); self.sort.clear(); let pipeline = self.fold_expr(*pipeline.body)?; self.replace_map.remove(¶m_id); self.partition = None; self.sort.clear(); self.sort_undone = sort_undone; // If the pipeline simplified to a non-TransformCall (e.g., sort was // dropped), use the pipeline's lineage since the original GROUP lineage // may reference expressions that no longer exist in the tree. // Otherwise, preserve the GROUP's lineage which includes the `by` columns. let lineage = if matches!(pipeline.kind, ExprKind::TransformCall(_)) { expr.lineage } else { pipeline.lineage }; return Ok(Expr { ty: expr.ty, lineage, ..pipeline }); } TransformKind::Window { kind, range, pipeline, } => { let tbl = self.fold_expr(*t.input)?; let pipeline = pipeline.kind.into_func().unwrap(); let table_param = &pipeline.params[0]; let param_id = table_param.name.parse::().unwrap(); self.replace_map.insert(param_id, tbl); self.window = WindowFrame { kind, range }; let pipeline = self.fold_expr(*pipeline.body)?; self.window = WindowFrame::default(); self.replace_map.remove(¶m_id); return Ok(Expr { ty: expr.ty, lineage: expr.lineage, ..pipeline }); } kind => (self.fold_expr(*t.input)?, fold_transform_kind(self, kind)?), }; // In case we're appending or joining another pipeline, we do not want to apply the // sub-pipeline's sort, as it may result in column lookup errors. Without this, we // would try to join on `album_id` in the outer pipeline of the following query, but // the column does not exist // // from artists // join side:left ( // from albums // sort {`album_id`} // derive {`album_name` = `name`} // select {`artist_id`, `album_name`} // ) (this.id == that.artist_id) let sort = if matches!(kind, TransformKind::Join { .. } | TransformKind::Append(_)) { vec![] } else { self.sort.clone() }; ExprKind::TransformCall(TransformCall { input: Box::new(input), kind: Box::new(kind), partition: self.partition.clone(), frame: self.window.clone(), sort, }) } kind => self.fold_expr_kind(kind)?, }; Ok(expr) } } ================================================ FILE: prqlc/prqlc/src/semantic/resolver/functions.rs ================================================ use std::collections::HashMap; use std::iter::zip; use itertools::{Itertools, Position}; use super::Resolver; use crate::ir::decl::{Decl, DeclKind, Module}; use crate::ir::pl::*; use crate::pr::{Ty, TyFunc}; use crate::semantic::resolver::types; use crate::semantic::{NS_PARAM, NS_THAT, NS_THIS}; use crate::Result; use crate::{Error, Reason, Span, WithErrorInfo}; impl Resolver<'_> { pub fn fold_function(&mut self, closure: Box, span: Option) -> Result { let closure = self.fold_function_types(closure)?; log::debug!( "func {} {}/{} params", closure.as_debug_name(), closure.args.len(), closure.params.len() ); if closure.args.len() > closure.params.len() { return Err(Error::new_simple(format!( "Too many arguments to function `{}`", closure.as_debug_name() )) .with_span(span)); } let enough_args = closure.args.len() == closure.params.len(); if !enough_args { return Ok(*expr_of_func(closure, span)); } // make sure named args are pushed into params let closure = if !closure.named_params.is_empty() { self.apply_args_to_closure(closure, [].into(), [].into())? } else { closure }; // push the env let closure_env = Module::from_exprs(closure.env); self.root_mod.module.stack_push(NS_PARAM, closure_env); let closure = Box::new(Func { env: HashMap::new(), ..*closure }); if log::log_enabled!(log::Level::Debug) { let name = closure .name_hint .clone() .unwrap_or_else(|| Ident::from_name("")); log::debug!("resolving args of function {name}"); } let res = self.resolve_function_args(closure)?; let closure = match res { Ok(func) => func, Err(func) => { return Ok(*expr_of_func(func, span)); } }; let needs_window = (closure.params.last()) .and_then(|p| p.ty.as_ref()) .map(types::is_sub_type_of_array) .unwrap_or_default(); // evaluate let res = if let ExprKind::Internal(operator_name) = &closure.body.kind { // special case: functions that have internal body if operator_name.starts_with("std.") { Expr { ty: closure.return_ty, needs_window, ..Expr::new(ExprKind::RqOperator { name: operator_name.clone(), args: closure.args, }) } } else { let expr = self.resolve_special_func(closure, needs_window)?; self.fold_expr(expr)? } } else { // base case: materialize self.materialize_function(closure)? }; // pop the env self.root_mod.module.stack_pop(NS_PARAM).unwrap(); Ok(Expr { span, ..res }) } #[allow(clippy::boxed_local)] fn materialize_function(&mut self, closure: Box) -> Result { log::debug!("stack_push for {}", closure.as_debug_name()); let (func_env, body, return_ty) = env_of_closure(*closure); self.root_mod.module.stack_push(NS_PARAM, func_env); // fold again, to resolve inner variables & functions let body = self.fold_expr(body)?; // remove param decls log::debug!("stack_pop: {:?}", body.id); let func_env = self.root_mod.module.stack_pop(NS_PARAM).unwrap(); Ok(if let ExprKind::Func(mut inner_closure) = body.kind { // body couldn't been resolved - construct a closure to be evaluated later inner_closure.env = func_env.into_exprs(); // Get the missing params (params that don't have args yet) let missing = inner_closure.params[inner_closure.args.len()..].to_vec(); // Create wrapper params and add references to them as args to the inner closure let mut wrapper_params = Vec::with_capacity(missing.len()); for (i, param) in missing.iter().enumerate() { let param_name = format!("_partial_{i}"); let substitute_arg = Expr::new(Ident::from_path(vec![ NS_PARAM.to_string(), param_name.clone(), ])); inner_closure.args.push(substitute_arg); wrapper_params.push(FuncParam { name: param_name, ty: param.ty.clone(), default_value: None, }); } Expr::new(ExprKind::Func(Box::new(Func { name_hint: None, args: vec![], params: wrapper_params, body: Box::new(Expr::new(ExprKind::Func(inner_closure))), // these don't matter named_params: Default::default(), return_ty: Default::default(), env: Default::default(), }))) } else { // resolved, return result // make sure to use the resolved type let mut body = body; if let Some(ret_ty) = return_ty.map(|x| *x) { body.ty = Some(ret_ty.clone()); } body }) } /// Folds function types, so they are resolved to material types, ready for type checking. pub fn fold_function_types(&mut self, mut func: Box) -> Result> { func.params = func .params .into_iter() .map(|p| -> Result<_> { Ok(FuncParam { ty: fold_type_opt(self, p.ty)?, ..p }) }) .try_collect()?; func.return_ty = fold_type_opt(self, func.return_ty)?; Ok(func) } pub fn apply_args_to_closure( &mut self, mut closure: Box, args: Vec, mut named_args: HashMap, ) -> Result> { // named arguments are consumed only by the first function // named for mut param in closure.named_params.drain(..) { let param_name = param.name.split('.').next_back().unwrap_or(¶m.name); let default = param.default_value.take().unwrap(); let arg = named_args.remove(param_name).unwrap_or(*default); closure.args.push(arg); closure.params.insert(closure.args.len() - 1, param); } if let Some((name, _)) = named_args.into_iter().next() { // TODO: report all remaining named_args as separate errors return Err(Error::new_simple(format!( "unknown named argument `{name}` to closure {:?}", closure.name_hint ))); } // positional closure.args.extend(args); Ok(closure) } /// Resolves function arguments. Will return `Err(func)` is partial application is required. fn resolve_function_args( &mut self, #[allow(clippy::boxed_local)] to_resolve: Box, ) -> Result, Box>> { let mut closure = Box::new(Func { args: vec![Expr::new(Literal::Null); to_resolve.args.len()], ..*to_resolve }); let mut partial_application_position = None; let func_name = &closure.name_hint; let (relations, other): (Vec<_>, Vec<_>) = zip(&closure.params, to_resolve.args) .enumerate() .partition(|(_, (param, _))| { let is_relation = param .ty .as_ref() .map(|t| t.is_relation()) .unwrap_or_default(); is_relation }); let has_relations = !relations.is_empty(); // resolve relational args if has_relations { self.root_mod.module.shadow(NS_THIS); self.root_mod.module.shadow(NS_THAT); // First, resolve all relational arguments let mut resolved_relations = Vec::new(); for (pos, (index, (param, mut arg))) in relations.into_iter().with_position() { let is_last = matches!(pos, Position::Last | Position::Only); // just fold the argument alone if partial_application_position.is_none() { arg = self .fold_and_type_check(arg, param, func_name)? .unwrap_or_else(|a| { partial_application_position = Some(index); a }); } log::debug!("resolved arg to {}", arg.kind.as_ref()); resolved_relations.push((index, arg, is_last)); } // Then, add relation frames into scope for (index, arg, is_last) in resolved_relations { if partial_application_position.is_none() { let frame = arg.lineage.as_ref().ok_or_else(|| { // Provide helpful error for empty arrays/tuples used directly // (not from functions like std.from_text which set lineage properly) match &arg.kind { ExprKind::Array(v) if v.is_empty() => Error::new(Reason::Expected { who: None, expected: "a table or query".to_string(), found: "an empty array `[]`".to_string(), }) .with_span(arg.span), ExprKind::Tuple(v) if v.is_empty() => Error::new(Reason::Expected { who: None, expected: "a table or query".to_string(), found: "an empty tuple `{}`".to_string(), }) .with_span(arg.span), _ => Error::new_bug(4317).with_span(closure.body.span), } })?; if is_last { self.root_mod.module.insert_frame(frame, NS_THIS); } else { self.root_mod.module.insert_frame(frame, NS_THAT); } } closure.args[index] = arg; } } // resolve other positional for (index, (param, mut arg)) in other { if partial_application_position.is_none() { if let ExprKind::Tuple(fields) = arg.kind { // if this is a tuple, resolve elements separately, // so they can be added to scope, before resolving subsequent elements. let mut fields_new = Vec::with_capacity(fields.len()); for field in fields { let field = self.fold_within_namespace(field, ¶m.name)?; // add aliased columns into scope if let Some(alias) = field.alias.clone() { let id = field.id.unwrap(); self.root_mod.module.insert_frame_col(NS_THIS, alias, id); } fields_new.push(field); } // note that this tuple node has to be resolved itself // (it's elements are already resolved and so their resolving // should be skipped) arg.kind = ExprKind::Tuple(fields_new); } arg = self .fold_and_type_check(arg, param, func_name)? .unwrap_or_else(|a| { partial_application_position = Some(index); a }); } closure.args[index] = arg; } if has_relations { self.root_mod.module.unshadow(NS_THIS); self.root_mod.module.unshadow(NS_THAT); } Ok(if let Some(position) = partial_application_position { log::debug!( "partial application of {} at arg {position}", closure.as_debug_name() ); Err(extract_partial_application(closure, position)) } else { Ok(closure) }) } fn fold_and_type_check( &mut self, arg: Expr, param: &FuncParam, func_name: &Option, ) -> Result> { let mut arg = self.fold_within_namespace(arg, ¶m.name)?; // don't validate types of unresolved exprs if arg.id.is_some() { // validate type let expects_func = param .ty .as_ref() .map(|t| t.kind.is_function()) .unwrap_or_default(); if !expects_func && arg.kind.is_func() { return Ok(Err(arg)); } let who = || { func_name .as_ref() .map(|n| format!("function {n}, param `{}`", param.name)) }; self.validate_expr_type(&mut arg, param.ty.as_ref(), &who)?; } Ok(Ok(arg)) } fn fold_within_namespace(&mut self, expr: Expr, param_name: &str) -> Result { let prev_namespace = self.default_namespace.take(); if param_name.starts_with("noresolve.") { return Ok(expr); } else if let Some((ns, _)) = param_name.split_once('.') { self.default_namespace = Some(ns.to_string()); } else { self.default_namespace = None; }; let res = self.fold_expr(expr); self.default_namespace = prev_namespace; res } } fn extract_partial_application(mut func: Box, position: usize) -> Box { // Input: // Func { // params: [x, y, z], // args: [ // x, // Func { // params: [a, b], // args: [a], // body: arg_body // }, // z // ], // body: parent_body // } // Output: // Func { // params: [b], // args: [], // body: Func { // params: [x, y, z], // args: [ // x, // Func { // params: [a, b], // args: [a, b], // body: arg_body // }, // z // ], // body: parent_body // } // } // This is quite in-efficient, especially for long pipelines. // Maybe it could be special-cased, for when the arg func has a single param. // In that case, it may be possible to pull the arg func up and basically swap // it with the parent func. let arg = func.args.get_mut(position).unwrap(); let arg_func = arg.kind.as_func_mut().unwrap(); let param_name = format!("_partial_{}", arg.id.unwrap()); let substitute_arg = Expr::new(Ident::from_path(vec![ NS_PARAM.to_string(), param_name.clone(), ])); arg_func.args.push(substitute_arg); // set the arg func body to the parent func Box::new(Func { name_hint: None, return_ty: None, body: Box::new(Expr::new(ExprKind::Func(func))), params: vec![FuncParam { name: param_name, ty: None, default_value: None, }], named_params: Default::default(), args: Default::default(), env: Default::default(), }) } fn env_of_closure(closure: Func) -> (Module, Expr, Option>) { let mut func_env = Module::default(); for (param, arg) in zip(closure.params, closure.args) { let v = Decl { declared_at: arg.id, kind: DeclKind::Expr(Box::new(arg)), ..Default::default() }; let param_name = param.name.split('.').next_back().unwrap(); func_env.names.insert(param_name.to_string(), v); } (func_env, *closure.body, closure.return_ty.map(Box::new)) } pub fn expr_of_func(func: Box, span: Option) -> Box { let ty = TyFunc { params: func .params .iter() .skip(func.args.len()) .map(|a| a.ty.clone()) .collect(), return_ty: func .return_ty .clone() .or_else(|| func.clone().body.ty) .map(Box::new), name_hint: func.name_hint.clone(), }; Box::new(Expr { ty: Some(Ty::new(ty)), span, ..Expr::new(ExprKind::Func(func)) }) } ================================================ FILE: prqlc/prqlc/src/semantic/resolver/inference.rs ================================================ use itertools::Itertools; use super::Resolver; use crate::ir::decl::{Decl, TableDecl, TableExpr}; use crate::ir::pl::{Lineage, LineageColumn, LineageInput}; use crate::pr::{Ident, Ty, TyTupleField}; use crate::semantic::{NS_DEFAULT_DB, NS_INFER}; use crate::Result; impl Resolver<'_> { pub fn infer_table_column( &mut self, table_ident: &Ident, col_name: &str, ) -> Result<(), String> { let table = self.root_mod.module.get_mut(table_ident).unwrap(); let table_decl = table.kind.as_table_decl_mut().unwrap(); let Some(columns) = table_decl.ty.as_mut().and_then(|t| t.as_relation_mut()) else { return Err(format!("Variable {table_ident:?} is not a relation.")); }; let has_wildcard = columns .iter() .any(|c| matches!(c, TyTupleField::Wildcard(_))); if !has_wildcard { return Err(format!("Table {table_ident:?} does not have wildcard.")); } let exists = columns.iter().any(|c| match c { TyTupleField::Single(Some(n), _) => n == col_name, _ => false, }); if exists { return Ok(()); } columns.push(TyTupleField::Single(Some(col_name.to_string()), None)); // also add into input tables of this table expression if let TableExpr::RelationVar(expr) = &table_decl.expr { if let Some(frame) = &expr.lineage { let wildcard_inputs = (frame.columns.iter()) .filter_map(|c| c.as_all()) .collect_vec(); match wildcard_inputs.len() { 0 => return Err(format!("Cannot infer where {table_ident}.{col_name} is from")), 1 => { let (input_id, _) = wildcard_inputs.into_iter().next().unwrap(); // input_id comes from LineageColumn::All in frame.columns. // Should be valid, but if this panics, see #5280 and lowering.rs // for the pattern where columns reference out-of-scope inputs. let input = frame.find_input(*input_id).unwrap(); let table_ident = input.table.clone(); self.infer_table_column(&table_ident, col_name)?; } _ => { return Err(format!("Cannot infer where {table_ident}.{col_name} is from. It could be any of {wildcard_inputs:?}")) } } } } Ok(()) } /// Converts a identifier that points to a table declaration to lineage of that table. pub fn lineage_of_table_decl( &mut self, table_fq: &Ident, input_name: String, input_id: usize, ) -> Lineage { let table_decl = self.root_mod.module.get(table_fq).unwrap(); let TableDecl { ty, expr } = table_decl.kind.as_table_decl().unwrap(); // For CTEs (RelationVar), trace lineage back to the underlying source tables. // For UNIONs and JOINs, this includes all underlying source tables. let underlying_inputs = match expr { TableExpr::RelationVar(rel) => rel.lineage.as_ref().map(|l| &l.inputs), _ => None, }; let inputs = match underlying_inputs { Some(inputs) if !inputs.is_empty() => inputs .iter() .map(|inp| LineageInput { id: input_id, name: input_name.clone(), table: inp.table.clone(), }) .collect(), _ => vec![LineageInput { id: input_id, name: input_name.clone(), table: table_fq.clone(), }], }; // TODO: can this panic? let columns = ty.as_ref().unwrap().as_relation().unwrap(); let mut instance_frame = Lineage { inputs, columns: Vec::new(), ..Default::default() }; for col in columns { let col = match col { TyTupleField::Wildcard(_) => LineageColumn::All { input_id, except: columns .iter() .flat_map(|c| c.as_single().map(|x| x.0).cloned().flatten()) .collect(), }, TyTupleField::Single(col_name, _) => LineageColumn::Single { name: col_name .clone() .map(|col_name| Ident::from_path(vec![input_name.clone(), col_name])), target_id: input_id, target_name: col_name.clone(), }, }; instance_frame.columns.push(col); } log::debug!("instanced table {table_fq} as {instance_frame:?}"); instance_frame } /// Declares a new table for a relation literal. /// This is needed for column inference to work properly. pub(super) fn declare_table_for_literal( &mut self, input_id: usize, columns: Option>, name_hint: Option, ) -> Lineage { log::debug!("declare_table_for_literal: {input_id:?} {columns:?} {name_hint:?}"); let id = input_id; let global_name = format!("_literal_{id}"); // declare a new table in the `default_db` module let default_db_ident = Ident::from_name(NS_DEFAULT_DB); let default_db = self.root_mod.module.get_mut(&default_db_ident).unwrap(); let default_db = default_db.kind.as_module_mut().unwrap(); let infer_default = default_db.get(&Ident::from_name(NS_INFER)).unwrap().clone(); let mut infer_default = *infer_default.kind.into_infer().unwrap(); let table_decl = infer_default.as_table_decl_mut().unwrap(); table_decl.expr = TableExpr::None; if let Some(columns) = columns { table_decl.ty = Some(Ty::relation(columns)); } default_db .names .insert(global_name.clone(), Decl::from(infer_default)); // produce a frame of that table let input_name = name_hint.unwrap_or_else(|| global_name.clone()); let table_fq = default_db_ident + Ident::from_name(global_name); self.lineage_of_table_decl(&table_fq, input_name, id) } } ================================================ FILE: prqlc/prqlc/src/semantic/resolver/mod.rs ================================================ use crate::ir::decl::RootModule; use crate::utils::IdGenerator; mod expr; mod flatten; mod functions; mod inference; mod names; mod static_eval; mod stmt; mod transforms; mod types; /// Can fold (walk) over AST and for each function call or variable find what they are referencing. pub struct Resolver<'a> { root_mod: &'a mut RootModule, current_module_path: Vec, default_namespace: Option, /// Sometimes ident closures must be resolved and sometimes not. See [test::test_func_call_resolve]. in_func_call_name: bool, pub id: IdGenerator, } #[derive(Default, Clone)] pub struct ResolverOptions {} impl Resolver<'_> { pub fn new(root_mod: &mut RootModule) -> Resolver<'_> { Resolver { root_mod, current_module_path: Vec::new(), default_namespace: None, in_func_call_name: false, id: IdGenerator::new(), } } } #[cfg(test)] pub(super) mod test { use insta::assert_yaml_snapshot; use crate::ir::pl::{Expr, Lineage, PlFold}; use crate::{Errors, Result}; pub fn erase_ids(expr: Expr) -> Expr { IdEraser {}.fold_expr(expr).unwrap() } struct IdEraser {} impl PlFold for IdEraser { fn fold_expr(&mut self, mut expr: Expr) -> Result { expr.kind = self.fold_expr_kind(expr.kind)?; expr.id = None; expr.target_id = None; Ok(expr) } } fn parse_and_resolve(query: &str) -> Result { let ctx = crate::semantic::test::parse_and_resolve(query)?; let (main, _) = ctx.find_main_rel(&[]).unwrap(); Ok(*main.clone().into_relation_var().unwrap()) } fn resolve_lineage(query: &str) -> Result { Ok(parse_and_resolve(query)?.lineage.unwrap()) } fn resolve_derive(query: &str) -> Result, Errors> { let expr = parse_and_resolve(query)?; let derive = expr.kind.into_transform_call().unwrap(); let exprs = derive .kind .into_derive() .unwrap_or_else(|e| panic!("Failed to convert `{e:?}`")) .kind .into_tuple() .unwrap_or_else(|e| panic!("Failed to convert `{e:?}`")); let exprs = IdEraser {}.fold_exprs(exprs).unwrap(); Ok(exprs) } #[test] fn test_variables_1() { assert_yaml_snapshot!(resolve_derive( r#" from employees derive { gross_salary = salary + payroll_tax, gross_cost = gross_salary + benefits_cost } "# ) .unwrap()); } #[test] #[ignore] fn test_non_existent_function() { // `myfunc` is a valid reference to a column and // a column can be a function, right? // If not, how would we express that with type system? parse_and_resolve(r#"from mytable | filter (myfunc col1)"#).unwrap_err(); } #[test] fn test_functions_1() { assert_yaml_snapshot!(resolve_derive( r#" let subtract = a b -> a - b from employees derive { net_salary = subtract gross_salary tax } "# ) .unwrap()); } #[test] fn test_functions_nested() { assert_yaml_snapshot!(resolve_derive( r#" let lag_day = x -> s"lag_day_todo({x})" let ret = x dividend_return -> x / (lag_day x) - 1 + dividend_return from a derive (ret b c) "# ) .unwrap()); } #[test] fn test_functions_pipeline() { assert_yaml_snapshot!(resolve_derive( r#" from a derive one = (foo | sum) "# ) .unwrap()); assert_yaml_snapshot!(resolve_derive( r#" let plus_one = x -> x + 1 let plus = x y -> x + y from a derive {b = (sum foo | plus_one | plus 2)} "# ) .unwrap()); } #[test] fn test_named_args() { assert_yaml_snapshot!(resolve_derive( r#" let add_one = x to:1 -> x + to from foo_table derive { added = add_one bar to:3, added_default = add_one bar } "# ) .unwrap()); } #[test] fn test_frames_and_names() { assert_yaml_snapshot!(resolve_lineage( r#" from orders select {customer_no, gross, tax, gross - tax} take 20 "# ) .unwrap()); assert_yaml_snapshot!(resolve_lineage( r#" from table_1 join customers (==customer_no) "# ) .unwrap()); assert_yaml_snapshot!(resolve_lineage( r#" from e = employees join salaries (==emp_no) group {e.emp_no, e.gender} ( aggregate { emp_salary = average salaries.salary } ) "# ) .unwrap()); } // Helper function to verify basic lineage structure after append fn verify_append_lineage_basics( final_lineage: &crate::ir::pl::Lineage, expected_inputs: &[&str], ) { let input_names: Vec<&str> = final_lineage .inputs .iter() .map(|i| i.name.as_str()) .collect(); for expected_input in expected_inputs { assert!(input_names.contains(expected_input)); assert!(final_lineage.find_input_by_name(expected_input).is_some()); } assert!(!final_lineage.columns.is_empty()); for col in &final_lineage.columns { match col { crate::ir::pl::LineageColumn::Single { name, target_id, .. } => { assert!(target_id > &0); assert!(name.is_some()); } crate::ir::pl::LineageColumn::All { .. } => {} } } } // Helper function to find source frames by input name fn find_source_frames<'a>( fc: &'a crate::semantic::reporting::FrameCollector, top_input_name: &str, bottom_input_name: &str, ) -> ( Option<&'a crate::ir::pl::Lineage>, Option<&'a crate::ir::pl::Lineage>, ) { let mut top_frame = None; let mut bottom_frame = None; for (_span, frame) in &fc.frames { if frame.inputs.len() == 1 { let input_name = &frame.inputs[0].name; if input_name == top_input_name && top_frame.is_none() { top_frame = Some(frame); } else if input_name == bottom_input_name && bottom_frame.is_none() { bottom_frame = Some(frame); } } } (top_frame, bottom_frame) } // Helper function to verify column-level lineage for Single columns fn verify_single_column_lineage( final_lineage: &crate::ir::pl::Lineage, fc: &crate::semantic::reporting::FrameCollector, top_frame: &crate::ir::pl::Lineage, bottom_frame: &crate::ir::pl::Lineage, ) { assert_eq!(final_lineage.columns.len(), top_frame.columns.len()); assert_eq!(final_lineage.columns.len(), bottom_frame.columns.len()); for ((union_col, top_col), bottom_col) in final_lineage .columns .iter() .zip(top_frame.columns.iter()) .zip(bottom_frame.columns.iter()) { if let ( crate::ir::pl::LineageColumn::Single { .. }, crate::ir::pl::LineageColumn::Single { name: top_name, target_id: top_target_id, .. }, crate::ir::pl::LineageColumn::Single { name: bottom_name, target_id: bottom_target_id, .. }, ) = (union_col, top_col, bottom_col) { if let (Some(top_name), Some(bottom_name)) = (top_name, bottom_name) { assert_eq!(top_name.name, bottom_name.name); } assert!(fc.nodes.iter().any(|n| n.id == *top_target_id)); assert!(fc.nodes.iter().any(|n| n.id == *bottom_target_id)); } } for col in &final_lineage.columns { if let crate::ir::pl::LineageColumn::Single { target_id, .. } = col { assert!(fc.nodes.iter().any(|n| n.id == *target_id)); } } } // Helper function to verify expression graph contains all expected nodes fn verify_expression_graph_nodes( fc: &crate::semantic::reporting::FrameCollector, final_lineage: &crate::ir::pl::Lineage, top_frame: &crate::ir::pl::Lineage, bottom_frame: &crate::ir::pl::Lineage, ) { for input in &final_lineage.inputs { assert!(fc.nodes.iter().any(|n| n.id == input.id)); } let top_col_target_ids: Vec = top_frame .columns .iter() .filter_map(|c| match c { crate::ir::pl::LineageColumn::Single { target_id, .. } => Some(*target_id), _ => None, }) .collect(); let bottom_col_target_ids: Vec = bottom_frame .columns .iter() .filter_map(|c| match c { crate::ir::pl::LineageColumn::Single { target_id, .. } => Some(*target_id), _ => None, }) .collect(); for target_id in &top_col_target_ids { assert!(fc.nodes.iter().any(|n| n.id == *target_id)); } for target_id in &bottom_col_target_ids { assert!(fc.nodes.iter().any(|n| n.id == *target_id)); } } #[test] fn test_append_union_different_tables() { // This test verifies that lineage tracking for append/union operations // correctly tracks inputs from both tables and shows column-level lineage. use crate::internal::pl_to_lineage; let query = r#" from employees select { name, salary } append ( from managers select { name, salary } ) "#; let pl = crate::prql_to_pl(query).unwrap(); let fc = pl_to_lineage(pl).unwrap(); let final_lineage = &fc.frames.last().unwrap().1; assert_yaml_snapshot!(final_lineage); verify_append_lineage_basics(final_lineage, &["employees", "managers"]); let (top_frame, bottom_frame) = find_source_frames(&fc, "employees", "managers"); let top_frame = top_frame.unwrap(); let bottom_frame = bottom_frame.unwrap(); verify_single_column_lineage(final_lineage, &fc, top_frame, bottom_frame); let employees_input = final_lineage.find_input_by_name("employees").unwrap(); let managers_input = final_lineage.find_input_by_name("managers").unwrap(); assert!(final_lineage .inputs .iter() .any(|inp| inp.id == employees_input.id)); assert!(final_lineage .inputs .iter() .any(|inp| inp.id == managers_input.id)); verify_expression_graph_nodes(&fc, final_lineage, top_frame, bottom_frame); } #[test] fn test_append_union_same_table_with_exclude() { // This test attempts to exercise the All columns path by unioning // the same table with itself using select with exclude. use crate::internal::pl_to_lineage; let query = r#" from employees select !{name} append ( from employees select !{salary} ) "#; let pl = crate::prql_to_pl(query).unwrap(); let fc = pl_to_lineage(pl).unwrap(); let final_lineage = &fc.frames.last().unwrap().1; verify_append_lineage_basics(final_lineage, &["employees"]); } #[test] fn test_append_union_all_columns_same_input() { // This test exercises the All columns path with same input_id (lines 765-766) // to ensure code coverage for merging except sets when both All columns // come from the same input. use crate::ir::pl::{ Expr, ExprKind, Lineage, LineageColumn, LineageInput, TransformCall, TransformKind, }; use std::collections::HashSet; let input = LineageInput { id: 100, name: "employees".to_string(), table: crate::ir::pl::Ident { path: vec!["default_db".to_string()], name: "employees".to_string(), }, }; let mut top_lineage = Lineage::default(); top_lineage.inputs.push(input.clone()); top_lineage.columns.push(LineageColumn::All { input_id: 100, except: { let mut set = HashSet::new(); set.insert("name".to_string()); set }, }); let mut bottom_lineage = Lineage::default(); bottom_lineage.inputs.push(input.clone()); bottom_lineage.columns.push(LineageColumn::All { input_id: 100, except: { let mut set = HashSet::new(); set.insert("salary".to_string()); set }, }); let mut top_expr = Expr::new(ExprKind::Ident(crate::ir::pl::Ident::from_name("top"))); top_expr.lineage = Some(top_lineage); let mut bottom_expr = Expr::new(ExprKind::Ident(crate::ir::pl::Ident::from_name("bottom"))); bottom_expr.lineage = Some(bottom_lineage); let transform_call = TransformCall { kind: Box::new(TransformKind::Append(Box::new(bottom_expr))), input: Box::new(top_expr), partition: None, frame: crate::ir::pl::WindowFrame::default(), sort: Vec::new(), }; let result = transform_call.infer_lineage().unwrap(); match &result.columns[0] { LineageColumn::All { input_id, except } => { assert_eq!(*input_id, 100); assert!(except.contains("name")); assert!(except.contains("salary")); } _ => panic!("Expected All column"), } } #[test] fn test_cte_lineage_traces_to_source_table() { // This test verifies that simple CTEs trace lineage back to // the underlying source table instead of showing the CTE name. use crate::internal::pl_to_lineage; let query = r#" let employees_usa = (from employees | filter country == "USA") from employees_usa select {name, salary} "#; let pl = crate::prql_to_pl(query).unwrap(); let fc = pl_to_lineage(pl).unwrap(); let final_lineage = &fc.frames.last().unwrap().1; assert_eq!( final_lineage.inputs.len(), 1, "Simple CTE should have 1 input, got {:?}", final_lineage.inputs ); let input = &final_lineage.inputs[0]; assert_eq!( input.name, "employees_usa", "Input name should be the CTE alias" ); assert_eq!( input.table.name, "employees", "Table should trace back to source table 'employees', got {:?}", input.table ); } #[test] fn test_direct_table_lineage_uses_table_itself() { // This test verifies that direct table references (non-CTEs) // use the table itself as the lineage input, exercising the // fallback path in lineage_of_table_decl. use crate::internal::pl_to_lineage; let query = r#" from employees select {name, salary} "#; let pl = crate::prql_to_pl(query).unwrap(); let fc = pl_to_lineage(pl).unwrap(); let final_lineage = &fc.frames.last().unwrap().1; assert_eq!( final_lineage.inputs.len(), 1, "Direct table should have 1 input" ); let input = &final_lineage.inputs[0]; assert_eq!( input.table.name, "employees", "Table should be 'employees' directly" ); } #[test] fn test_cte_lineage_with_union_traces_to_all_source_tables() { // This test verifies that CTEs with UNIONs trace lineage // back to ALL underlying source tables. use crate::internal::pl_to_lineage; let query = r#" let combined = ( from employees select {name, dept} append ( from contractors select {name, dept} ) ) from combined select {name} "#; let pl = crate::prql_to_pl(query).unwrap(); let fc = pl_to_lineage(pl).unwrap(); let final_lineage = &fc.frames.last().unwrap().1; // Should have inputs from both employees and contractors assert_eq!( final_lineage.inputs.len(), 2, "CTE with UNION should have 2 inputs, got {:?}", final_lineage.inputs ); let tables: Vec<_> = final_lineage .inputs .iter() .map(|inp| inp.table.name.as_str()) .collect(); assert!( tables.contains(&"employees"), "Should contain employees table, got {:?}", tables ); assert!( tables.contains(&"contractors"), "Should contain contractors table, got {:?}", tables ); } } ================================================ FILE: prqlc/prqlc/src/semantic/resolver/names.rs ================================================ use std::collections::HashSet; use itertools::Itertools; use super::Resolver; use crate::ir::decl::{Decl, DeclKind, Module}; use crate::ir::pl::{Expr, ExprKind}; use crate::pr::Ident; use crate::semantic::{NS_INFER, NS_INFER_MODULE, NS_SELF, NS_THAT, NS_THIS}; use crate::Error; use crate::Result; use crate::WithErrorInfo; impl Resolver<'_> { pub(super) fn resolve_ident(&mut self, ident: &Ident) -> Result { let mut res = if let Some(default_namespace) = self.default_namespace.clone() { self.resolve_ident_core(ident, Some(&default_namespace)) } else { let mut ident = ident.clone().prepend(self.current_module_path.clone()); let mut res = self.resolve_ident_core(&ident, None); for _ in 0..self.current_module_path.len() { if res.is_ok() { break; } ident = ident.pop_front().1.unwrap(); res = self.resolve_ident_core(&ident, None); } res }; match &res { Ok(fq_ident) => { let decl = self.root_mod.module.get(fq_ident).unwrap(); if let DeclKind::Import(target) = &decl.kind { let target = target.clone(); return self.resolve_ident(&target); } } Err(e) => { log::debug!( "cannot resolve `{ident}`: `{e:?}`, root_mod={:#?}", self.root_mod ); // attach available names let mut available_names = Vec::new(); available_names.extend(self.collect_columns_in_module(NS_THIS)); available_names.extend(self.collect_columns_in_module(NS_THAT)); if !available_names.is_empty() { let available_names = available_names.iter().map(Ident::to_string).join(", "); res = res.push_hint(format!("available columns: {available_names}")); } } } res } fn collect_columns_in_module(&mut self, mod_name: &str) -> Vec { let mut cols = Vec::new(); let Some(module) = self.root_mod.module.names.get(mod_name) else { return cols; }; let DeclKind::Module(this) = &module.kind else { return cols; }; for (ident, decl) in this.as_decls().into_iter().sorted_by_key(|x| x.1.order) { if let DeclKind::Column(_) = decl.kind { cols.push(ident); } } cols } pub(super) fn resolve_ident_core( &mut self, ident: &Ident, default_namespace: Option<&String>, ) -> Result { // special case: wildcard if ident.name == "*" { // TODO: we may want to raise an error if someone has passed `download*` in // an attempt to query for all `download` columns and expects to be able // to select a `download_2020_01_01` column later in the query. But // sometimes we want to query for `*.parquet` files, and give them an // alias. So we don't raise an error here, but if there's a way of // differentiating the cases, we can implement that. // if ident.name != "*" { // return Err("Unsupported feature: advanced wildcard column matching".to_string()); // } // For bare `*` (no prefix), prepend the default namespace so it // resolves like `this.*` within the current context. let wildcard_ident = match (ident.path.is_empty(), default_namespace) { (true, Some(ns)) => ident.clone().prepend(vec![ns.clone()]), _ => ident.clone(), }; return self .resolve_ident_wildcard(&wildcard_ident) .map_err(Error::new_simple); } // base case: direct lookup let decls = self.root_mod.module.lookup(ident); match decls.len() { // no match: try match * 0 => {} // single match, great! 1 => return Ok(decls.into_iter().next().unwrap()), // ambiguous _ => return Err(ambiguous_error(decls, None)), } let ident = if let Some(default_namespace) = default_namespace { let ident = ident.clone().prepend(vec![default_namespace.clone()]); let decls = self.root_mod.module.lookup(&ident); match decls.len() { // no match: try match * 0 => ident, // single match, great! 1 => return Ok(decls.into_iter().next().unwrap()), // ambiguous _ => return Err(ambiguous_error(decls, None)), } } else { ident.clone() }; // fallback case: try to match with NS_INFER and infer the declaration // from the original ident. match self.resolve_ident_fallback(&ident, NS_INFER) { // The declaration and all needed parent modules were created // -> just return the fq ident Ok(inferred_ident) => Ok(inferred_ident), // Was not able to infer. Err(None) => Err(Error::new_simple( format!("Unknown name `{}`", &ident).to_string(), )), Err(Some(msg)) => Err(msg), } } /// Try lookup of the ident with name replaced. If unsuccessful, recursively retry parent ident. fn resolve_ident_fallback( &mut self, ident: &Ident, name_replacement: &'static str, ) -> Result> { let infer_ident = ident.clone().with_name(name_replacement); // lookup of infer_ident let mut decls = self.root_mod.module.lookup(&infer_ident); if decls.is_empty() { if let Some(parent) = infer_ident.clone().pop() { // try to infer parent let _ = self.resolve_ident_fallback(&parent, NS_INFER_MODULE)?; // module was successfully inferred, retry the lookup decls = self.root_mod.module.lookup(&infer_ident) } } match decls.len() { 1 => { // single match, great! let infer_ident = decls.into_iter().next().unwrap(); self.infer_decl(infer_ident, ident) .map_err(|x| Some(Error::new_simple(x))) } 0 => Err(None), _ => Err(Some(ambiguous_error(decls, Some(&ident.name)))), } } /// Create a declaration of [original] from template provided by declaration of [infer_ident]. fn infer_decl(&mut self, infer_ident: Ident, original: &Ident) -> Result { let infer = self.root_mod.module.get(&infer_ident).unwrap(); let mut infer_default = *infer.kind.as_infer().cloned().unwrap(); if let DeclKind::Module(new_module) = &mut infer_default { // Modules are inferred only for database inference. // Because we want to infer database modules that nested arbitrarily deep, // we cannot store the template in DeclKind::Infer, but we override it here. *new_module = Module::new_database(); } let module_ident = infer_ident.pop().unwrap(); let module = self.root_mod.module.get_mut(&module_ident).unwrap(); let module = module.kind.as_module_mut().unwrap(); // insert default module .names .insert(original.name.clone(), Decl::from(infer_default)); // infer table columns if let Some(decl) = module.names.get(NS_SELF).cloned() { if let DeclKind::InstanceOf(table_ident, _) = decl.kind { log::debug!("inferring {original} to be from table {table_ident}"); self.infer_table_column(&table_ident, &original.name)?; } } Ok(module_ident + Ident::from_name(original.name.clone())) } fn resolve_ident_wildcard(&mut self, ident: &Ident) -> Result { let ident_self = ident.clone().pop().ok_or_else(|| { "Column wildcard `*` must be qualified, e.g. `table_name.*`".to_string() })? + Ident::from_name(NS_SELF); let mut res = self.root_mod.module.lookup(&ident_self); if res.contains(&ident_self) { res = HashSet::from_iter([ident_self]); } if res.len() != 1 { return Err(format!("Unknown relation {ident}")); } let module_fq_self = res.into_iter().next().unwrap(); // Materialize into a tuple literal, containing idents. let fields = self.construct_wildcard_include(&module_fq_self); // This is just a workaround to return an Expr from this function. // We wrap the expr into DeclKind::Expr and save it into the root module. let cols_expr = Expr { flatten: true, ..Expr::new(ExprKind::Tuple(fields)) }; let cols_expr = DeclKind::Expr(Box::new(cols_expr)); let save_as = "_wildcard_match"; self.root_mod .module .names .insert(save_as.to_string(), cols_expr.into()); // Then we can return ident to that decl. Ok(Ident::from_name(save_as)) } } fn ambiguous_error(idents: HashSet, replace_name: Option<&String>) -> Error { let all_this = idents.iter().all(|d| d.starts_with_part(NS_THIS)); let mut chunks = Vec::new(); for mut ident in idents { if all_this { let (_, rem) = ident.pop_front(); if let Some(rem) = rem { ident = rem; } else { continue; } } if let Some(name) = replace_name { ident.name.clone_from(name); } chunks.push(ident.to_string()); } chunks.sort(); let hint = format!("could be any of: {}", chunks.join(", ")); Error::new_simple("Ambiguous name").push_hint(hint) } ================================================ FILE: prqlc/prqlc/src/semantic/resolver/snapshots/prqlc__semantic__resolver__test__append_union_different_tables.snap ================================================ --- source: prqlc/prqlc/src/semantic/resolver/mod.rs expression: final_lineage --- columns: - Single: name: - employees - name target_id: 132 target_name: ~ - Single: name: - employees - salary target_id: 133 target_name: ~ inputs: - id: 130 name: employees table: - default_db - employees - id: 119 name: managers table: - default_db - managers ================================================ FILE: prqlc/prqlc/src/semantic/resolver/snapshots/prqlc__semantic__resolver__test__frames_and_names-2.snap ================================================ --- source: prqlc/prqlc/src/semantic/resolver/mod.rs expression: "resolve_lineage(r#\"\n from table_1\n join customers (==customer_no)\n \"#).unwrap()" --- columns: - All: input_id: 117 except: [] - All: input_id: 114 except: [] inputs: - id: 117 name: table_1 table: - default_db - table_1 - id: 114 name: customers table: - default_db - customers ================================================ FILE: prqlc/prqlc/src/semantic/resolver/snapshots/prqlc__semantic__resolver__test__frames_and_names-3.snap ================================================ --- source: prqlc/prqlc/src/semantic/resolver/mod.rs expression: "resolve_lineage(r#\"\n from e = employees\n join salaries (==emp_no)\n group {e.emp_no, e.gender} (\n aggregate {\n emp_salary = average salaries.salary\n }\n )\n \"#).unwrap()" --- columns: - Single: name: - e - emp_no target_id: 127 target_name: ~ - Single: name: - e - gender target_id: 128 target_name: ~ - Single: name: - emp_salary target_id: 146 target_name: ~ inputs: - id: 120 name: e table: - default_db - employees - id: 117 name: salaries table: - default_db - salaries ================================================ FILE: prqlc/prqlc/src/semantic/resolver/snapshots/prqlc__semantic__resolver__test__frames_and_names.snap ================================================ --- source: prqlc/prqlc/src/semantic/resolver/mod.rs expression: "resolve_lineage(r#\"\n from orders\n select {customer_no, gross, tax, gross - tax}\n take 20\n \"#).unwrap()" --- columns: - Single: name: - orders - customer_no target_id: 121 target_name: ~ - Single: name: - orders - gross target_id: 122 target_name: ~ - Single: name: - orders - tax target_id: 123 target_name: ~ - Single: name: ~ target_id: 124 target_name: ~ inputs: - id: 119 name: orders table: - default_db - orders ================================================ FILE: prqlc/prqlc/src/semantic/resolver/snapshots/prqlc__semantic__resolver__test__functions_1.snap ================================================ --- source: prqlc/prqlc/src/semantic/resolver/mod.rs expression: "resolve_derive(r#\"\n let subtract = a b -> a - b\n\n from employees\n derive {\n net_salary = subtract gross_salary tax\n }\n \"#).unwrap()" --- - RqOperator: name: std.sub args: - Ident: - this - employees - gross_salary span: "1:128-140" - Ident: - this - employees - tax span: "1:141-144" span: "1:119-144" alias: net_salary ================================================ FILE: prqlc/prqlc/src/semantic/resolver/snapshots/prqlc__semantic__resolver__test__functions_nested.snap ================================================ --- source: prqlc/prqlc/src/semantic/resolver/mod.rs expression: "resolve_derive(r#\"\n let lag_day = x -> s\"lag_day_todo({x})\"\n let ret = x dividend_return -> x / (lag_day x) - 1 + dividend_return\n\n from a\n derive (ret b c)\n \"#).unwrap()" --- - RqOperator: name: std.add args: - RqOperator: name: std.sub args: - RqOperator: name: std.div_f args: - Ident: - this - a - b span: "1:179-180" - SString: - String: lag_day_todo( - Expr: expr: Ident: - this - a - b span: "1:179-180" format: ~ - String: ) span: "1:102-111" span: "1:97-112" - Literal: Integer: 1 span: "1:115-116" ty: kind: Primitive: Int span: ~ name: ~ span: "1:97-116" - Ident: - this - a - c span: "1:181-182" span: "1:175-182" ================================================ FILE: prqlc/prqlc/src/semantic/resolver/snapshots/prqlc__semantic__resolver__test__functions_pipeline-2.snap ================================================ --- source: prqlc/prqlc/src/semantic/resolver/mod.rs expression: "resolve_derive(r#\"\n let plus_one = x -> x + 1\n let plus = x y -> x + y\n\n from a\n derive {b = (sum foo | plus_one | plus 2)}\n \"#).unwrap()" --- - RqOperator: name: std.add args: - Literal: Integer: 2 span: "1:146-147" ty: kind: Primitive: Int span: ~ name: ~ - RqOperator: name: std.add args: - RqOperator: name: std.sum args: - Ident: - this - a - foo span: "1:124-127" ty: kind: Array: ~ span: "0:1699-1701" name: array span: "1:120-127" - Literal: Integer: 1 span: "1:37-38" ty: kind: Primitive: Int span: ~ name: ~ span: "1:130-138" span: "1:141-147" alias: b ================================================ FILE: prqlc/prqlc/src/semantic/resolver/snapshots/prqlc__semantic__resolver__test__functions_pipeline.snap ================================================ --- source: prqlc/prqlc/src/semantic/resolver/mod.rs expression: "resolve_derive(r#\"\n from a\n derive one = (foo | sum)\n \"#).unwrap()" --- - RqOperator: name: std.sum args: - Ident: - this - a - foo span: "1:46-49" ty: kind: Array: ~ span: "0:1699-1701" name: array span: "1:52-55" alias: one ================================================ FILE: prqlc/prqlc/src/semantic/resolver/snapshots/prqlc__semantic__resolver__test__named_args.snap ================================================ --- source: prqlc/prqlc/src/semantic/resolver/mod.rs expression: "resolve_derive(r#\"\n let add_one = x to:1 -> x + to\n\n from foo_table\n derive {\n added = add_one bar to:3,\n added_default = add_one bar\n }\n \"#).unwrap()" --- - RqOperator: name: std.add args: - Ident: - this - foo_table - bar span: "1:125-128" - Literal: Integer: 3 span: "1:132-133" ty: kind: Primitive: Int span: ~ name: ~ span: "1:117-133" alias: added - RqOperator: name: std.add args: - Ident: - this - foo_table - bar span: "1:175-178" - Literal: Integer: 1 span: "1:32-33" ty: kind: Primitive: Int span: ~ name: ~ span: "1:167-178" alias: added_default ================================================ FILE: prqlc/prqlc/src/semantic/resolver/snapshots/prqlc__semantic__resolver__test__variables_1.snap ================================================ --- source: prqlc/prqlc/src/semantic/resolver/mod.rs expression: "resolve_derive(r#\"\n from employees\n derive {\n gross_salary = salary + payroll_tax,\n gross_cost = gross_salary + benefits_cost\n }\n \"#).unwrap()" --- - RqOperator: name: std.add args: - Ident: - this - employees - salary span: "1:80-86" - Ident: - this - employees - payroll_tax span: "1:89-100" span: "1:80-100" alias: gross_salary - RqOperator: name: std.add args: - Ident: - this - gross_salary span: "1:133-145" - Ident: - this - employees - benefits_cost span: "1:148-161" span: "1:133-161" alias: gross_cost ================================================ FILE: prqlc/prqlc/src/semantic/resolver/snapshots/prqlc__semantic__resolver__transforms__tests__aggregate_positional_arg-2.snap ================================================ --- source: prqlc/prqlc/src/semantic/resolver/transforms.rs expression: expr --- TransformCall: input: Ident: - default_db - c_invoice span: "1:9-23" ty: kind: Array: kind: Tuple: - Wildcard: ~ span: "0:1740-1744" name: ~ span: "0:1739-1745" name: relation lineage: columns: - All: input_id: 116 except: [] inputs: - id: 116 name: c_invoice table: - default_db - c_invoice kind: Aggregate: assigns: Tuple: - RqOperator: name: std.average args: - Ident: - this - c_invoice - amount span: "1:81-87" ty: kind: Array: ~ span: "0:1699-1701" name: array span: "1:73-87" span: "1:73-87" ty: kind: Tuple: - Single: - ~ - ~ span: ~ name: ~ partition: Tuple: - Ident: - this - c_invoice - issued_at span: "1:38-47" span: "1:38-47" ty: kind: Tuple: - Single: - issued_at - ~ span: ~ name: ~ span: "1:62-88" ty: kind: Array: kind: Tuple: - Single: - issued_at - ~ - Single: - ~ - ~ span: ~ name: ~ span: ~ name: ~ lineage: columns: - Single: name: - c_invoice - issued_at target_id: 118 target_name: ~ - Single: name: ~ target_id: 134 target_name: ~ inputs: - id: 116 name: c_invoice table: - default_db - c_invoice ================================================ FILE: prqlc/prqlc/src/semantic/resolver/static_eval.rs ================================================ //! Static analysis - compile time expression evaluation use crate::ir::pl::{Expr, ExprKind, Literal}; use crate::Result; impl super::Resolver<'_> { /// Tries to simplify this expression (and not child expressions) to a constant. pub fn maybe_static_eval(&mut self, expr: Expr) -> Result { Ok(match &expr.kind { ExprKind::RqOperator { .. } => { let id = expr.id; let span = expr.span; let expr = static_eval_rq_operator(expr); Expr { id, span, ..expr } } ExprKind::Case(_) => static_eval_case(expr), _ => expr, }) } } fn static_eval_rq_operator(mut expr: Expr) -> Expr { let (name, mut args) = expr.kind.into_rq_operator().unwrap(); match name.as_str() { "std.not" => { if let ExprKind::Literal(Literal::Boolean(val)) = &args[0].kind { return Expr::new(Literal::Boolean(!val)); } } "std.neg" => match &args[0].kind { ExprKind::Literal(Literal::Integer(val)) => return Expr::new(Literal::Integer(-val)), ExprKind::Literal(Literal::Float(val)) => return Expr::new(Literal::Float(-val)), _ => (), }, "std.eq" => { if let (ExprKind::Literal(left), ExprKind::Literal(right)) = (&args[0].kind, &args[1].kind) { // don't eval comparisons between different types of literals if left.as_ref() == right.as_ref() { return Expr::new(Literal::Boolean(left == right)); } } } "std.ne" => { if let (ExprKind::Literal(left), ExprKind::Literal(right)) = (&args[0].kind, &args[1].kind) { // don't eval comparisons between different types of literals if left.as_ref() == right.as_ref() { return Expr::new(Literal::Boolean(left != right)); } } } "std.and" => { if let ( ExprKind::Literal(Literal::Boolean(left)), ExprKind::Literal(Literal::Boolean(right)), ) = (&args[0].kind, &args[1].kind) { return Expr::new(Literal::Boolean(*left && *right)); } } "std.or" => { if let ( ExprKind::Literal(Literal::Boolean(left)), ExprKind::Literal(Literal::Boolean(right)), ) = (&args[0].kind, &args[1].kind) { return Expr::new(Literal::Boolean(*left || *right)); } } "std.coalesce" => { if let ExprKind::Literal(Literal::Null) = &args[0].kind { return args.remove(1); } } _ => {} }; expr.kind = ExprKind::RqOperator { name, args }; expr } fn static_eval_case(mut expr: Expr) -> Expr { let items = expr.kind.into_case().unwrap(); let mut res = Vec::with_capacity(items.len()); for item in items { if let ExprKind::Literal(Literal::Boolean(condition)) = item.condition.kind { if condition { res.push(item); break; } else { // this case can be removed continue; } } else { res.push(item); } } if res.is_empty() { return Expr::new(Literal::Null); } if res.len() == 1 { let is_true = matches!( res[0].condition.kind, ExprKind::Literal(Literal::Boolean(true)) ); if is_true { return *res.remove(0).value; } } expr.kind = ExprKind::Case(res); expr } ================================================ FILE: prqlc/prqlc/src/semantic/resolver/stmt.rs ================================================ use std::collections::HashMap; use crate::ir::decl::{Decl, DeclKind, Module, TableDecl, TableExpr}; use crate::ir::pl::*; use crate::pr::{Ty, TyKind, TyTupleField}; use crate::Result; use crate::WithErrorInfo; impl super::Resolver<'_> { // entry point to the resolver pub fn fold_statements(&mut self, stmts: Vec) -> Result<()> { for mut stmt in stmts { stmt.id = Some(self.id.gen()); if let Some(span) = stmt.span { self.root_mod.span_map.insert(stmt.id.unwrap(), span); } let ident = Ident { path: self.current_module_path.clone(), name: stmt.name().to_string(), }; let mut def = match stmt.kind { StmtKind::QueryDef(d) => { let decl = DeclKind::QueryDef(*d); self.root_mod .declare(ident, decl, stmt.id, Vec::new()) .with_span(stmt.span)?; continue; } StmtKind::VarDef(var_def) => self.fold_var_def(var_def)?, StmtKind::TypeDef(ty_def) => { let mut ty = self.fold_type(ty_def.value)?; ty.name = Some(ident.name.clone()); let decl = DeclKind::Ty(ty); self.root_mod .declare(ident, decl, stmt.id, stmt.annotations) .with_span(stmt.span)?; continue; } StmtKind::ModuleDef(module_def) => { self.current_module_path.push(ident.name); let decl = Decl { declared_at: stmt.id, kind: DeclKind::Module(Module { names: HashMap::new(), redirects: Vec::new(), shadowed: None, }), annotations: stmt.annotations, ..Default::default() }; let ident = Ident::from_path(self.current_module_path.clone()); self.root_mod .module .insert(ident, decl) .with_span(stmt.span)?; self.fold_statements(module_def.stmts)?; self.current_module_path.pop(); continue; } StmtKind::ImportDef(target) => { let decl = Decl { declared_at: stmt.id, kind: DeclKind::Import(target.name), annotations: stmt.annotations, ..Default::default() }; self.root_mod .module .insert(ident, decl) .with_span(stmt.span)?; continue; } }; if def.name == "main" { def.ty = Some(Ty::new(TyKind::Ident(Ident::from_path(vec![ "std", "relation", ])))); } if let Some(ExprKind::Func(closure)) = def.value.as_mut().map(|x| &mut x.kind) { if closure.name_hint.is_none() { closure.name_hint = Some(ident.clone()); } } let expected_ty = fold_type_opt(self, def.ty)?; let decl = match def.value { Some(mut def_value) => { // var value is provided // validate type if expected_ty.is_some() { let who = || Some(def.name.clone()); self.validate_expr_type(&mut def_value, expected_ty.as_ref(), &who)?; } prepare_expr_decl(def_value) } None => { // var value is not provided // is this a relation? if expected_ty.as_ref().is_some_and(|t| t.is_relation()) { // treat this var as a TableDecl DeclKind::TableDecl(TableDecl { ty: expected_ty, expr: TableExpr::LocalTable, }) } else { // treat this var as a param let mut expr = Box::new(Expr::new(ExprKind::Param(def.name))); expr.ty = expected_ty; DeclKind::Expr(expr) } } }; self.root_mod .declare(ident, decl, stmt.id, stmt.annotations) .with_span(stmt.span)?; } Ok(()) } } fn prepare_expr_decl(value: Box) -> DeclKind { match &value.lineage { Some(frame) => { let columns = (frame.columns.iter()) .map(|col| match col { LineageColumn::All { .. } => TyTupleField::Wildcard(None), LineageColumn::Single { name, .. } => { TyTupleField::Single(name.as_ref().map(|n| n.name.clone()), None) } }) .collect(); let ty = Some(Ty::relation(columns)); let expr = TableExpr::RelationVar(value); DeclKind::TableDecl(TableDecl { ty, expr }) } _ => DeclKind::Expr(value), } } ================================================ FILE: prqlc/prqlc/src/semantic/resolver/transforms.rs ================================================ use std::collections::HashMap; use std::iter::zip; use itertools::Itertools; use serde::Deserialize; use super::types::{ty_tuple_kind, type_intersection}; use super::Resolver; use crate::ir::decl::{Decl, DeclKind, Module}; use crate::ir::generic::{SortDirection, WindowKind}; use crate::ir::pl::*; use crate::pr::{Ty, TyKind, TyTupleField}; use crate::semantic::ast_expand::{restrict_null_literal, try_restrict_range}; use crate::semantic::resolver::functions::expr_of_func; use crate::semantic::{write_pl, NS_PARAM, NS_THIS}; use crate::{compiler_version, Error, Reason, Result, WithErrorInfo}; impl Resolver<'_> { /// try to convert function call with enough args into transform #[allow(clippy::boxed_local)] pub fn resolve_special_func(&mut self, func: Box, needs_window: bool) -> Result { let internal_name = func.body.kind.into_internal().unwrap(); let (kind, input) = match internal_name.as_str() { "select" => { let [assigns, tbl] = unpack::<2>(func.args); let assigns = Box::new(self.coerce_into_tuple(assigns)?); (TransformKind::Select { assigns }, tbl) } "filter" => { let [filter, tbl] = unpack::<2>(func.args); let filter = Box::new(filter); (TransformKind::Filter { filter }, tbl) } "derive" => { let [assigns, tbl] = unpack::<2>(func.args); let assigns = Box::new(self.coerce_into_tuple(assigns)?); (TransformKind::Derive { assigns }, tbl) } "aggregate" => { let [assigns, tbl] = unpack::<2>(func.args); let assigns = Box::new(self.coerce_into_tuple(assigns)?); (TransformKind::Aggregate { assigns }, tbl) } "sort" => { let [by, tbl] = unpack::<2>(func.args); let by = self .coerce_into_tuple(by)? .try_cast(|x| x.into_tuple(), Some("sort"), "tuple")? .into_iter() .map(|expr| { let (column, direction) = match expr.kind { ExprKind::RqOperator { name, mut args } if name == "std.neg" => { (args.remove(0), SortDirection::Desc) } _ => (expr, SortDirection::default()), }; let column = Box::new(column); ColumnSort { direction, column } }) .collect(); (TransformKind::Sort { by }, tbl) } "take" => { let [expr, tbl] = unpack::<2>(func.args); let range = if let ExprKind::Literal(Literal::Integer(n)) = expr.kind { range_from_ints(None, Some(n)) } else { match try_restrict_range(expr) { Ok((start, end)) => Range { start: restrict_null_literal(start).map(Box::new), end: restrict_null_literal(end).map(Box::new), }, Err(expr) => { return Err(Error::new(Reason::Expected { who: Some("`take`".to_string()), expected: "int or range".to_string(), found: write_pl(expr.clone()), }) // Possibly this should refer to the item after the `take` where // one exists? .with_span(expr.span)); } } }; (TransformKind::Take { range }, tbl) } "join" => { let [side, with, filter, tbl] = unpack::<4>(func.args); let side = { let span = side.span; let ident = side.clone() .try_cast(ExprKind::into_ident, Some("side"), "ident")?; // first try to match the raw ident string as a bare word match ident.to_string().as_str() { "inner" => JoinSide::Inner, "left" => JoinSide::Left, "right" => JoinSide::Right, "full" => JoinSide::Full, _ => { // if that fails, fold the ident and try treating the result as a literal // this allows the join side to be passed as a function parameter // NOTE: this is temporary, pending discussions and implementation, tracked in #4501 let folded = self.fold_expr(side)?.try_cast( ExprKind::into_literal, Some("side"), "string literal", )?; match folded.to_string().as_str() { "\"inner\"" => JoinSide::Inner, "\"left\"" => JoinSide::Left, "\"right\"" => JoinSide::Right, "\"full\"" => JoinSide::Full, _ => { return Err(Error::new(Reason::Expected { who: Some("`side`".to_string()), expected: "inner, left, right or full".to_string(), found: folded.to_string(), }) .with_span(span)) } } } } }; let filter = Box::new(filter); let with = Box::new(with); (TransformKind::Join { side, with, filter }, tbl) } "group" => { let [by, pipeline, tbl] = unpack::<3>(func.args); let by = Box::new(self.coerce_into_tuple(by)?); // construct the relation that is passed into the pipeline // (when generics are a thing, this can be removed) let partition = { let partition = Expr::new(ExprKind::All { within: Box::new(Expr::new(Ident::from_name(NS_THIS))), except: by.clone(), }); // wrap into select, so the names are resolved correctly let partition = FuncCall { name: Box::new(Expr::new(Ident::from_path(vec!["std", "select"]))), args: vec![partition, tbl], named_args: Default::default(), }; let partition = Expr::new(ExprKind::FuncCall(partition)); // fold, so lineage and types are inferred self.fold_expr(partition)? }; let pipeline = self.fold_by_simulating_eval(pipeline, &partition)?; // unpack tbl back out let tbl = *partition.kind.into_transform_call().unwrap().input; let pipeline = Box::new(pipeline); (TransformKind::Group { by, pipeline }, tbl) } "window" => { let [rows, range, expanding, rolling, pipeline, tbl] = unpack::<6>(func.args); let expanding = { let as_bool = expanding.kind.as_literal().and_then(|l| l.as_boolean()); *as_bool.ok_or_else(|| { Error::new(Reason::Expected { who: Some("parameter `expanding`".to_string()), expected: "a boolean".to_string(), found: write_pl(expanding.clone()), }) .with_span(expanding.span) })? }; let rolling = { let as_int = rolling.kind.as_literal().and_then(|x| x.as_integer()); *as_int.ok_or_else(|| { Error::new(Reason::Expected { who: Some("parameter `rolling`".to_string()), expected: "a number".to_string(), found: write_pl(rolling.clone()), }) .with_span(rolling.span) })? }; let rows = { let range_tuple = try_restrict_range(rows).map_err(|expr| { Error::new(Reason::Expected { who: Some("parameter `rows`".to_string()), expected: "a range".to_string(), found: write_pl(expr.clone()), }) .with_span(expr.span) })?; into_literal_range(range_tuple)? }; let range = { let range_tuple = try_restrict_range(range).map_err(|expr| { Error::new(Reason::Expected { who: Some("parameter `range`".to_string()), expected: "a range".to_string(), found: write_pl(expr.clone()), }) .with_span(expr.span) })?; into_literal_range(range_tuple)? }; let (kind, start, end) = if expanding { (WindowKind::Rows, None, Some(0)) } else if rolling > 0 { (WindowKind::Rows, Some(-rolling + 1), Some(0)) } else if !range_is_empty(&rows) { (WindowKind::Rows, rows.0, rows.1) } else if !range_is_empty(&range) { (WindowKind::Range, range.0, range.1) } else { (WindowKind::Rows, None, None) }; // let start = Expr::new(start.map_or(Literal::Null, Literal::Integer)); // let end = Expr::new(end.map_or(Literal::Null, Literal::Integer)); let range = Range { start: start.map(Literal::Integer).map(Expr::new).map(Box::new), end: end.map(Literal::Integer).map(Expr::new).map(Box::new), }; let pipeline = self.fold_by_simulating_eval(pipeline, &tbl)?; let transform_kind = TransformKind::Window { kind, range, pipeline: Box::new(pipeline), }; (transform_kind, tbl) } "append" => { let [bottom, top] = unpack::<2>(func.args); (TransformKind::Append(Box::new(bottom)), top) } "loop" => { let [pipeline, tbl] = unpack::<2>(func.args); let pipeline = self.fold_by_simulating_eval(pipeline, &tbl)?; (TransformKind::Loop(Box::new(pipeline)), tbl) } "in" => { // yes, this is not a transform, but this is the most appropriate place for it let [pattern, value] = unpack::<2>(func.args); if pattern.ty.as_ref().is_some_and(|x| x.kind.is_array()) { return Ok(Expr::new(ExprKind::RqOperator { name: "std.array_in".to_string(), args: vec![value, pattern], })); } let pattern = match try_restrict_range(pattern) { Ok((start, end)) => { let start = restrict_null_literal(start); let end = restrict_null_literal(end); let start = start.map(|s| new_binop(value.clone(), &["std", "gte"], s)); let end = end.map(|e| new_binop(value, &["std", "lte"], e)); let res = maybe_binop(start, &["std", "and"], end); let res = res.unwrap_or_else(|| { Expr::new(ExprKind::Literal(Literal::Boolean(true))) }); return Ok(res); } Err(expr) => expr, }; return Err(Error::new(Reason::Expected { who: Some("std.in".to_string()), expected: "a pattern".to_string(), found: write_pl(pattern.clone()), }) .with_span(pattern.span)); } "tuple_every" => { // yes, this is not a transform, but this is the most appropriate place for it let [list] = unpack::<1>(func.args); let list = list.kind.into_tuple().unwrap(); let mut res = None; for item in list { res = maybe_binop(res, &["std", "and"], Some(item)); } let res = res.unwrap_or_else(|| Expr::new(ExprKind::Literal(Literal::Boolean(true)))); return Ok(res); } "tuple_map" => { // yes, this is not a transform, but this is the most appropriate place for it let [func, list] = unpack::<2>(func.args); let list_items = list.kind.into_tuple().unwrap(); let list_items = list_items .into_iter() .map(|item| { Expr::new(ExprKind::FuncCall(FuncCall::new_simple( func.clone(), vec![item], ))) }) .collect_vec(); return Ok(Expr { kind: ExprKind::Tuple(list_items), ..list }); } "tuple_zip" => { // yes, this is not a transform, but this is the most appropriate place for it let [a, b] = unpack::<2>(func.args); let a = a.kind.into_tuple().unwrap(); let b = b.kind.into_tuple().unwrap(); let mut res = Vec::new(); for (a, b) in std::iter::zip(a, b) { res.push(Expr::new(ExprKind::Tuple(vec![a, b]))); } return Ok(Expr::new(ExprKind::Tuple(res))); } "_eq" => { // yes, this is not a transform, but this is the most appropriate place for it let [list] = unpack::<1>(func.args); let list = list.kind.into_tuple().unwrap(); let [a, b]: [Expr; 2] = list.try_into().unwrap(); let res = maybe_binop(Some(a), &["std", "eq"], Some(b)).unwrap(); return Ok(res); } "from_text" => { // yes, this is not a transform, but this is the most appropriate place for it let [format, text_expr] = unpack::<2>(func.args); let text = match text_expr.kind { ExprKind::Literal(Literal::String(text)) => text, _ => { return Err(Error::new(Reason::Expected { who: Some("std.from_text".to_string()), expected: "a string literal".to_string(), found: format!("`{}`", write_pl(text_expr.clone())), }) .with_span(text_expr.span)); } }; let res = { let span = format.span; let format = format .try_cast(ExprKind::into_ident, Some("format"), "ident")? .to_string(); match format.as_str() { "csv" => from_text::parse_csv(&text) .map_err(|r| Error::new_simple(r).with_span(span))?, "json" => from_text::parse_json(&text) .map_err(|r| Error::new_simple(r).with_span(span))?, _ => { return Err(Error::new(Reason::Expected { who: Some("`format`".to_string()), expected: "csv or json".to_string(), found: format, }) .with_span(span)) } } }; let expr_id = text_expr.id.unwrap(); let input_name = text_expr.alias.unwrap_or_else(|| "text".to_string()); let columns: Vec<_> = res .columns .iter() .cloned() .map(|x| TyTupleField::Single(Some(x), None)) .collect(); let frame = self.declare_table_for_literal(expr_id, Some(columns), Some(input_name)); let res = Expr::new(ExprKind::Array( res.rows .into_iter() .map(|row| { Expr::new(ExprKind::Tuple( row.into_iter() .map(|lit| Expr::new(ExprKind::Literal(lit))) .collect(), )) }) .collect(), )); let res = Expr { lineage: Some(frame), id: text_expr.id, ..res }; return Ok(res); } "prql_version" => { // yes, this is not a transform, but this is the most appropriate place for it let ver = compiler_version().to_string(); return Ok(Expr::new(ExprKind::Literal(Literal::String(ver)))); } "count" | "row_number" => { // HACK: these functions get `this`, resolved to `{x = {_self}}`, which // throws an error during lowering. // But because these functions don't *really* need an arg, we can just pass // a null instead. return Ok(Expr { needs_window, ..Expr::new(ExprKind::RqOperator { name: format!("std.{internal_name}"), args: vec![Expr::new(Literal::Null)], }) }); } _ => { return Err( Error::new_simple(format!("unknown operator {internal_name}")) .push_hint("this is a bug in prqlc") .with_span(func.body.span), ) } }; let transform_call = TransformCall { kind: Box::new(kind), input: Box::new(input), partition: None, frame: WindowFrame::default(), sort: Vec::new(), }; let ty = self.infer_type_of_special_func(&transform_call)?; Ok(Expr { ty, ..Expr::new(ExprKind::TransformCall(transform_call)) }) } /// Wraps non-tuple Exprs into a singleton Tuple. pub(super) fn coerce_into_tuple(&mut self, expr: Expr) -> Result { let is_tuple_ty = expr.ty.as_ref().is_some_and(|t| t.kind.is_tuple()) && !expr.kind.is_all(); Ok(if is_tuple_ty { // a helpful check for a common anti-pattern if let Some(alias) = expr.alias { return Err(Error::new(Reason::Unexpected { found: format!("assign to `{alias}`"), }) .push_hint(format!("move assign into the tuple: `[{alias} = ...]`")) .with_span(expr.span)); } expr } else { let span = expr.span; let mut expr = Expr::new(ExprKind::Tuple(vec![expr])); expr.span = span; self.fold_expr(expr)? }) } /// Figure out the type of a function call, if this function is a *special function*. /// (declared in std module & requires special handling). pub fn infer_type_of_special_func( &mut self, transform_call: &TransformCall, ) -> Result> { // Long term plan is to make this function obsolete with generic function parameters. // In other words, I hope to make our type system powerful enough to express return // type of all std module functions. Ok(match transform_call.kind.as_ref() { TransformKind::Select { assigns } => assigns .ty .clone() .map(|x| Ty::new(TyKind::Array(Some(Box::new(x))))), TransformKind::Derive { assigns } => { let input = transform_call.input.ty.clone().unwrap(); let input = input.into_relation().unwrap(); let derived = assigns.ty.clone().unwrap(); let derived = derived.kind.into_tuple().unwrap(); Some(Ty::new(TyKind::Array(Some(Box::new(Ty::new( ty_tuple_kind([input, derived].concat()), )))))) } TransformKind::Aggregate { assigns } => { let tuple = assigns.ty.clone().unwrap(); Some(Ty::new(TyKind::Array(Some(Box::new(tuple))))) } TransformKind::Filter { .. } | TransformKind::Sort { .. } | TransformKind::Take { .. } => transform_call.input.ty.clone(), TransformKind::Join { with, .. } => { let input = transform_call.input.ty.clone().unwrap(); let input = input.into_relation().unwrap(); let with_name = with.alias.clone(); let with = with.ty.clone().unwrap(); let with = with.kind.into_array().unwrap().unwrap(); let with = TyTupleField::Single(with_name, Some(*with)); Some(Ty::new(TyKind::Array(Some(Box::new(Ty::new( ty_tuple_kind([input, vec![with]].concat()), )))))) } TransformKind::Group { pipeline, by } => { let by = by.ty.clone().unwrap(); let by = by.kind.into_tuple().unwrap(); let pipeline = pipeline.ty.clone().unwrap(); let pipeline = pipeline.kind.into_function().unwrap().unwrap(); let pipeline = pipeline.return_ty.unwrap().into_relation().unwrap(); Some(Ty::new(TyKind::Array(Some(Box::new(Ty::new( ty_tuple_kind([by, pipeline].concat()), )))))) } TransformKind::Window { pipeline, .. } | TransformKind::Loop(pipeline) => { let pipeline = pipeline.ty.clone().unwrap(); let pipeline = pipeline.kind.into_function().unwrap().unwrap(); pipeline.return_ty.map(|x| *x) } TransformKind::Append(bottom) => { let top = transform_call.input.ty.clone().unwrap(); let bottom = bottom.ty.clone().unwrap(); Some(type_intersection(top, bottom)) } }) } } fn range_is_empty(range: &(Option, Option)) -> bool { match (&range.0, &range.1) { (Some(s), Some(e)) => s > e, _ => false, } } fn range_from_ints(start: Option, end: Option) -> Range { let start = start.map(|x| Box::new(Expr::new(ExprKind::Literal(Literal::Integer(x))))); let end = end.map(|x| Box::new(Expr::new(ExprKind::Literal(Literal::Integer(x))))); Range { start, end } } fn into_literal_range(range: (Expr, Expr)) -> Result<(Option, Option)> { fn into_int(bound: Expr) -> Result> { match bound.kind { ExprKind::Literal(Literal::Null) => Ok(None), ExprKind::Literal(Literal::Integer(i)) => Ok(Some(i)), _ => Err(Error::new_simple("expected an int literal").with_span(bound.span)), } } Ok((into_int(range.0)?, into_int(range.1)?)) } impl Resolver<'_> { /// Simulate evaluation of the inner pipeline of group or window // Creates a dummy node that acts as value that pipeline can be resolved upon. fn fold_by_simulating_eval(&mut self, pipeline: Expr, val: &Expr) -> Result { log::debug!("fold by simulating evaluation"); let span = pipeline.span; let param_name = "_tbl"; let param_id = self.id.gen(); // resolver will not resolve a function call if any arguments are missing // but would instead return a closure to be resolved later. // because the pipeline of group is a function that takes a table chunk // and applies the transforms to it, it would not get resolved. // thats why we trick the resolver with a dummy node that acts as table // chunk and instruct resolver to apply the transform on that. let mut dummy = Expr::new(ExprKind::Ident(Ident::from_name(param_name))); dummy.lineage.clone_from(&val.lineage); dummy.ty.clone_from(&val.ty); let pipeline = Expr::new(ExprKind::FuncCall(FuncCall::new_simple( pipeline, vec![dummy], ))); let env = Module::singleton(param_name, Decl::from(DeclKind::Column(param_id))); self.root_mod.module.stack_push(NS_PARAM, env); let mut pipeline = self.fold_expr(pipeline)?; // attach the span to the TransformCall, as this is what will // be preserved after resolving is complete pipeline.span = pipeline.span.or(span); self.root_mod.module.stack_pop(NS_PARAM).unwrap(); // now, we need wrap the result into a closure and replace // the dummy node with closure's parameter. // validate that the return type is a relation // this can be removed after we have proper type checking for all std functions let expected = Some(Ty::relation(vec![TyTupleField::Wildcard(None)])); self.validate_expr_type(&mut pipeline, expected.as_ref(), &|| { Some("pipeline".to_string()) })?; // construct the function back let func = Box::new(Func { name_hint: None, body: Box::new(pipeline), return_ty: None, args: vec![], params: vec![FuncParam { name: param_id.to_string(), ty: None, default_value: None, }], named_params: vec![], env: Default::default(), }); Ok(*expr_of_func(func, span)) } } impl TransformCall { pub fn infer_lineage(&self) -> Result { use TransformKind::*; fn lineage_or_default(expr: &Expr) -> Result { expr.lineage.clone().ok_or_else(|| { Error::new_simple("expected {expr:?} to have table type").with_span(expr.span) }) } Ok(match self.kind.as_ref() { Select { assigns } => { let mut lineage = lineage_or_default(&self.input)?; lineage.clear(); lineage.apply_assigns(assigns, false); lineage } Derive { assigns } => { let mut lineage = lineage_or_default(&self.input)?; lineage.apply_assigns(assigns, false); lineage } Group { pipeline, by, .. } => { let mut lineage = lineage_or_default(&self.input)?; lineage.clear(); lineage.apply_assigns(by, false); // pipeline's body is resolved, just use its type let Func { body, .. } = pipeline.kind.as_func().unwrap().as_ref(); let partition_lin = lineage_or_default(body).unwrap(); lineage.columns.extend(partition_lin.columns); log::debug!(".. type={lineage}"); lineage } Window { pipeline, .. } => { // pipeline's body is resolved, just use its type let Func { body, .. } = pipeline.kind.as_func().unwrap().as_ref(); lineage_or_default(body).unwrap() } Aggregate { assigns } => { let mut lineage = lineage_or_default(&self.input)?; lineage.clear(); lineage.apply_assigns(assigns, false); lineage } Join { with, .. } => { let left = lineage_or_default(&self.input)?; let right = lineage_or_default(with)?; join(left, right) } Append(bottom) => { let top = lineage_or_default(&self.input)?; let bottom = lineage_or_default(bottom)?; append(top, bottom)? } Loop(_) => lineage_or_default(&self.input)?, Sort { .. } | Filter { .. } | Take { .. } => lineage_or_default(&self.input)?, }) } } fn join(mut lhs: Lineage, rhs: Lineage) -> Lineage { lhs.columns.extend(rhs.columns); lhs.inputs.extend(rhs.inputs); lhs } fn append(mut top: Lineage, bottom: Lineage) -> Result { if top.columns.len() != bottom.columns.len() { return Err(Error::new_simple( "cannot append two relations with non-matching number of columns.", )) .push_hint(format!( "top has {} columns, but bottom has {}", top.columns.len(), bottom.columns.len() )); } // Merge inputs from both relations so lineage can track both sources // This is similar to how `join` handles inputs top.inputs.extend(bottom.inputs); // Merge columns positionally // In a union, columns are aligned by position, so column 0 from top // and column 0 from bottom become one output column let mut columns = Vec::with_capacity(top.columns.len()); for (t, b) in zip(top.columns, bottom.columns) { columns.push(match (t, b) { // For All columns, keep the top's input_id but merge except sets ( LineageColumn::All { input_id: input_id_t, except: except_t, }, LineageColumn::All { input_id: _input_id_b, except: except_b, }, ) => { // Merge except sets from both tables // This preserves exclusion information even when input_ids differ // (e.g., "from employees select !{name}" append "from managers select !{salary}") let mut except = except_t; except.extend(except_b); LineageColumn::All { input_id: input_id_t, except, } } ( LineageColumn::Single { name: name_t, target_id, target_name, }, LineageColumn::Single { name: name_b, .. }, ) => { // For Single columns in a union, we keep the top's target_id // Both inputs are now tracked in top.inputs, so lineage can // reference either source even though the column only points to one match (name_t, name_b) { (None, None) => LineageColumn::Single { name: None, target_id, target_name, }, (None, Some(name)) | (Some(name), _) => LineageColumn::Single { name: Some(name), target_id, target_name, }, } } (t, b) => return Err(Error::new_simple(format!( "cannot match columns `{t:?}` and `{b:?}`" )) .push_hint( "make sure that top and bottom relations of append has the same column layout", )), }); } top.columns = columns; Ok(top) } impl Lineage { pub fn clear(&mut self) { self.prev_columns.clear(); self.prev_columns.append(&mut self.columns); } pub fn apply_assigns(&mut self, assigns: &Expr, inline_refs: bool) { match &assigns.kind { ExprKind::Tuple(fields) => { for expr in fields { self.apply_assigns(expr, inline_refs); } // hack for making `x | select { y = this }` work if let Some(alias) = &assigns.alias { if self.columns.len() == 1 { let col = self.columns.first().unwrap(); if let LineageColumn::All { input_id, .. } = col { let input = self.inputs.iter_mut().find(|i| i.id == *input_id).unwrap(); input.name.clone_from(alias); } } } } _ => self.apply_assign(assigns, inline_refs), } } pub fn apply_assign(&mut self, expr: &Expr, inline_refs: bool) { // special case: all except if let ExprKind::All { within, except } = &expr.kind { let mut within_lineage = Lineage::default(); within_lineage.inputs.extend(self.inputs.clone()); within_lineage.apply_assigns(within, true); let mut except_lineage = Lineage::default(); except_lineage.inputs.extend(self.inputs.clone()); except_lineage.apply_assigns(except, true); 'within: for col in within_lineage.columns { match col { LineageColumn::Single { ref name, ref target_id, ref target_name, .. } => { let is_excluded = except_lineage.columns.iter().any(|e| match e { LineageColumn::Single { name: e_name, .. } => name == e_name, LineageColumn::All { input_id: e_iid, except: e_except, } => { target_id == e_iid && !e_except.contains(target_name.as_ref().unwrap()) } }); if !is_excluded { self.columns.push(col); } } LineageColumn::All { input_id, mut except, } => { for excluded in &except_lineage.columns { match excluded { LineageColumn::Single { name: Some(name), .. } => { // input_id comes from LineageColumn::All in self.columns, // which should reference valid inputs in self.inputs. // If this panics, it may indicate a scope issue similar to // #5280 (join inside group) - see lowering.rs for the fix pattern. let input = self.find_input(input_id).unwrap(); let ex_input_name = name.iter().next().unwrap(); if ex_input_name == &input.name { except.insert(name.name.clone()); } } LineageColumn::Single { .. } => {} LineageColumn::All { input_id: e_iid, except: e_e, } => { if *e_iid == input_id { // The two `All`s match and will erase each other. // The only remaining columns are those from the first wildcard // that are not excluded, but are excluded in the second wildcard. // Same assumption as above - input_id should be valid here. let input = self.find_input(input_id).unwrap(); let input_name = input.name.clone(); for remaining in e_e.difference(&except).sorted() { self.columns.push(LineageColumn::Single { name: Some(Ident { path: vec![input_name.clone()], name: remaining.clone(), }), target_id: input_id, target_name: Some(remaining.clone()), }) } continue 'within; } } } } self.columns.push(LineageColumn::All { input_id, except }); } } } return; } // special case: include a tuple if expr.ty.as_ref().is_some_and(|x| x.kind.is_tuple()) && expr.kind.is_ident() { // this ident is a tuple, which means it much point to an input let input_id = expr.target_id.unwrap(); self.columns.push(LineageColumn::All { input_id, except: Default::default(), }); return; } // special case: an ref that should be inlined because this node // might not exist in the resulting AST if let Some(target_id) = expr.target_id.filter(|_| inline_refs) { let ident = expr.kind.as_ident().unwrap().clone().pop_front().1.unwrap(); let input = &self.find_input(target_id); self.columns.push(if input.is_some() { LineageColumn::Single { target_name: Some(ident.name.clone()), name: Some(ident), target_id, } } else { LineageColumn::Single { target_name: None, name: Some(ident), target_id, } }); return; } // base case: define the expr as a new lineage column let (target_id, target_name) = (expr.id.unwrap(), None); let alias = expr.alias.as_ref().map(Ident::from_name); let name = alias.or_else(|| expr.kind.as_ident()?.clone().pop_front().1); // remove names from columns with the same name if name.is_some() { for c in &mut self.columns { if let LineageColumn::Single { name: n, .. } = c { if n.as_ref().map(|i| &i.name) == name.as_ref().map(|i| &i.name) { *n = None; } } } } self.columns.push(LineageColumn::Single { name, target_id, target_name, }); } pub fn find_input_by_name(&self, input_name: &str) -> Option<&LineageInput> { self.inputs.iter().find(|i| i.name == input_name) } pub fn find_input(&self, input_id: usize) -> Option<&LineageInput> { self.inputs.iter().find(|i| i.id == input_id) } /// Renames all frame inputs to the given alias. pub fn rename(&mut self, alias: String) { for input in &mut self.inputs { input.name.clone_from(&alias); } for col in &mut self.columns { match col { LineageColumn::All { .. } => {} LineageColumn::Single { name: Some(name), .. } => name.path = vec![alias.clone()], _ => {} } } } } /// Expects closure's args to be resolved. /// Note that named args are before positional args, in order of declaration. fn unpack(func_args: Vec) -> [Expr; P] { func_args.try_into().expect("bad special function cast") } mod from_text { use super::*; use crate::ir::rq::RelationLiteral; // TODO: Can we dynamically get the types, like in pandas? We need to put // quotes around strings and not around numbers. // https://stackoverflow.com/questions/64369887/how-do-i-read-csv-data-without-knowing-the-structure-at-compile-time pub fn parse_csv(text: &str) -> Result { let text = text.trim(); let mut rdr = csv::Reader::from_reader(text.as_bytes()); fn parse_header(row: &csv::StringRecord) -> Vec { row.into_iter().map(|x| x.to_string()).collect() } fn parse_row(row: csv::StringRecord) -> Vec { row.into_iter() .map(|x| Literal::String(x.to_string())) .collect() } Ok(RelationLiteral { columns: parse_header(rdr.headers().map_err(|e| e.to_string())?), rows: rdr .records() .map(|row_result| row_result.map(parse_row)) .try_collect() .map_err(|e| e.to_string())?, }) } type JsonFormat1Row = HashMap; #[derive(Deserialize)] struct JsonFormat2 { columns: Vec, data: Vec>, } fn map_json_primitive(primitive: serde_json::Value) -> Literal { use serde_json::Value::*; match primitive { Null => Literal::Null, Bool(bool) => Literal::Boolean(bool), Number(number) if number.is_i64() => Literal::Integer(number.as_i64().unwrap()), Number(number) if number.is_f64() => Literal::Float(number.as_f64().unwrap()), Number(_) => Literal::Null, String(string) => Literal::String(string), Array(_) => Literal::Null, Object(_) => Literal::Null, } } fn object_to_vec( mut row_map: HashMap, columns: &[String], ) -> Vec { columns .iter() .map(|c| { row_map .remove(c) .map(map_json_primitive) .unwrap_or(Literal::Null) }) .collect_vec() } pub fn parse_json(text: &str) -> Result { parse_json1(text).or_else(|err1| { parse_json2(text) .map_err(|err2| format!("While parsing rows: {err1}\nWhile parsing object: {err2}")) }) } fn parse_json1(text: &str) -> Result { let data: Vec = serde_json::from_str(text).map_err(|e| e.to_string())?; let mut columns = data .first() .ok_or("json: no rows")? .keys() .cloned() .collect_vec(); // JSON object keys are not ordered, so have to apply some order to produce // deterministic results columns.sort(); let rows = data .into_iter() .map(|row_map| object_to_vec(row_map, &columns)) .collect_vec(); Ok(RelationLiteral { columns, rows }) } fn parse_json2(text: &str) -> Result { let JsonFormat2 { columns, data } = serde_json::from_str(text).map_err(|x| x.to_string())?; Ok(RelationLiteral { columns, rows: data .into_iter() .map(|row| row.into_iter().map(map_json_primitive).collect_vec()) .collect_vec(), }) } } #[cfg(test)] mod tests { use insta::assert_yaml_snapshot; use crate::semantic::test::parse_resolve_and_lower; #[test] fn test_aggregate_positional_arg() { // distinct query #292 assert_yaml_snapshot!(parse_resolve_and_lower(" from c_invoice select invoice_no group invoice_no ( take 1 ) ").unwrap(), @r" def: version: ~ other: {} tables: - id: 0 name: ~ relation: kind: ExternRef: LocalTable: - c_invoice columns: - Single: invoice_no - Wildcard relation: kind: Pipeline: - From: source: 0 columns: - - Single: invoice_no - 0 - - Wildcard - 1 name: c_invoice prefer_cte: true - Select: - 0 - Take: range: start: ~ end: kind: Literal: Integer: 1 span: ~ partition: - 0 sort: [] - Select: - 0 columns: - Single: invoice_no "); // oops, two arguments #339 let result = parse_resolve_and_lower( " from c_invoice aggregate average amount ", ); assert!(result.is_err()); // oops, two arguments let result = parse_resolve_and_lower( " from c_invoice group issued_at (aggregate average amount) ", ); assert!(result.is_err()); // correct function call let ctx = crate::semantic::test::parse_and_resolve( " from c_invoice group issued_at ( aggregate (average amount) ) ", ) .unwrap(); let (res, _) = ctx.find_main_rel(&[]).unwrap().clone(); let expr = res.clone().into_relation_var().unwrap(); let expr = super::super::test::erase_ids(*expr); assert_yaml_snapshot!(expr); } #[test] fn test_transform_sort() { assert_yaml_snapshot!(parse_resolve_and_lower(" from invoices sort {issued_at, -amount, +num_of_articles} sort issued_at sort (-issued_at) sort {issued_at} sort {-issued_at} ").unwrap(), @r" def: version: ~ other: {} tables: - id: 0 name: ~ relation: kind: ExternRef: LocalTable: - invoices columns: - Single: issued_at - Single: amount - Single: num_of_articles - Wildcard relation: kind: Pipeline: - From: source: 0 columns: - - Single: issued_at - 0 - - Single: amount - 1 - - Single: num_of_articles - 2 - - Wildcard - 3 name: invoices prefer_cte: true - Sort: - direction: Asc column: 0 - direction: Desc column: 1 - direction: Asc column: 2 - Sort: - direction: Asc column: 0 - Sort: - direction: Desc column: 0 - Sort: - direction: Asc column: 0 - Sort: - direction: Desc column: 0 - Select: - 0 - 1 - 2 - 3 columns: - Single: issued_at - Single: amount - Single: num_of_articles - Wildcard "); } } ================================================ FILE: prqlc/prqlc/src/semantic/resolver/types.rs ================================================ use super::Resolver; use crate::codegen::{write_ty, write_ty_kind}; use crate::ir::pl::*; use crate::pr::{PrimitiveSet, Ty, TyKind, TyTupleField}; use crate::Result; use crate::{Error, Reason, WithErrorInfo}; impl Resolver<'_> { pub fn infer_type(expr: &Expr) -> Result> { if let Some(ty) = &expr.ty { return Ok(Some(ty.clone())); } let kind = match &expr.kind { ExprKind::Literal(ref literal) => match literal { Literal::Null => return Ok(None), Literal::Integer(_) => TyKind::Primitive(PrimitiveSet::Int), Literal::Float(_) => TyKind::Primitive(PrimitiveSet::Float), Literal::Boolean(_) => TyKind::Primitive(PrimitiveSet::Bool), Literal::String(_) => TyKind::Primitive(PrimitiveSet::Text), Literal::RawString(_) => TyKind::Primitive(PrimitiveSet::Text), Literal::Date(_) => TyKind::Primitive(PrimitiveSet::Date), Literal::Time(_) => TyKind::Primitive(PrimitiveSet::Time), Literal::Timestamp(_) => TyKind::Primitive(PrimitiveSet::Timestamp), Literal::ValueAndUnit(_) => return Ok(None), // TODO }, ExprKind::Ident(_) | ExprKind::FuncCall(_) => return Ok(None), ExprKind::SString(_) => return Ok(None), ExprKind::FString(_) => TyKind::Primitive(PrimitiveSet::Text), ExprKind::TransformCall(_) => return Ok(None), // TODO ExprKind::Tuple(fields) => { let mut ty_fields: Vec = Vec::with_capacity(fields.len()); let has_other = false; for field in fields { let ty = Resolver::infer_type(field)?; if field.flatten { if let Some(fields) = ty.as_ref().and_then(|x| x.kind.as_tuple()) { ty_fields.extend(fields.iter().cloned()); continue; } } // TODO: move this into de-sugar stage (expand PL) // TODO: this will not infer nested namespaces let name = field .alias .clone() .or_else(|| field.kind.as_ident().map(|i| i.name.clone())); ty_fields.push(TyTupleField::Single(name, ty)); } if has_other { ty_fields.push(TyTupleField::Wildcard(None)); } ty_tuple_kind(ty_fields) } ExprKind::Array(items) => { let mut item_tys = Vec::with_capacity(items.len()); for item in items { let item_ty = Resolver::infer_type(item)?; if let Some(item_ty) = item_ty { item_tys.push(item_ty); } } // TODO verify that types of all items are the same let items_ty = item_tys.into_iter().next().map(Box::new); TyKind::Array(items_ty) } _ => return Ok(None), }; Ok(Some(Ty { kind, name: None, span: None, })) } /// Validates that found node has expected type. Returns assumed type of the node. pub fn validate_expr_type( &mut self, found: &mut Expr, expected: Option<&Ty>, who: &F, ) -> Result<(), Error> where F: Fn() -> Option, { if expected.is_none() { // expected is none: there is no validation to be done return Ok(()); }; let Some(found_ty) = &mut found.ty else { // found is none: infer from expected if found.lineage.is_none() && expected.unwrap().is_relation() { // special case: infer a table type // inferred tables are needed for s-strings that represent tables // similarly as normal table references, we want to be able to infer columns // of this table, which means it needs to be defined somewhere // in the module structure. let Some(id) = found.id else { // Expression has no id - this happens with bare lambdas like `x -> y` let found_desc = match &found.kind { ExprKind::Func(_) => "a function".to_string(), kind => format!("{kind:?}"), }; return Err(Error::new(Reason::Expected { who: None, expected: "a table".to_string(), found: found_desc, }) .with_span(found.span)); }; let frame = self.declare_table_for_literal(id, None, found.alias.clone()); // override the empty frame with frame of the new table found.lineage = Some(frame) } // base case: infer expected type found.ty = expected.cloned(); return Ok(()); }; self.validate_type(found_ty, expected, who) .with_span(found.span) } /// Validates that found node has expected type. Returns assumed type of the node. pub fn validate_type( &mut self, found: &mut Ty, expected: Option<&Ty>, who: &F, ) -> Result<(), Error> where F: Fn() -> Option, { // infer let Some(expected) = expected else { // expected is none: there is no validation to be done return Ok(()); }; let expected_is_above = is_super_type_of(expected, found); if expected_is_above { return Ok(()); } // A temporary hack for allowing calling window functions from within // aggregate and derive. if expected.kind.is_array() && !found.kind.is_function() { return Ok(()); } Err(compose_type_error(found, expected, who)) } } pub fn ty_tuple_kind(fields: Vec) -> TyKind { let mut res: Vec = Vec::with_capacity(fields.len()); for field in fields { if let TyTupleField::Single(name, _) = &field { // remove names from previous fields with the same name if name.is_some() { for f in res.iter_mut() { if f.as_single().and_then(|x| x.0.as_ref()) == name.as_ref() { *f.as_single_mut().unwrap().0 = None; } } } } res.push(field); } TyKind::Tuple(res) } fn compose_type_error(found_ty: &mut Ty, expected: &Ty, who: &F) -> Error where F: Fn() -> Option, { fn display_ty(ty: &Ty) -> String { if ty.name.is_none() { if let TyKind::Tuple(fields) = &ty.kind { if fields.len() == 1 && fields[0].is_wildcard() { return "a tuple".to_string(); } } } format!("type `{}`", write_ty(ty)) } let who = who(); let is_join = who .as_ref() .map(|x| x.contains("std.join")) .unwrap_or_default(); let mut e = Error::new(Reason::Expected { who, expected: display_ty(expected), found: display_ty(found_ty), }); if found_ty.kind.is_function() && !expected.kind.is_function() { let found = found_ty.kind.as_function().unwrap(); let func_name = if let Some(func) = found { func.name_hint.as_ref() } else { None }; let to_what = func_name .map(|n| format!("to function {n}")) .unwrap_or_else(|| "in this function call?".to_string()); e = e.push_hint(format!("Argument might be missing {to_what}?")); } if is_join && found_ty.kind.is_tuple() && !expected.kind.is_tuple() { e = e.push_hint("Try using `(...)` instead of `{...}`"); } if let Some(expected_name) = &expected.name { let expanded = write_ty_kind(&expected.kind); e = e.push_hint(format!("Type `{expected_name}` expands to `{expanded}`")); } e } /// Analogous to [crate::ir::pl::Lineage::rename()] pub fn rename_relation(ty_kind: &mut TyKind, alias: String) { if let TyKind::Array(Some(items_ty)) = ty_kind { rename_tuples(&mut items_ty.kind, alias); } } fn rename_tuples(ty_kind: &mut TyKind, alias: String) { flatten_tuples(ty_kind); if let TyKind::Tuple(fields) = ty_kind { let inner_fields = std::mem::take(fields); let ty = Ty::new(TyKind::Tuple(inner_fields)); fields.push(TyTupleField::Single(Some(alias), Some(ty))); } } fn flatten_tuples(ty_kind: &mut TyKind) { if let TyKind::Tuple(fields) = ty_kind { let mut new_fields = Vec::new(); for field in fields.drain(..) { let TyTupleField::Single(name, Some(ty)) = field else { new_fields.push(field); continue; }; // recurse // let ty = ty.flatten_tuples(); let TyKind::Tuple(inner_fields) = ty.kind else { new_fields.push(TyTupleField::Single(name, Some(ty))); continue; }; new_fields.extend(inner_fields); } fields.extend(new_fields); } } pub fn is_super_type_of(superset: &Ty, subset: &Ty) -> bool { if superset.is_relation() && subset.is_relation() { return true; } is_super_type_of_kind(&superset.kind, &subset.kind) } pub fn is_super_type_of_opt(superset: Option<&Ty>, subset: Option<&Ty>) -> bool { let Some(subset) = subset else { return true; }; let Some(superset) = superset else { return true; }; is_super_type_of_kind(&superset.kind, &subset.kind) } pub fn is_sub_type_of_array(ty: &Ty) -> bool { let array = TyKind::Array(None); is_super_type_of_kind(&array, &ty.kind) } fn is_super_type_of_kind(superset: &TyKind, subset: &TyKind) -> bool { match (superset, subset) { (TyKind::Primitive(l0), TyKind::Primitive(r0)) => l0 == r0, (TyKind::Function(None), TyKind::Function(_)) => true, (TyKind::Function(Some(_)), TyKind::Function(None)) => true, (TyKind::Function(Some(sup)), TyKind::Function(Some(sub))) => { if is_not_super_type_of(sup.return_ty.as_deref(), sub.return_ty.as_deref()) { return false; } if sup.params.len() != sub.params.len() { return false; } for (sup_arg, sub_arg) in sup.params.iter().zip(&sub.params) { if is_not_super_type_of(sup_arg.as_ref(), sub_arg.as_ref()) { return false; } } true } (TyKind::Array(sup), TyKind::Array(sub)) => { is_super_type_of_opt(sup.as_deref(), sub.as_deref()) } (TyKind::Tuple(sup_tuple), TyKind::Tuple(sub_tuple)) => { let sup_has_wildcard = sup_tuple .iter() .any(|f| matches!(f, TyTupleField::Wildcard(_))); let sub_has_wildcard = sub_tuple .iter() .any(|f| matches!(f, TyTupleField::Wildcard(_))); let mut sup_fields = sup_tuple.iter().filter(|f| f.is_single()); let mut sub_fields = sub_tuple.iter().filter(|f| f.is_single()); loop { let sup = sup_fields.next(); let sub = sub_fields.next(); match (sup, sub) { (Some(TyTupleField::Single(_, sup)), Some(TyTupleField::Single(_, sub))) => { if is_not_super_type_of(sup.as_ref(), sub.as_ref()) { return false; } } (_, Some(_)) => { if !sup_has_wildcard { return false; } } (Some(_), None) => { if !sub_has_wildcard { return false; } } (None, None) => break, } } true } (l, r) => l == r, } } fn is_not_super_type_of(sup: Option<&Ty>, sub: Option<&Ty>) -> bool { if let Some(sub_ret) = sub { if let Some(sup_ret) = sup { if !is_super_type_of(sup_ret, sub_ret) { return true; } } } false } fn maybe_type_intersection(a: Option, b: Option) -> Option { match (a, b) { (Some(a), Some(b)) => Some(type_intersection(a, b)), (x, None) | (None, x) => x, } } pub fn type_intersection(a: Ty, b: Ty) -> Ty { match (a.kind, b.kind) { (a_kind, b_kind) if a_kind == b_kind => Ty { kind: a_kind, ..a }, // tuple (TyKind::Tuple(a_fields), TyKind::Tuple(b_fields)) => { type_intersection_of_tuples(a_fields, b_fields) } // array (TyKind::Array(Some(a)), TyKind::Array(Some(b))) => { Ty::new(TyKind::Array(Some(Box::new(type_intersection(*a, *b))))) } _ => todo!(), } } fn type_intersection_of_tuples(a: Vec, b: Vec) -> Ty { let a_has_other = a.iter().any(|f| f.is_wildcard()); let b_has_other = b.iter().any(|f| f.is_wildcard()); let mut a_fields = a.into_iter().filter_map(|f| f.into_single().ok()); let mut b_fields = b.into_iter().filter_map(|f| f.into_single().ok()); let mut fields = Vec::new(); let mut has_other = false; loop { match (a_fields.next(), b_fields.next()) { (None, None) => break, (None, Some(b_field)) => { if !a_has_other { todo!(); } has_other = true; fields.push(TyTupleField::Single(b_field.0, b_field.1)); } (Some(a_field), None) => { if !b_has_other { todo!(); } has_other = true; fields.push(TyTupleField::Single(a_field.0, a_field.1)); } (Some((a_name, a_ty)), Some((b_name, b_ty))) => { let name = match (a_name, b_name) { (Some(a), Some(b)) if a == b => Some(a), (None, None) | (Some(_), Some(_)) => None, (None, Some(n)) | (Some(n), None) => Some(n), }; let ty = maybe_type_intersection(a_ty, b_ty); fields.push(TyTupleField::Single(name, ty)); } } } if has_other { fields.push(TyTupleField::Wildcard(None)); } Ty::new(TyKind::Tuple(fields)) } ================================================ FILE: prqlc/prqlc/src/semantic/std.prql ================================================ # The PRQL standard library defines the following functions and transforms. # The definitions are whitespace insensitive, and have this form: # # ``` # let my_func = param1 param2 ... -> body_expr # ``` # # Where: # * `my_func` is the name of the function # * `param1` is the first parameter optionally followed by a type in "< ... >" # * `param2` etc. follow the same pattern as param1 # * `` is the type of result wrapped in "< ... >" # * `body_expr` defines the function body that creates the result. # It can be PRQL code or `internal ...` to indicate internal compiler code. # Operators let mul = left right -> internal std.mul let div_i = left right -> internal std.div_i let div_f = left right -> internal std.div_f let mod = left right -> internal std.mod let add = left right -> internal std.add let sub = left right -> internal std.sub let eq = left right -> internal std.eq let ne = left right -> internal std.ne let gt = left right -> internal std.gt let lt = left right -> internal std.lt let gte = left right -> internal std.gte let lte = left right -> internal std.lte let and = left right -> internal std.and let or = left right -> internal std.or let coalesce = left right -> internal std.coalesce let regex_search = text pattern -> internal std.regex_search let neg = expr -> internal std.neg let not = expr -> internal std.not # Types ## Type primitives type int = int type float = float type bool = bool type text = text type date = date type time = time type timestamp = timestamp type `func` = func ## Generic array type array = [] ## Generic relation type relation = [{..}] ## Range type range = {start = *, end = *} ## Transform type transform = func relation -> relation # Functions ## Relational transforms let from = func `default_db.source` -> source let select = func columns tbl -> internal select let filter = func condition tbl -> internal filter let derive = func columns tbl -> internal derive let aggregate = func columns tbl -> internal aggregate let sort = func by tbl -> internal sort let take = func expr tbl -> internal take let join = func `default_db.with` condition `noresolve.side`:inner tbl -> internal join let group = func by pipeline tbl -> internal group let window = func rows:0..-1 range:0..-1 expanding :false rolling :0 pipeline tbl -> internal window let append = `default_db.bottom` top -> internal append let intersect = `default_db.bottom` top -> ( t = top join (b = bottom) (tuple_every (tuple_map _eq (tuple_zip t.* b.*))) select t.* ) let remove = `default_db.bottom` top -> ( t = top join side:left (b = bottom) (tuple_every (tuple_map _eq (tuple_zip t.* b.*))) filter (tuple_every (tuple_map _is_null b.*)) select t.* ) let loop = func pipeline top -> internal loop ## Aggregate functions # These return either a scalar when used within `aggregate`, or a column when used anywhere else. let min = column -> internal std.min let max = column -> internal std.max let sum = column -> internal std.sum let average = column -> internal std.average let stddev = column -> internal std.stddev let all = column -> internal std.all let any = column -> internal std.any let concat_array = column -> internal std.concat_array # Counts number of items in the column. # Note that the count will include null values. let count = column -> internal count # Deprecated in favour of filtering input to the [std.count] function (not yet implemented). @{deprecated} let count_distinct = column -> internal std.count_distinct ## Window functions let lag = offset column -> internal std.lag let lead = offset column -> internal std.lead let first = column -> internal std.first let last = column -> internal std.last let rank = column -> internal std.rank let rank_dense = column -> internal std.rank_dense let row_number = column -> internal row_number # Mathematical functions module math { let abs = column -> internal std.math.abs let floor = column -> internal std.math.floor let ceil = column -> internal std.math.ceil let pi = -> internal std.math.pi let exp = column -> internal std.math.exp let ln = column -> internal std.math.ln let log10 = column -> internal std.math.log10 let log = func base column -> internal std.math.log let sqrt = column -> internal std.math.sqrt let degrees = column -> internal std.math.degrees let radians = column -> internal std.math.radians let cos = column -> internal std.math.cos let acos = column -> internal std.math.acos let sin = column -> internal std.math.sin let asin = column -> internal std.math.asin let tan = column -> internal std.math.tan let atan = column -> internal std.math.atan let pow = exponent column -> internal std.math.pow let round = n_digits column -> internal std.math.round } ## Misc functions let as = `noresolve.type` column -> internal std.as let in = pattern value -> internal in ## Tuple functions let tuple_every = func list -> internal tuple_every let tuple_map = func fn list -> internal tuple_map let tuple_zip = func a b -> internal tuple_zip let _eq = func a -> internal _eq let _is_null = func a -> _param.a == null ## Misc let from_text = input `noresolve.format`:csv -> internal from_text ## Text functions module text { let lower = column -> internal std.text.lower let upper = column -> internal std.text.upper let ltrim = column -> internal std.text.ltrim let rtrim = column -> internal std.text.rtrim let trim = column -> internal std.text.trim let length = column -> internal std.text.length let extract = offset length column -> internal std.text.extract let replace = pattern replacement column -> internal std.text.replace let starts_with = prefix column -> internal std.text.starts_with let contains = substr column -> internal std.text.contains let ends_with = suffix column -> internal std.text.ends_with } ## Date functions module date { let to_text = format column -> internal std.date.to_text } ## File-reading functions, primarily for DuckDB let read_parquet = func source binary_as_string :false file_row_number :false hive_partitioning :null union_by_name :false -> internal std.read_parquet let read_csv = source -> internal std.read_csv let read_json = source -> internal std.read_json ## PRQL compiler functions module `prql` { let version = -> internal prql_version } # Deprecated, will be removed in 0.12.0 let prql_version = -> internal prql_version ================================================ FILE: prqlc/prqlc/src/sql/dialect.rs ================================================ //! Feature map for SQL dialects. //! //! The general principle with is to strive to target only the generic (i.e. default) dialect. //! //! This means that we prioritize common dialects and old dialect versions, because such //! implementations would also be supported by newer versions. //! //! Dialect-specifics should be added only if: //! - the generic dialect is not supported (i.e. LIMIT is not supported in MS SQL), //! - dialect-specific impl is more performant than generic impl. //! //! As a consequence, generated SQL may be verbose, since it will avoid newer or less adopted SQL //! constructs. The upside is much less complex translator. use core::fmt::Debug; use std::any::{Any, TypeId}; use chrono::format::{Fixed, Item, Numeric, Pad, StrftimeItems}; use serde::{Deserialize, Serialize}; use sqlparser::ast::DateTimeField; use strum::VariantNames; use crate::{Error, Result}; /// Convert a chrono format `Item` back to its strftime string representation. fn chrono_item_to_strftime(item: &Item) -> String { let pad_char = |pad: &Pad| match pad { Pad::None => "-", Pad::Zero => "", Pad::Space => "_", }; let numeric_char = |num: &Numeric| -> String { match num { Numeric::Year => "Y", Numeric::YearMod100 => "y", Numeric::Month => "m", Numeric::Day => "d", Numeric::Hour => "H", Numeric::Hour12 => "I", Numeric::Minute => "M", Numeric::Second => "S", Numeric::Nanosecond => "f", _ => return format!("{num:?}"), } .to_string() }; match item { Item::Numeric(num, pad) => format!("%{}{}", pad_char(pad), numeric_char(num)), Item::Fixed(Fixed::ShortMonthName) => "%b".to_string(), Item::Fixed(Fixed::LongMonthName) => "%B".to_string(), Item::Fixed(Fixed::ShortWeekdayName) => "%a".to_string(), Item::Fixed(Fixed::LongWeekdayName) => "%A".to_string(), Item::Fixed(Fixed::UpperAmPm) => "%p".to_string(), Item::Fixed(Fixed::LowerAmPm) => "%P".to_string(), Item::Fixed(Fixed::RFC3339) => "%+".to_string(), _ => format!("{item:?}"), } } /// SQL dialect. /// /// This only changes the output for a relatively small subset of features. /// /// If something does not work in a specific dialect, please raise in a /// GitHub issue. // Make sure to update Python bindings, JS bindings & docs in the book. #[derive( Debug, PartialEq, Eq, Clone, Copy, Serialize, Default, Deserialize, strum::Display, strum::EnumIter, strum::EnumMessage, strum::EnumString, strum::VariantNames, )] #[strum(serialize_all = "lowercase")] pub enum Dialect { Ansi, BigQuery, ClickHouse, DuckDb, #[default] Generic, GlareDb, MsSql, MySql, Postgres, Redshift, SQLite, Snowflake, } // Is this the best approach for the Enum / Struct — basically that we have one // Enum that gets its respective Struct, and then the Struct can also get its // respective Enum? impl Dialect { pub(super) fn handler(&self) -> Box { match self { Dialect::MsSql => Box::new(MsSqlDialect), Dialect::MySql => Box::new(MySqlDialect), Dialect::BigQuery => Box::new(BigQueryDialect), Dialect::SQLite => Box::new(SQLiteDialect), Dialect::ClickHouse => Box::new(ClickHouseDialect), Dialect::Snowflake => Box::new(SnowflakeDialect), Dialect::DuckDb => Box::new(DuckDbDialect), Dialect::Postgres => Box::new(PostgresDialect), Dialect::Redshift => Box::new(RedshiftDialect), Dialect::GlareDb => Box::new(GlareDbDialect), Dialect::Ansi | Dialect::Generic => Box::new(GenericDialect), } } pub fn support_level(&self) -> SupportLevel { match self { Dialect::DuckDb | Dialect::SQLite | Dialect::Postgres | Dialect::Redshift | Dialect::MySql | Dialect::Generic | Dialect::GlareDb | Dialect::ClickHouse => SupportLevel::Supported, Dialect::MsSql | Dialect::Ansi | Dialect::BigQuery | Dialect::Snowflake => { SupportLevel::Unsupported } } } #[deprecated(note = "Use `Dialect::VARIANTS` instead")] pub fn names() -> &'static [&'static str] { Dialect::VARIANTS } } pub enum SupportLevel { Supported, Unsupported, Nascent, } #[derive(Debug)] pub struct GenericDialect; #[derive(Debug)] pub struct SQLiteDialect; #[derive(Debug)] pub struct MySqlDialect; #[derive(Debug)] pub struct MsSqlDialect; #[derive(Debug)] pub struct BigQueryDialect; #[derive(Debug)] pub struct ClickHouseDialect; #[derive(Debug)] pub struct SnowflakeDialect; #[derive(Debug)] pub struct DuckDbDialect; #[derive(Debug)] pub struct PostgresDialect; #[derive(Debug)] pub struct RedshiftDialect; #[derive(Debug)] pub struct GlareDbDialect; pub(super) enum ColumnExclude { Exclude, Except, } pub enum IdentQuotingStyle { AlwaysQuoted, ConditionallyQuoted, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum IntervalQuotingStyle { // INTERVAL 1 day NoQuotes, // INTERVAL '1' day ValueQuoted, // INTERVAL '1 day' ValueAndUnitQuoted, } pub(super) trait DialectHandler: Any + Debug { fn use_fetch(&self) -> bool { false } fn ident_quote(&self) -> char { '"' } fn ident_quoting_style(&self) -> IdentQuotingStyle { IdentQuotingStyle::ConditionallyQuoted } fn column_exclude(&self) -> Option { None } /// Support for DISTINCT in set ops (UNION DISTINCT, INTERSECT DISTINCT) /// When not supported we fallback to implicit DISTINCT. fn set_ops_distinct(&self) -> bool { true } /// Support or EXCEPT ALL. /// When not supported, fallback to anti join. fn except_all(&self) -> bool { true } fn intersect_all(&self) -> bool { self.except_all() } /// Support for CONCAT function. /// When not supported we fallback to use `||` as concat operator. fn has_concat_function(&self) -> bool { true } /// Whether or not intervals such as `INTERVAL 1 HOUR` require quotes like /// `INTERVAL '1 HOUR'` or `INTERVAL '1' HOUR` fn interval_quoting_style(&self, _dtf: &DateTimeField) -> IntervalQuotingStyle { IntervalQuotingStyle::NoQuotes } /// Support for GROUP BY * fn stars_in_group(&self) -> bool { true } fn supports_distinct_on(&self) -> bool { false } /// Get the date format for the given dialect /// PRQL uses the same format as `chrono` crate /// (see https://docs.rs/chrono/latest/chrono/format/strftime/index.html) fn translate_prql_date_format(&self, prql_date_format: &str) -> Result { Ok(StrftimeItems::new(prql_date_format) .map(|item| self.translate_chrono_item(item)) .collect::>>()? .join("")) } fn translate_chrono_item(&self, _item: Item) -> Result { Err(Error::new_simple( "Date formatting is not yet supported for this dialect", )) } fn supports_zero_columns(&self) -> bool { false } fn translate_sql_array( &self, elements: Vec, ) -> crate::Result { use sqlparser::ast::Expr; // Default SQL syntax: [elem1, elem2, ...] Ok(Expr::Array(sqlparser::ast::Array { elem: elements, named: false, })) } /// Whether source and subqueries should be put between simple parentheses /// for `UNION` and similar verbs. fn prefers_subquery_parentheses_shorthand(&self) -> bool { false } /// Whether window functions require an ORDER BY clause. /// Snowflake requires ORDER BY for ranking functions like ROW_NUMBER(). fn requires_order_by_in_window_function(&self) -> bool { false } } impl dyn DialectHandler { #[inline] pub fn is(&self) -> bool { TypeId::of::() == self.type_id() } } impl DialectHandler for GenericDialect { fn translate_chrono_item(&self, _item: Item) -> Result { Err(Error::new_simple("Date formatting requires a dialect")) } } impl DialectHandler for PostgresDialect { fn interval_quoting_style(&self, _dtf: &DateTimeField) -> IntervalQuotingStyle { IntervalQuotingStyle::ValueAndUnitQuoted } fn supports_distinct_on(&self) -> bool { true } // https://www.postgresql.org/docs/current/functions-formatting.html fn translate_chrono_item<'a>(&self, item: Item) -> Result { Ok(match item { Item::Numeric(Numeric::Year, Pad::Zero) => "YYYY".to_string(), Item::Numeric(Numeric::YearMod100, Pad::Zero) => "YY".to_string(), Item::Numeric(Numeric::Month, Pad::None) => "FMMM".to_string(), Item::Numeric(Numeric::Month, Pad::Zero) => "MM".to_string(), Item::Numeric(Numeric::Day, Pad::None) => "FMDD".to_string(), Item::Numeric(Numeric::Day, Pad::Zero) => "DD".to_string(), Item::Numeric(Numeric::Hour, Pad::None) => "FMHH24".to_string(), Item::Numeric(Numeric::Hour, Pad::Zero) => "HH24".to_string(), Item::Numeric(Numeric::Hour12, Pad::Zero) => "HH12".to_string(), Item::Numeric(Numeric::Minute, Pad::Zero) => "MI".to_string(), Item::Numeric(Numeric::Second, Pad::Zero) => "SS".to_string(), Item::Numeric(Numeric::Nanosecond, Pad::Zero) => "US".to_string(), // Microseconds Item::Fixed(Fixed::ShortMonthName) => "Mon".to_string(), // By default long names are blank-padded to 9 chars so we need to use FM prefix Item::Fixed(Fixed::LongMonthName) => "FMMonth".to_string(), Item::Fixed(Fixed::ShortWeekdayName) => "Dy".to_string(), Item::Fixed(Fixed::LongWeekdayName) => "FMDay".to_string(), Item::Fixed(Fixed::UpperAmPm) => "AM".to_string(), Item::Fixed(Fixed::RFC3339) => "YYYY-MM-DD\"T\"HH24:MI:SS.USZ".to_string(), Item::Literal(literal) => { // literals are split at every non alphanumeric character if literal.chars().any(|c| c.is_ascii_alphanumeric()) { // If the literal contains alphanumeric characters, we need to quote it // to avoid it being interpreted as a pattern understood by Postgres. // We hence need to put it in double quotes to force it to be interpreted as literal text format!("\"{literal}\"") } else { literal.replace('\'', "''").replace('"', "\\\"") } } Item::Space(spaces) => spaces.to_string(), _ => { return Err(Error::new_simple( "PRQL doesn't support this format specifier", )) } }) } fn supports_zero_columns(&self) -> bool { true } fn prefers_subquery_parentheses_shorthand(&self) -> bool { true } } impl DialectHandler for RedshiftDialect { fn ident_quoting_style(&self) -> IdentQuotingStyle { // Use conditional quoting with dialect-specific keywords IdentQuotingStyle::ConditionallyQuoted } fn interval_quoting_style(&self, dtf: &DateTimeField) -> IntervalQuotingStyle { if matches!(dtf, DateTimeField::Week(_) | DateTimeField::Weeks) { IntervalQuotingStyle::ValueAndUnitQuoted } else { IntervalQuotingStyle::ValueQuoted } } fn supports_distinct_on(&self) -> bool { false } // https://docs.aws.amazon.com/redshift/latest/dg/r_FORMAT_strings.html fn translate_chrono_item<'a>(&self, item: Item) -> Result { Ok(match item { Item::Numeric(Numeric::Year, Pad::Zero) => "YYYY".to_string(), Item::Numeric(Numeric::YearMod100, Pad::Zero) => "YY".to_string(), Item::Numeric(Numeric::Month, Pad::None) => "FMMM".to_string(), Item::Numeric(Numeric::Month, Pad::Zero) => "MM".to_string(), Item::Numeric(Numeric::Day, Pad::None) => "FMDD".to_string(), Item::Numeric(Numeric::Day, Pad::Zero) => "DD".to_string(), Item::Numeric(Numeric::Hour, Pad::None) => "FMHH24".to_string(), Item::Numeric(Numeric::Hour, Pad::Zero) => "HH24".to_string(), Item::Numeric(Numeric::Hour12, Pad::Zero) => "HH12".to_string(), Item::Numeric(Numeric::Minute, Pad::Zero) => "MI".to_string(), Item::Numeric(Numeric::Second, Pad::Zero) => "SS".to_string(), Item::Numeric(Numeric::Nanosecond, Pad::Zero) => "US".to_string(), // Microseconds Item::Fixed(Fixed::ShortMonthName) => "Mon".to_string(), // By default long names are blank-padded to 9 chars so we need to use FM prefix Item::Fixed(Fixed::LongMonthName) => "FMMonth".to_string(), Item::Fixed(Fixed::ShortWeekdayName) => "Dy".to_string(), Item::Fixed(Fixed::LongWeekdayName) => "FMDay".to_string(), Item::Fixed(Fixed::UpperAmPm) => "AM".to_string(), Item::Fixed(Fixed::RFC3339) => "YYYY-MM-DD\"T\"HH24:MI:SS.USZ".to_string(), Item::Literal(literal) => { // literals are split at every non alphanumeric character if literal.chars().any(|c| c.is_ascii_alphanumeric()) { // If the literal contains alphanumeric characters, we need to quote it // to avoid it being interpreted as a pattern understood by Redshift. // We hence need to put it in double quotes to force it to be interpreted as literal text format!("\"{literal}\"") } else { literal.replace('\'', "''").replace('"', "\\\"") } } Item::Space(spaces) => spaces.to_string(), _ => { return Err(Error::new_simple( "PRQL doesn't support this format specifier", )) } }) } fn supports_zero_columns(&self) -> bool { true } fn prefers_subquery_parentheses_shorthand(&self) -> bool { true } // Redshift only supports CONCAT with two elements, the docs recommend to use the || operator // for more than two elements: https://docs.aws.amazon.com/redshift/latest/dg/r_CONCAT.html fn has_concat_function(&self) -> bool { false } } impl DialectHandler for GlareDbDialect { fn interval_quoting_style(&self, _dtf: &DateTimeField) -> IntervalQuotingStyle { IntervalQuotingStyle::ValueAndUnitQuoted } } impl DialectHandler for SQLiteDialect { fn set_ops_distinct(&self) -> bool { false } fn except_all(&self) -> bool { false } fn has_concat_function(&self) -> bool { false } fn stars_in_group(&self) -> bool { false } } impl DialectHandler for MsSqlDialect { fn use_fetch(&self) -> bool { true } // https://learn.microsoft.com/en-us/sql/t-sql/language-elements/set-operators-except-and-intersect-transact-sql?view=sql-server-ver16 fn except_all(&self) -> bool { false } fn set_ops_distinct(&self) -> bool { false } // https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings fn translate_chrono_item<'a>(&self, item: Item) -> Result { Ok(match item { Item::Numeric(Numeric::Year, Pad::Zero) => "yyyy".to_string(), Item::Numeric(Numeric::YearMod100, Pad::Zero) => "yy".to_string(), Item::Numeric(Numeric::Month, Pad::None) => "M".to_string(), Item::Numeric(Numeric::Month, Pad::Zero) => "MM".to_string(), Item::Numeric(Numeric::Day, Pad::None) => "d".to_string(), Item::Numeric(Numeric::Day, Pad::Zero) => "dd".to_string(), Item::Numeric(Numeric::Hour, Pad::None) => "H".to_string(), Item::Numeric(Numeric::Hour, Pad::Zero) => "HH".to_string(), Item::Numeric(Numeric::Hour12, Pad::Zero) => "hh".to_string(), Item::Numeric(Numeric::Minute, Pad::Zero) => "mm".to_string(), Item::Numeric(Numeric::Second, Pad::Zero) => "ss".to_string(), Item::Numeric(Numeric::Nanosecond, Pad::Zero) => "ffffff".to_string(), // Microseconds Item::Fixed(Fixed::ShortMonthName) => "MMM".to_string(), Item::Fixed(Fixed::LongMonthName) => "MMMM".to_string(), Item::Fixed(Fixed::ShortWeekdayName) => "ddd".to_string(), Item::Fixed(Fixed::LongWeekdayName) => "dddd".to_string(), Item::Fixed(Fixed::UpperAmPm) => "tt".to_string(), Item::Fixed(Fixed::RFC3339) => "yyyy-MM-dd'T'HH:mm:ss.ffffff'Z'".to_string(), Item::Literal(literal) => { // literals are split at every non alphanumeric character if literal.chars().any(|c| c.is_ascii_alphanumeric()) { // If the literal contains alphanumeric characters, we need to quote it // to avoid it being interpreted as a pattern understood by MSSQL. // We hence need to put it in double quotes to force it to be interpreted as literal text format!("\"{literal}\"") } else { // MSSQL uses single quotes around literal .replace('"', "\\\"") .replace('\'', "\"\'\"") .replace('%', "\\%") } } Item::Space(spaces) => spaces.to_string(), _ => { return Err(Error::new_simple( "PRQL doesn't support this format specifier", )) } }) } } impl DialectHandler for MySqlDialect { fn ident_quote(&self) -> char { '`' } fn set_ops_distinct(&self) -> bool { // https://dev.mysql.com/doc/refman/8.0/en/set-operations.html true } // https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_date-format fn translate_chrono_item<'a>(&self, item: Item) -> Result { Ok(match item { Item::Numeric(Numeric::Year, Pad::Zero) => "%Y".to_string(), Item::Numeric(Numeric::YearMod100, Pad::Zero) => "%y".to_string(), Item::Numeric(Numeric::Month, Pad::None) => "%c".to_string(), Item::Numeric(Numeric::Month, Pad::Zero) => "%m".to_string(), Item::Numeric(Numeric::Day, Pad::None) => "%e".to_string(), Item::Numeric(Numeric::Day, Pad::Zero) => "%d".to_string(), Item::Numeric(Numeric::Hour, Pad::None) => "%k".to_string(), Item::Numeric(Numeric::Hour, Pad::Zero) => "%H".to_string(), Item::Numeric(Numeric::Hour12, Pad::Zero) => "%I".to_string(), Item::Numeric(Numeric::Minute, Pad::Zero) => "%i".to_string(), Item::Numeric(Numeric::Second, Pad::Zero) => "%S".to_string(), Item::Numeric(Numeric::Nanosecond, Pad::Zero) => "%f".to_string(), // Microseconds Item::Fixed(Fixed::ShortMonthName) => "%b".to_string(), Item::Fixed(Fixed::LongMonthName) => "%M".to_string(), Item::Fixed(Fixed::ShortWeekdayName) => "%a".to_string(), Item::Fixed(Fixed::LongWeekdayName) => "%W".to_string(), Item::Fixed(Fixed::UpperAmPm) => "%p".to_string(), Item::Fixed(Fixed::RFC3339) => "%Y-%m-%dT%H:%i:%S.%fZ".to_string(), Item::Literal(literal) => literal.replace('\'', "''").replace('%', "%%"), Item::Space(spaces) => spaces.to_string(), _ => { return Err(Error::new_simple( "PRQL doesn't support this format specifier", )) } }) } } impl DialectHandler for ClickHouseDialect { fn ident_quote(&self) -> char { '`' } fn supports_distinct_on(&self) -> bool { true } // https://clickhouse.com/docs/en/sql-reference/functions/date-time-functions#formatDateTimeInJodaSyntax fn translate_chrono_item<'a>(&self, item: Item) -> Result { Ok(match item { Item::Numeric(Numeric::Year, Pad::Zero) => "yyyy".to_string(), Item::Numeric(Numeric::YearMod100, Pad::Zero) => "yy".to_string(), Item::Numeric(Numeric::Month, Pad::None) => "M".to_string(), Item::Numeric(Numeric::Month, Pad::Zero) => "MM".to_string(), Item::Numeric(Numeric::Day, Pad::None) => "d".to_string(), Item::Numeric(Numeric::Day, Pad::Zero) => "dd".to_string(), Item::Numeric(Numeric::Hour, Pad::None) => "H".to_string(), Item::Numeric(Numeric::Hour, Pad::Zero) => "HH".to_string(), Item::Numeric(Numeric::Hour12, Pad::Zero) => "hh".to_string(), Item::Numeric(Numeric::Minute, Pad::Zero) => "mm".to_string(), Item::Numeric(Numeric::Second, Pad::Zero) => "ss".to_string(), Item::Numeric(Numeric::Nanosecond, Pad::Zero) => "SSSSSS".to_string(), // Microseconds Item::Fixed(Fixed::ShortMonthName) => "MMM".to_string(), Item::Fixed(Fixed::LongMonthName) => "MMMM".to_string(), Item::Fixed(Fixed::ShortWeekdayName) => "EEE".to_string(), Item::Fixed(Fixed::LongWeekdayName) => "EEEE".to_string(), Item::Fixed(Fixed::UpperAmPm) => "aa".to_string(), Item::Fixed(Fixed::RFC3339) => "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'".to_string(), Item::Literal(literal) => { // literals are split at every non alphanumeric character if literal.chars().any(|c| c.is_ascii_alphanumeric()) { // If the literal contains alphanumeric characters, we need to quote it // to avoid it being interpreted as a pattern understood by Clickhouse. // Clickhouse uses backticks around format!("'{literal}'") } else { literal.replace('\'', "\\'\\'") } } Item::Space(spaces) => spaces.to_string(), _ => { return Err(Error::new_simple( "PRQL doesn't support this format specifier", )) } }) } } impl DialectHandler for BigQueryDialect { fn ident_quote(&self) -> char { '`' } fn column_exclude(&self) -> Option { // https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax#select_except Some(ColumnExclude::Except) } fn set_ops_distinct(&self) -> bool { // https://cloud.google.com/bigquery/docs/reference/standard-sql/query-syntax#set_operators true } fn prefers_subquery_parentheses_shorthand(&self) -> bool { true } // https://cloud.google.com/bigquery/docs/reference/standard-sql/format-elements#format_elements_date_time fn translate_chrono_item<'a>(&self, item: Item) -> Result { Ok(match item { Item::Numeric(Numeric::Year, Pad::Zero) => "%Y".to_string(), Item::Numeric(Numeric::YearMod100, Pad::Zero) => "%y".to_string(), Item::Numeric(Numeric::Month, Pad::Zero) => "%m".to_string(), Item::Numeric(Numeric::Day, Pad::Zero) => "%d".to_string(), Item::Numeric(Numeric::Hour, Pad::Zero) => "%H".to_string(), Item::Numeric(Numeric::Hour12, Pad::Zero) => "%I".to_string(), Item::Numeric(Numeric::Minute, Pad::Zero) => "%M".to_string(), Item::Numeric(Numeric::Second, Pad::Zero) => "%S".to_string(), Item::Fixed(Fixed::ShortMonthName) => "%b".to_string(), Item::Fixed(Fixed::LongMonthName) => "%B".to_string(), Item::Fixed(Fixed::ShortWeekdayName) => "%a".to_string(), Item::Fixed(Fixed::LongWeekdayName) => "%A".to_string(), Item::Fixed(Fixed::UpperAmPm) => "%p".to_string(), Item::Fixed(Fixed::RFC3339) => "%Y-%m-%dT%H:%M:%S%Ez".to_string(), Item::Literal(literal) => literal.replace('\'', "''").replace('%', "%%"), Item::Space(spaces) => spaces.to_string(), item => { return Err(Error::new_simple(format!( "format specifier `{}` is not supported for BigQuery", chrono_item_to_strftime(&item), ))) } }) } } impl DialectHandler for SnowflakeDialect { fn ident_quoting_style(&self) -> IdentQuotingStyle { // Due to snowflake's identifier casing rules, identifiers are always quoted // https://docs.snowflake.com/en/sql-reference/identifiers-syntax#label-identifier-casing IdentQuotingStyle::AlwaysQuoted } fn column_exclude(&self) -> Option { // https://docs.snowflake.com/en/sql-reference/sql/select.html Some(ColumnExclude::Exclude) } fn set_ops_distinct(&self) -> bool { // https://docs.snowflake.com/en/sql-reference/operators-query.html false } fn interval_quoting_style(&self, _dtf: &DateTimeField) -> IntervalQuotingStyle { // Snowflake requires quotes around value and unit together // https://docs.snowflake.com/en/sql-reference/data-types-datetime#interval-constants IntervalQuotingStyle::ValueAndUnitQuoted } fn requires_order_by_in_window_function(&self) -> bool { // https://docs.snowflake.com/en/sql-reference/functions/row_number // ROW_NUMBER() requires ORDER BY in window specification true } } impl DialectHandler for DuckDbDialect { fn column_exclude(&self) -> Option { // https://duckdb.org/2022/05/04/friendlier-sql.html#select--exclude Some(ColumnExclude::Exclude) } fn except_all(&self) -> bool { // https://duckdb.org/docs/sql/query_syntax/setops.html false } fn supports_distinct_on(&self) -> bool { true } // https://duckdb.org/docs/sql/functions/dateformat fn translate_chrono_item<'a>(&self, item: Item) -> Result { Ok(match item { Item::Numeric(Numeric::Year, Pad::Zero) => "%Y".to_string(), Item::Numeric(Numeric::YearMod100, Pad::Zero) => "%y".to_string(), Item::Numeric(Numeric::Month, Pad::None) => "%-m".to_string(), Item::Numeric(Numeric::Month, Pad::Zero) => "%m".to_string(), Item::Numeric(Numeric::Day, Pad::None) => "%-d".to_string(), Item::Numeric(Numeric::Day, Pad::Zero) => "%d".to_string(), Item::Numeric(Numeric::Hour, Pad::None) => "%-H".to_string(), Item::Numeric(Numeric::Hour, Pad::Zero) => "%H".to_string(), Item::Numeric(Numeric::Hour12, Pad::Zero) => "%I".to_string(), Item::Numeric(Numeric::Minute, Pad::Zero) => "%M".to_string(), Item::Numeric(Numeric::Second, Pad::Zero) => "%S".to_string(), Item::Numeric(Numeric::Nanosecond, Pad::Zero) => "%f".to_string(), // Microseconds Item::Fixed(Fixed::ShortMonthName) => "%b".to_string(), Item::Fixed(Fixed::LongMonthName) => "%B".to_string(), Item::Fixed(Fixed::ShortWeekdayName) => "%a".to_string(), Item::Fixed(Fixed::LongWeekdayName) => "%A".to_string(), Item::Fixed(Fixed::UpperAmPm) => "%p".to_string(), Item::Fixed(Fixed::RFC3339) => "%Y-%m-%dT%H:%M:%S.%fZ".to_string(), Item::Literal(literal) => literal.replace('\'', "''").replace('%', "%%"), Item::Space(spaces) => spaces.to_string(), _ => { return Err(Error::new_simple( "PRQL doesn't support this format specifier", )) } }) } } #[cfg(test)] mod tests { use std::str::FromStr; use chrono::format::{Fixed, Item, Numeric, Pad}; use insta::{assert_debug_snapshot, assert_snapshot}; use super::{chrono_item_to_strftime, BigQueryDialect, Dialect, DialectHandler}; #[test] fn test_dialect_from_str() { assert_debug_snapshot!(Dialect::from_str("postgres"), @r" Ok( Postgres, ) "); assert_debug_snapshot!(Dialect::from_str("foo"), @r" Err( VariantNotFound, ) "); } // -- chrono_item_to_strftime tests -- #[test] fn chrono_item_to_strftime_numerics_zero_pad() { assert_snapshot!(chrono_item_to_strftime(&Item::Numeric(Numeric::Year, Pad::Zero)), @"%Y"); assert_snapshot!(chrono_item_to_strftime(&Item::Numeric(Numeric::YearMod100, Pad::Zero)), @"%y"); assert_snapshot!(chrono_item_to_strftime(&Item::Numeric(Numeric::Month, Pad::Zero)), @"%m"); assert_snapshot!(chrono_item_to_strftime(&Item::Numeric(Numeric::Day, Pad::Zero)), @"%d"); assert_snapshot!(chrono_item_to_strftime(&Item::Numeric(Numeric::Hour, Pad::Zero)), @"%H"); assert_snapshot!(chrono_item_to_strftime(&Item::Numeric(Numeric::Hour12, Pad::Zero)), @"%I"); assert_snapshot!(chrono_item_to_strftime(&Item::Numeric(Numeric::Minute, Pad::Zero)), @"%M"); assert_snapshot!(chrono_item_to_strftime(&Item::Numeric(Numeric::Second, Pad::Zero)), @"%S"); assert_snapshot!(chrono_item_to_strftime(&Item::Numeric(Numeric::Nanosecond, Pad::Zero)), @"%f"); } #[test] fn chrono_item_to_strftime_numerics_no_pad() { assert_snapshot!(chrono_item_to_strftime(&Item::Numeric(Numeric::Day, Pad::None)), @"%-d"); assert_snapshot!(chrono_item_to_strftime(&Item::Numeric(Numeric::Month, Pad::None)), @"%-m"); } #[test] fn chrono_item_to_strftime_numerics_space_pad() { assert_snapshot!(chrono_item_to_strftime(&Item::Numeric(Numeric::Day, Pad::Space)), @"%_d"); assert_snapshot!(chrono_item_to_strftime(&Item::Numeric(Numeric::Hour, Pad::Space)), @"%_H"); } #[test] fn chrono_item_to_strftime_numeric_unknown() { // Numeric variants not in the explicit list fall through to Debug format let result = chrono_item_to_strftime(&Item::Numeric(Numeric::Ordinal, Pad::Zero)); assert!(result.contains("Ordinal"), "got: {result}"); } #[test] fn chrono_item_to_strftime_fixed() { assert_snapshot!(chrono_item_to_strftime(&Item::Fixed(Fixed::ShortMonthName)), @"%b"); assert_snapshot!(chrono_item_to_strftime(&Item::Fixed(Fixed::LongMonthName)), @"%B"); assert_snapshot!(chrono_item_to_strftime(&Item::Fixed(Fixed::ShortWeekdayName)), @"%a"); assert_snapshot!(chrono_item_to_strftime(&Item::Fixed(Fixed::LongWeekdayName)), @"%A"); assert_snapshot!(chrono_item_to_strftime(&Item::Fixed(Fixed::UpperAmPm)), @"%p"); assert_snapshot!(chrono_item_to_strftime(&Item::Fixed(Fixed::LowerAmPm)), @"%P"); assert_snapshot!(chrono_item_to_strftime(&Item::Fixed(Fixed::RFC3339)), @"%+"); } #[test] fn chrono_item_to_strftime_fixed_unknown() { // Fixed variants not in the explicit list fall through to Debug format let result = chrono_item_to_strftime(&Item::Fixed(Fixed::TimezoneOffsetColon)); assert!(result.contains("TimezoneOffsetColon"), "got: {result}"); } #[test] fn chrono_item_to_strftime_non_numeric_non_fixed() { // Literal and Space items fall through to Debug format let result = chrono_item_to_strftime(&Item::Literal("-")); assert!(result.contains("Literal"), "got: {result}"); let result = chrono_item_to_strftime(&Item::Space(" ")); assert!(result.contains("Space"), "got: {result}"); } // -- BigQueryDialect::translate_chrono_item tests -- #[test] fn bigquery_translate_numeric_specifiers() { let bq = BigQueryDialect; assert_snapshot!(bq.translate_chrono_item(Item::Numeric(Numeric::Year, Pad::Zero)).unwrap(), @"%Y"); assert_snapshot!(bq.translate_chrono_item(Item::Numeric(Numeric::YearMod100, Pad::Zero)).unwrap(), @"%y"); assert_snapshot!(bq.translate_chrono_item(Item::Numeric(Numeric::Month, Pad::Zero)).unwrap(), @"%m"); assert_snapshot!(bq.translate_chrono_item(Item::Numeric(Numeric::Day, Pad::Zero)).unwrap(), @"%d"); assert_snapshot!(bq.translate_chrono_item(Item::Numeric(Numeric::Hour, Pad::Zero)).unwrap(), @"%H"); assert_snapshot!(bq.translate_chrono_item(Item::Numeric(Numeric::Hour12, Pad::Zero)).unwrap(), @"%I"); assert_snapshot!(bq.translate_chrono_item(Item::Numeric(Numeric::Minute, Pad::Zero)).unwrap(), @"%M"); assert_snapshot!(bq.translate_chrono_item(Item::Numeric(Numeric::Second, Pad::Zero)).unwrap(), @"%S"); } #[test] fn bigquery_translate_fixed_specifiers() { let bq = BigQueryDialect; assert_snapshot!(bq.translate_chrono_item(Item::Fixed(Fixed::ShortMonthName)).unwrap(), @"%b"); assert_snapshot!(bq.translate_chrono_item(Item::Fixed(Fixed::LongMonthName)).unwrap(), @"%B"); assert_snapshot!(bq.translate_chrono_item(Item::Fixed(Fixed::ShortWeekdayName)).unwrap(), @"%a"); assert_snapshot!(bq.translate_chrono_item(Item::Fixed(Fixed::LongWeekdayName)).unwrap(), @"%A"); assert_snapshot!(bq.translate_chrono_item(Item::Fixed(Fixed::UpperAmPm)).unwrap(), @"%p"); assert_snapshot!(bq.translate_chrono_item(Item::Fixed(Fixed::RFC3339)).unwrap(), @"%Y-%m-%dT%H:%M:%S%Ez"); } #[test] fn bigquery_translate_literal() { let bq = BigQueryDialect; assert_snapshot!(bq.translate_chrono_item(Item::Literal("-")).unwrap(), @"-"); assert_snapshot!(bq.translate_chrono_item(Item::Literal("/")).unwrap(), @"/"); // Single quotes are escaped by doubling assert_snapshot!(bq.translate_chrono_item(Item::Literal("'")).unwrap(), @"''"); // Percent signs are escaped by doubling assert_snapshot!(bq.translate_chrono_item(Item::Literal("%")).unwrap(), @"%%"); } #[test] fn bigquery_translate_space() { let bq = BigQueryDialect; assert_snapshot!(bq.translate_chrono_item(Item::Space(" ")).unwrap(), @" "); assert_snapshot!(bq.translate_chrono_item(Item::Space(" ")).unwrap(), @" "); } #[test] fn bigquery_translate_unsupported_specifier() { let bq = BigQueryDialect; // Nanosecond (%f) is not supported by BigQuery let err = bq .translate_chrono_item(Item::Numeric(Numeric::Nanosecond, Pad::Zero)) .unwrap_err(); assert_snapshot!(err.to_string(), @r#"Error { kind: Error, span: None, reason: Simple("format specifier `%f` is not supported for BigQuery"), hints: [], code: None }"#); // Non-zero padding is not supported by BigQuery let err = bq .translate_chrono_item(Item::Numeric(Numeric::Day, Pad::None)) .unwrap_err(); assert_snapshot!(err.to_string(), @r#"Error { kind: Error, span: None, reason: Simple("format specifier `%-d` is not supported for BigQuery"), hints: [], code: None }"#); let err = bq .translate_chrono_item(Item::Numeric(Numeric::Month, Pad::None)) .unwrap_err(); assert_snapshot!(err.to_string(), @r#"Error { kind: Error, span: None, reason: Simple("format specifier `%-m` is not supported for BigQuery"), hints: [], code: None }"#); // LowerAmPm (%P) is not supported by BigQuery let err = bq .translate_chrono_item(Item::Fixed(Fixed::LowerAmPm)) .unwrap_err(); assert_snapshot!(err.to_string(), @r#"Error { kind: Error, span: None, reason: Simple("format specifier `%P` is not supported for BigQuery"), hints: [], code: None }"#); } } /* ## Set operations support matrix Set-ops have quite different support in major SQL dialects. This is an attempt to document it. | SQL construct | SQLite | BQ | Postgres | MySQL 8+ | DuckDB |-------------------------------|---------|--------|----------|----------|-------- | UNION (implicit DISTINCT) | x | | x | x | x | UNION DISTINCT | | x | x | x | x | UNION ALL | x | x | x | x | x | EXCEPT (implicit DISTINCT) | x | | x | x | x | EXCEPT DISTINCT | | x | x | x | x | EXCEPT ALL | | | x | x | ### UNION DISTINCT For UNION, these are equivalent: - a UNION DISTINCT b, - DISTINCT (a UNION ALL b) - DISTINCT (a UNION ALL (DISTINCT b)) - DISTINCT ((DISTINCT a) UNION ALL b) - DISTINCT ((DISTINCT a) UNION ALL (DISTINCT b)) ### EXCEPT DISTINCT For EXCEPT it makes a difference when DISTINCT is applied. Below is a test query to validate the behavior. When applied before EXCEPT, the output should be [3] and when applied after EXCEPT, the output should be [2, 3]. ``` SELECT * FROM (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 2 UNION ALL SELECT 3) t EXCEPT SELECT * FROM (SELECT 1 UNION ALL SELECT 2) t; ``` All dialects seem to be applying *before*, but none seem to document that. ### INTERSECT DISTINCT For INTERSECT, it does not matter when DISTINCT is applied. BigQuery documentation does mention it is applied *after*, which makes me think there is a difference I'm not seeing. My reasoning is that: - Distinct is equivalent to applying `group * (take 1)`. - In effect, this is a restriction that "each group can have at most one value". - If we apply DISTINCT to any input of INTERSECT ALL, this restriction on the input is retained through the operation. That's because no group will not contain more values than it started with, and no group that was present in both inputs, will be missing from the output. - Thus, applying distinct after INTERSECT ALL is equivalent to applying it to any of the inputs. */ ================================================ FILE: prqlc/prqlc/src/sql/gen_expr.rs ================================================ //! Contains functions that compile [crate::pr::pl] nodes into [sqlparser] nodes. use std::cmp::Ordering; use itertools::Itertools; use prqlc_parser::generic::{InterpolateItem, Range}; use regex::Regex; use sqlparser::ast::{ self as sql_ast, BinaryOperator, DateTimeField, Fetch, Function, FunctionArg, FunctionArgExpr, FunctionArgumentList, ObjectName, OrderByExpr, SelectItem, UnaryOperator, Value, WindowFrameBound, WindowSpec, }; use super::gen_projection::try_into_exprs; use super::{keywords, Context}; use crate::ir::generic::{ColumnSort, SortDirection, WindowFrame, WindowKind}; use crate::ir::pl::{self, Ident, Literal}; use crate::ir::rq; use crate::sql::dialect::{IdentQuotingStyle, IntervalQuotingStyle}; use crate::sql::pq::context::ColumnDecl; use crate::utils::{valid_ident, OrMap}; use crate::{Error, Result, Span, WithErrorInfo}; pub(super) fn translate_expr(expr: rq::Expr, ctx: &mut Context) -> Result { Ok(match expr.kind { rq::ExprKind::ColumnRef(cid) => translate_cid(cid, ctx)?, // Fairly hacky — convert everything to a string, then concat it, // then convert to sql_ast::Expr. We can't use the `Item::sql_ast::Expr` code above // since we don't want to intersperse with spaces. rq::ExprKind::SString(s_string_items) => { let text = translate_sstring(s_string_items, ctx)?; ExprOrSource::Source(SourceExpr { text, binding_strength: 100, window_frame: false, }) } rq::ExprKind::Param(id) => ExprOrSource::Source(SourceExpr { text: format!("${id}"), binding_strength: 100, window_frame: false, }), rq::ExprKind::Literal(l) => translate_literal(l, ctx)?.into(), rq::ExprKind::Case(mut cases) => { let default = cases .last() .filter(|last| { matches!( last.condition.kind, rq::ExprKind::Literal(Literal::Boolean(true)) ) }) .map(|def| translate_expr(def.value.clone(), ctx)) .transpose()? .map(|x| x.into_ast()); if default.is_some() { cases.pop(); } let else_result = default .or(Some(sql_ast::Expr::Value(Value::Null.into()))) .map(Box::new); let conditions = cases .into_iter() .map(|case| -> Result<_> { let condition = translate_expr(case.condition, ctx)?.into_ast(); let result = translate_expr(case.value, ctx)?.into_ast(); Ok(sql_ast::CaseWhen { condition, result }) }) .try_collect()?; sql_ast::Expr::Case { case_token: sqlparser::ast::helpers::attached_token::AttachedToken::empty(), end_token: sqlparser::ast::helpers::attached_token::AttachedToken::empty(), operand: None, conditions, else_result, } .into() } rq::ExprKind::Operator { ref name, ref args } => { // A few special cases and then fall-through to the standard approach. match name.as_str() { // See notes in `std.rs` re whether we use names vs. // `FunctionDecl` vs. an Enum; and getting the correct // number of args from there. Currently the error messages // for the wrong number of args will be bad (though it's an // unusual case where RQ contains something like `std.eq` // with the wrong number of args). "std.eq" | "std.ne" => { if let [a, b] = args.as_slice() { if a.kind == rq::ExprKind::Literal(Literal::Null) || b.kind == rq::ExprKind::Literal(Literal::Null) { return Ok(process_null(name, args, ctx)?.into()); } else { let op = operator_from_name(name).unwrap(); return Ok(translate_binary_operator(a, b, op, ctx)?.into()); } } } "std.concat" => return Ok(process_concat(&expr, ctx)?.into()), "std.array_in" => return Ok(process_array_in(&expr, args, ctx)?.into()), "std.date.to_text" => { return Ok(process_date_to_text(&expr, name, args, ctx)?.into()) } _ => match try_into_between(expr.clone(), ctx)? { Some(between_expr) => return Ok(between_expr.into()), None => { if let Some(op) = operator_from_name(name) { if let [left, right] = args.as_slice() { return Ok(translate_binary_operator(left, right, op, ctx)?.into()); } } } }, } super::operators::translate_operator_expr(expr, ctx)? } rq::ExprKind::Array(exprs) => { let elements = exprs .iter() .map(|e| translate_expr(e.clone(), ctx).map(|x| x.into_ast())) .try_collect()?; let sql_array = ctx.dialect.translate_sql_array(elements)?; // Return as SourceExpr so it can be interpolated into s-strings ExprOrSource::Source(SourceExpr { text: sql_array.to_string(), binding_strength: 100, window_frame: false, }) } }) } /// Translates into IS NULL if possible fn process_null(name: &str, args: &[rq::Expr], ctx: &mut Context) -> Result { let (a, b) = (&args[0], &args[1]); let operand = if matches!(a.kind, rq::ExprKind::Literal(Literal::Null)) { b } else { a }; // If this were an Enum, we could match on it (see notes in `std.rs`). if name == "std.eq" { let strength = sql_ast::Expr::IsNull(Box::new(sql_ast::Expr::Value(Value::Null.into()))) .binding_strength(); let expr = translate_operand(operand.clone(), true, strength, Associativity::Both, ctx)?; let expr = Box::new(expr.into_ast()); Ok(sql_ast::Expr::IsNull(expr)) } else if name == "std.ne" { let strength = sql_ast::Expr::IsNotNull(Box::new(sql_ast::Expr::Value(Value::Null.into()))) .binding_strength(); let expr = translate_operand(operand.clone(), true, strength, Associativity::Both, ctx)?; let expr = Box::new(expr.into_ast()); Ok(sql_ast::Expr::IsNotNull(expr)) } else { unreachable!() } } /// Translates into IN (v1, v2, ...) if possible fn process_array_in( expr: &rq::Expr, args: &[rq::Expr], ctx: &mut Context, ) -> Result { match args { [col_expr @ rq::Expr { kind: rq::ExprKind::ColumnRef(_) | rq::ExprKind::Literal(_) | rq::ExprKind::SString(_) | rq::ExprKind::Param(_) | rq::ExprKind::Operator { name: _, args: _ }, .. }, rq::Expr { kind: rq::ExprKind::Array(in_values), .. }] => { if in_values.is_empty() { // We avoid producing `in ()` expressions since they are not syntactically valid // in some engines like PostgreSQL or MySQL. // We can instead optimize this to a condition that is always false Ok(sql_ast::Expr::Value(Value::Boolean(false).into())) } else { Ok(sql_ast::Expr::InList { expr: Box::new(translate_expr(col_expr.clone(), ctx)?.into_ast()), list: in_values .iter() .map(|a| Ok(translate_expr(a.clone(), ctx)?.into_ast())) .collect::>>()?, negated: false, }) } } _ => Err( Error::new_simple("args to `std.array_in` must be an expression and an array") .with_span(expr.span), ), } } /// Translates PRQL date format (based on `chrono` crate) to dialect specific date format /// For now only date format as string literal is supported fn process_date_to_text( expr: &rq::Expr, op_name: &str, args: &[rq::Expr], ctx: &mut Context, ) -> Result { if let [date_format_exp @ rq::Expr { kind: rq::ExprKind::Literal(Literal::String(date_format)), .. }, col_expr] = args { let expr = rq::Expr { kind: rq::ExprKind::Operator { name: op_name.to_string(), args: vec![ rq::Expr { kind: rq::ExprKind::Literal(Literal::String( ctx.dialect .translate_prql_date_format(date_format) .map_err(|e| e.with_span(date_format_exp.span))?, )), span: date_format_exp.span, }, col_expr.clone(), ], }, ..expr.clone() }; Ok(super::operators::translate_operator_expr(expr, ctx)?.into_ast()) } else { Err( Error::new_simple("`std.date.to_text` only supports a string literal as format") .with_span(expr.span), ) } } fn process_concat(expr: &rq::Expr, ctx: &mut Context) -> Result { if ctx.dialect.has_concat_function() { let concat_args = collect_concat_args(expr); let args_list = concat_args .iter() .map(|a| { translate_expr((*a).clone(), ctx) .map(|x| FunctionArg::Unnamed(FunctionArgExpr::Expr(x.into_ast()))) }) .try_collect()?; let args = sql_ast::FunctionArguments::List(FunctionArgumentList { args: args_list, clauses: vec![], duplicate_treatment: None, }); Ok(sql_ast::Expr::Function(Function { name: ObjectName(vec![sqlparser::ast::ObjectNamePart::Identifier( sql_ast::Ident::new("CONCAT"), )]), args, over: None, filter: None, null_treatment: None, within_group: vec![], parameters: sql_ast::FunctionArguments::None, uses_odbc_syntax: false, })) } else { let concat_args = collect_concat_args(expr); let mut iter = concat_args.into_iter(); let first_expr = iter.next().unwrap(); let mut current_expr = translate_expr(first_expr.clone(), ctx)?.into_ast(); for arg in iter { let translated_arg = translate_expr(arg.clone(), ctx)?.into_ast(); current_expr = sql_ast::Expr::BinaryOp { left: Box::new(current_expr), op: BinaryOperator::StringConcat, right: Box::new(translated_arg), }; } Ok(current_expr) } } fn translate_binary_operator( left: &rq::Expr, right: &rq::Expr, op: BinaryOperator, ctx: &mut Context, ) -> Result { let strength = op.binding_strength(); let left = translate_operand(left.clone(), true, strength, op.associativity(), ctx)?; let right = translate_operand(right.clone(), false, strength, op.associativity(), ctx)?; let left = Box::new(left.into_ast()); let right = Box::new(right.into_ast()); Ok(sql_ast::Expr::BinaryOp { left, op, right }) } fn collect_concat_args(expr: &rq::Expr) -> Vec<&rq::Expr> { match &expr.kind { rq::ExprKind::Operator { name, args } if name == "std.concat" => { args.iter().flat_map(collect_concat_args).collect() } _ => vec![expr], } } /// Translate expr into a BETWEEN statement if possible, otherwise returns the expr unchanged. fn try_into_between(expr: rq::Expr, ctx: &mut Context) -> Result> { match expr.kind { rq::ExprKind::Operator { name, args } if name == "std.and" => { let [a, b]: [_; 2] = args.try_into().unwrap(); match (a.kind, b.kind) { ( rq::ExprKind::Operator { name: a_name, args: a_args, }, rq::ExprKind::Operator { name: b_name, args: b_args, }, ) if a_name == "std.gte" && b_name == "std.lte" => { let [a_l, a_r]: [_; 2] = a_args.try_into().unwrap(); let [b_l, b_r]: [_; 2] = b_args.try_into().unwrap(); // We need for the values on each arm to be the same; e.g. x // > 3 and x < 5 if a_l == b_l { return Ok(Some(sql_ast::Expr::Between { expr: Box::new( translate_operand(a_l, true, 0, Associativity::Both, ctx)? .into_ast(), ), negated: false, low: Box::new( translate_operand(a_r, true, 0, Associativity::Both, ctx)? .into_ast(), ), high: Box::new( translate_operand(b_r, true, 0, Associativity::Both, ctx)? .into_ast(), ), })); } } _ => (), } } _ => (), } Ok(None) } fn operator_from_name(name: &str) -> Option { use BinaryOperator::*; match name { "std.mul" => Some(Multiply), "std.add" => Some(Plus), "std.sub" => Some(Minus), "std.eq" => Some(Eq), "std.ne" => Some(NotEq), "std.gt" => Some(Gt), "std.lt" => Some(Lt), "std.gte" => Some(GtEq), "std.lte" => Some(LtEq), "std.and" => Some(And), "std.or" => Some(Or), "std.concat" => Some(StringConcat), _ => None, } } pub(super) fn translate_literal(l: Literal, ctx: &Context) -> Result { Ok(match l { Literal::Null => sql_ast::Expr::Value(Value::Null.into()), Literal::String(s) | Literal::RawString(s) => { sql_ast::Expr::Value(Value::SingleQuotedString(s).into()) } Literal::Boolean(b) => sql_ast::Expr::Value(Value::Boolean(b).into()), Literal::Float(f) => sql_ast::Expr::Value(Value::Number(format!("{f:?}"), false).into()), Literal::Integer(i) => sql_ast::Expr::Value(Value::Number(format!("{i}"), false).into()), Literal::Date(value) => translate_datetime_literal(sql_ast::DataType::Date, value, ctx), Literal::Time(value) => translate_datetime_literal( sql_ast::DataType::Time(None, sql_ast::TimezoneInfo::None), value, ctx, ), Literal::Timestamp(value) => translate_datetime_literal( sql_ast::DataType::Timestamp(None, sql_ast::TimezoneInfo::None), value, ctx, ), Literal::ValueAndUnit(vau) => { let sql_parser_datetime = match vau.unit.as_str() { "years" => DateTimeField::Year, "months" => DateTimeField::Month, "weeks" => DateTimeField::Week(None), "days" => DateTimeField::Day, "hours" => DateTimeField::Hour, "minutes" => DateTimeField::Minute, "seconds" => DateTimeField::Second, "milliseconds" => DateTimeField::Millisecond, "microseconds" => DateTimeField::Microsecond, _ => { return Err(Error::new_simple(format!( "Unsupported interval unit: {}", vau.unit ))) } }; match ctx.dialect.interval_quoting_style(&sql_parser_datetime) { IntervalQuotingStyle::ValueAndUnitQuoted => { //postgres requires quotes around number and unit together eg '3 WEEK' let value = Box::new(sql_ast::Expr::Value( Value::SingleQuotedString(format!("{} {}", vau.n, sql_parser_datetime)) .into(), )); sql_ast::Expr::Interval(sqlparser::ast::Interval { value, leading_field: None, //set to none since field is now contained in string leading_precision: None, last_field: None, fractional_seconds_precision: None, }) } IntervalQuotingStyle::NoQuotes => { let value = Box::new(translate_literal(Literal::Integer(vau.n), ctx)?); sql_ast::Expr::Interval(sqlparser::ast::Interval { value, leading_field: Some(sql_parser_datetime), leading_precision: None, last_field: None, fractional_seconds_precision: None, }) } // Redshift requires quotes around the number only, otherwise months and years are // not supported. eg '3' MONTH IntervalQuotingStyle::ValueQuoted => { // Adding single quotes around the number let value = Box::new(sql_ast::Expr::Value( Value::SingleQuotedString(vau.n.to_string()).into(), )); sql_ast::Expr::Interval(sqlparser::ast::Interval { value, leading_field: Some(sql_parser_datetime), leading_precision: None, last_field: None, fractional_seconds_precision: None, }) } } } }) } fn translate_datetime_literal( data_type: sql_ast::DataType, value: String, ctx: &Context, ) -> sql_ast::Expr { if ctx.dialect.is::() { translate_datetime_literal_with_sqlite_function(data_type, value) } else { translate_datetime_literal_with_typed_string(data_type, value) } } fn translate_datetime_literal_with_typed_string( data_type: sql_ast::DataType, value: String, ) -> sql_ast::Expr { sql_ast::Expr::TypedString(sqlparser::ast::TypedString { data_type, value: sqlparser::ast::Value::SingleQuotedString(value).into(), uses_odbc_syntax: false, }) } fn translate_datetime_literal_with_sqlite_function( data_type: sql_ast::DataType, value: String, ) -> sql_ast::Expr { // TODO: promote parsing timezone handling to the parser; we should be storing // structured data rather than strings in the AST let timezone_indicator_regex = Regex::new(r"([+-]\d{2}):?(\d{2})$").unwrap(); let time_value = if let Some(groups) = timezone_indicator_regex.captures(value.as_str()) { // formalize the timezone indicator to be [+-]HH:MM // ref: https://www.sqlite.org/lang_datefunc.html timezone_indicator_regex .replace(&value, format!("{}:{}", &groups[1], &groups[2]).as_str()) .to_string() } else { value }; let arg = FunctionArg::Unnamed(FunctionArgExpr::Expr(sql_ast::Expr::Value( Value::SingleQuotedString(time_value).into(), ))); let func_name = match data_type { sql_ast::DataType::Date => data_type.to_string(), sql_ast::DataType::Time(..) => data_type.to_string(), sql_ast::DataType::Timestamp(..) => "DATETIME".to_string(), _ => unreachable!(), }; let args = sql_ast::FunctionArguments::List(FunctionArgumentList { args: vec![arg], clauses: vec![], duplicate_treatment: None, }); sql_ast::Expr::Function(Function { name: ObjectName(vec![sqlparser::ast::ObjectNamePart::Identifier( sql_ast::Ident::new(func_name), )]), args, over: None, filter: None, null_treatment: None, within_group: vec![], parameters: sql_ast::FunctionArguments::None, uses_odbc_syntax: false, }) } pub(super) fn translate_cid(cid: rq::CId, ctx: &mut Context) -> Result { if ctx.query.pre_projection { log::debug!("translating {cid:?} pre projection"); let decl = ctx.anchor.column_decls.get(&cid).expect("bad RQ ids"); Ok(match decl { ColumnDecl::Compute(compute) => { let window = compute.window.clone(); let span = compute.expr.span; let prev_wf = ctx.query.window_function; ctx.query.window_function = window.is_some(); let expr = translate_expr(compute.expr.clone(), ctx)?; ctx.query.window_function = prev_wf; if let Some(window) = window { translate_windowed(expr, window, ctx, span)? } else { expr } } ColumnDecl::RelationColumn(riid, _, col) => { let column = match col.clone() { rq::RelationColumn::Wildcard => translate_star(ctx, None)?, rq::RelationColumn::Single(name) => name.unwrap(), }; let t = &ctx.anchor.relation_instances[riid]; let table_ident = t.table_ref.name.clone().map(Ident::from_name); let ident = translate_ident(table_ident, Some(column), ctx); sql_ast::Expr::CompoundIdentifier(ident).into() } }) } else { // translate into ident let column_decl = &&ctx.anchor.column_decls[&cid]; let table_name = if let ColumnDecl::RelationColumn(riid, _, _) = column_decl { let t = &ctx.anchor.relation_instances[riid]; Some(t.table_ref.name.clone().unwrap()) } else { None }; let column = match &column_decl { ColumnDecl::RelationColumn(_, _, rq::RelationColumn::Wildcard) => { translate_star(ctx, None)? } _ => { let name = ctx.anchor.column_names.get(&cid).cloned(); name.expect("name of this column has not been to be set before generating SQL") } }; let ident = translate_ident(table_name.map(Ident::from_name), Some(column), ctx); log::debug!("translating {cid:?} post projection: {ident:?}"); let ident = sql_ast::Expr::CompoundIdentifier(ident); Ok(ident.into()) } } pub(super) fn translate_star(ctx: &Context, span: Option) -> Result { if !ctx.query.allow_stars { Err( Error::new_simple("Target dialect does not support * in this position.") .with_span(span), ) } else { Ok("*".to_string()) } } pub(super) fn translate_sstring( items: Vec>, ctx: &mut Context, ) -> Result { Ok(items .into_iter() .map(|s_string_item| match s_string_item { InterpolateItem::String(string) => Ok(string), InterpolateItem::Expr { expr, .. } => { translate_expr(*expr, ctx).map(|expr| expr.into_source()) } }) .collect::>>()? .join("")) } /// Aggregate several ordered ranges into one, computing the intersection. /// /// Returns a tuple of `(start, end)`, where `end` is optional. pub(super) fn range_of_ranges(ranges: Vec>) -> Result> { let mut current = Range::default(); for range in ranges { let mut range = try_range_into_int(range)?; // b = b + a.start -1 (take care of 1-based index!) range.start = range.start.or_map(current.start, |a, b| a + b - 1); range.end = range.end.map(|b| current.start.unwrap_or(1) + b - 1); // b.end = min(a.end, b.end) range.end = current.end.or_map(range.end, i64::min); current = range; } if let Some((s, e)) = current.start.zip(current.end) { if e < s { return Ok(Range { start: None, end: Some(0), }); } } Ok(current) } fn unpack_as_int_literal(bound: rq::Expr) -> Result { Some(bound.kind) .and_then(|x| x.into_literal().ok()) .and_then(|x| x.into_integer().ok()) .ok_or_else(|| Error::new_simple("expected an integer literal").with_span(bound.span)) } fn try_range_into_int(range: Range) -> Result> { Ok(Range { start: range.start.map(unpack_as_int_literal).transpose()?, end: range.end.map(unpack_as_int_literal).transpose()?, }) } pub(super) fn expr_of_i64(number: i64) -> sql_ast::Expr { sql_ast::Expr::Value(Value::Number(number.to_string(), number.leading_zeros() < 32).into()) } pub(super) fn fetch_of_i64(take: i64, ctx: &mut Context) -> Fetch { let kind = rq::ExprKind::Literal(Literal::Integer(take)); let expr = rq::Expr { kind, span: None }; Fetch { quantity: Some(translate_expr(expr, ctx).unwrap().into_ast()), with_ties: false, percent: false, } } pub(super) fn translate_select_item(cid: rq::CId, ctx: &mut Context) -> Result { let expr = translate_cid(cid, ctx)?.into_ast(); let inferred_name = match &expr { // sql_ast::Expr::Identifier is used for s-strings sql_ast::Expr::CompoundIdentifier(parts) => parts.last().map(|p| &p.value), _ => None, } .filter(|n| *n != "*"); let expected = ctx.anchor.column_names.get(&cid); if inferred_name != expected { // use expected name let ident = expected.cloned().unwrap_or_else(|| { // or use something that will not clash with other names ctx.anchor.col_name.gen() }); ctx.anchor.column_names.insert(cid, ident.to_string()); return Ok(SelectItem::ExprWithAlias { alias: translate_ident_part(ident, ctx), expr, }); } Ok(SelectItem::UnnamedExpr(expr)) } fn translate_windowed( expr: ExprOrSource, window: rq::Window, ctx: &mut Context, span: Option, ) -> Result { let default_frame = { let (kind, range) = if window.sort.is_empty() { (WindowKind::Rows, Range::unbounded()) } else { ( WindowKind::Range, Range { start: None, end: Some(rq::Expr { kind: rq::ExprKind::Literal(Literal::Integer(0)), span: None, }), }, ) }; WindowFrame { kind, range } }; let supports_frame = matches!( expr, ExprOrSource::Source(SourceExpr { window_frame: true, .. }) ); let mut order_by: Vec = (window.sort) .into_iter() .map(|sort| translate_column_sort(&sort, ctx)) .try_collect()?; // Some dialects (e.g., Snowflake) require ORDER BY for window functions // When no ORDER BY is specified, use ORDER BY 1 as a fallback if order_by.is_empty() && ctx.dialect.requires_order_by_in_window_function() { order_by.push(OrderByExpr { expr: sql_ast::Expr::Value(Value::Number("1".to_string(), false).into()), options: sqlparser::ast::OrderByOptions { asc: None, nulls_first: None, }, with_fill: None, }); } let window = WindowSpec { window_name: None, partition_by: try_into_exprs(window.partition, ctx, span)?, order_by, window_frame: if supports_frame && window.frame != default_frame { Some(try_into_window_frame(window.frame)?) } else { None }, }; let expr = expr.into_source(); Ok(ExprOrSource::Source(SourceExpr { text: format!("{expr} OVER ({window})"), binding_strength: 100, window_frame: false, })) } fn try_into_window_frame(frame: WindowFrame) -> Result { fn parse_bound(bound: rq::Expr) -> Result { let as_int = unpack_as_int_literal(bound)?; Ok(match as_int { 0 => WindowFrameBound::CurrentRow, 1.. => WindowFrameBound::Following(Some(Box::new(sql_ast::Expr::Value( sql_ast::Value::Number(as_int.to_string(), false).into(), )))), _ => WindowFrameBound::Preceding(Some(Box::new(sql_ast::Expr::Value( sql_ast::Value::Number((-as_int).to_string(), false).into(), )))), }) } Ok(sql_ast::WindowFrame { units: match frame.kind { WindowKind::Rows => sql_ast::WindowFrameUnits::Rows, WindowKind::Range => sql_ast::WindowFrameUnits::Range, }, start_bound: if let Some(start) = frame.range.start { parse_bound(start)? } else { WindowFrameBound::Preceding(None) }, end_bound: Some(if let Some(end) = frame.range.end { parse_bound(end)? } else { WindowFrameBound::Following(None) }), }) } pub(super) fn translate_column_sort( sort: &ColumnSort, ctx: &mut Context, ) -> Result { Ok(OrderByExpr { expr: translate_cid(sort.column, ctx)?.into_ast(), options: sqlparser::ast::OrderByOptions { asc: if matches!(sort.direction, SortDirection::Asc) { None // default order is ASC, so there is no need to emit it } else { Some(false) }, nulls_first: None, }, with_fill: None, }) } /// Translate a PRQL Ident to a Vec of SQL Idents. // We return a vec of SQL Idents because sqlparser sometimes uses // [ObjectName](sql_ast::ObjectName) and sometimes uses // [sql_ast::Expr::CompoundIdentifier](sql_ast::Expr::CompoundIdentifier), each of which // contains `Vec`. pub(super) fn translate_ident( table_ident: Option, column: Option, ctx: &Context, ) -> Vec { let mut parts = Vec::with_capacity(4); if !ctx.query.omit_ident_prefix || column.is_none() { if let Some(table) = table_ident { parts.extend(table); } } parts.extend(column); parts .into_iter() .map(|x| translate_ident_part(x, ctx)) .collect() } pub(super) fn translate_ident_part(ident: String, ctx: &Context) -> sql_ast::Ident { let is_bare = valid_ident().is_match(&ident); match ctx.dialect.ident_quoting_style() { IdentQuotingStyle::ConditionallyQuoted => { if is_bare && !keywords::is_keyword(&ident, &ctx.dialect_enum) { sql_ast::Ident::new(ident) } else { sql_ast::Ident::with_quote(ctx.dialect.ident_quote(), ident) } } IdentQuotingStyle::AlwaysQuoted => { sql_ast::Ident::with_quote(ctx.dialect.ident_quote(), ident) } } } pub(super) fn translate_operand( expr: rq::Expr, is_left: bool, parent_strength: i32, parent_associativity: Associativity, context: &mut Context, ) -> Result { let expr = translate_expr(expr, context)?; if needs_parentheses(&expr, is_left, parent_strength, parent_associativity) { Ok(expr.wrap_in_parenthesis()) } else { Ok(expr) } } /// For an operation represented as `a child b` with a surrounding parent /// operation (e.g., `(a child b) parent c` or `a parent (b child c)`): /// /// 1. When the child operator has higher precedence than the parent, /// parentheses *are not* required. /// /// 2. When the child operator has lower precedence than the parent, /// parentheses *are* required. /// /// 3. When the child and parent operators have the same precedence, the child /// is on the {left,right} and the parent is {left,right} associative, /// parentheses are not required. Some examples of when parentheses are not required: /// - `(a - b) - c` & `(a + b) - c` — as opposed to `a - (b - c)` /// - `a + (b - c)` & `a + (b + c)` — as opposed to `a - (b + c)` & `a - (b - c)` /// /// // // If it were possible to evaluate this with less context that would be // preferable, but it's not clear how to do that. (For example, even if we // passed a reference to the parent, that still wouldn't tell us whether the // child were on the left or the right, which is required...) // // Note that the code is deliberately somewhat verbose. While it could instead // be a neat single expression, it was quite difficult to work through, so // please do not make the code terser without being confident that it's easier // to understand. fn needs_parentheses( expr: &ExprOrSource, is_left: bool, parent_strength: i32, parent_associativity: Associativity, ) -> bool { let rule_3a = matches!(parent_associativity, Associativity::Both); let rule_3b_left = is_left && parent_associativity.left_associative(); let rule_3b_right = !is_left && parent_associativity.right_associative(); match expr.binding_strength().cmp(&parent_strength) { // Rule 1 Ordering::Greater => false, // Rule 2 Ordering::Less => true, // Rule 3 Ordering::Equal => !(rule_3a || rule_3b_left || rule_3b_right), } } /// Associativity of an expression's operator. /// Note that there's no exponent symbol in SQL, so we don't seem to require a `Right` variant. /// https://en.wikipedia.org/wiki/Operator_associativity #[allow(dead_code)] #[derive(Debug, PartialEq, Eq)] pub enum Associativity { Left, /// `Both` means mathematically associative, like `+` or `*` Both, Right, } impl Associativity { /// Returns true iff `a + b + c = (a + b) + c` fn left_associative(&self) -> bool { matches!(self, Associativity::Left | Associativity::Both) } /// Returns true iff `a + b + c = a + (b + c)` fn right_associative(&self) -> bool { matches!(self, Associativity::Right | Associativity::Both) } } trait SQLExpression { /// Returns binding strength of a SQL expression /// https://www.postgresql.org/docs/14/sql-syntax-lexical.html#id-1.5.3.5.13.2 /// https://docs.microsoft.com/en-us/sql/t-sql/language-elements/operator-precedence-transact-sql?view=sql-server-ver16 fn binding_strength(&self) -> i32; /// Default to `Both`, but expected to be overwritten by concrete types fn associativity(&self) -> Associativity { Associativity::Both } } impl SQLExpression for sql_ast::Expr { fn binding_strength(&self) -> i32 { // Strength of an expression depends only on the top-level operator, because all // other nested expressions can only have lower strength match self { sql_ast::Expr::BinaryOp { op, .. } => op.binding_strength(), sql_ast::Expr::UnaryOp { op, .. } => op.binding_strength(), sql_ast::Expr::Like { .. } | sql_ast::Expr::ILike { .. } => 7, sql_ast::Expr::IsNull(_) | sql_ast::Expr::IsNotNull(_) => 5, // all other items types bind stronger (function calls, literals, ...) _ => 20, } } fn associativity(&self) -> Associativity { match self { sql_ast::Expr::BinaryOp { op, .. } => op.associativity(), sql_ast::Expr::UnaryOp { op, .. } => op.associativity(), _ => Associativity::Both, } } } impl SQLExpression for BinaryOperator { fn binding_strength(&self) -> i32 { use BinaryOperator::*; match self { Modulo | Multiply | Divide => 11, Minus | Plus => 10, Gt | Lt | GtEq | LtEq | Eq | NotEq => 6, And => 3, Or => 2, _ => 9, } } fn associativity(&self) -> Associativity { use BinaryOperator::*; match self { Minus | Divide | Modulo => Associativity::Left, _ => Associativity::Both, } } } impl SQLExpression for UnaryOperator { fn binding_strength(&self) -> i32 { match self { UnaryOperator::Minus | UnaryOperator::Plus => 13, UnaryOperator::Not => 4, _ => 9, } } } /// A wrapper around sql_ast::Expr, that may have already been converted to source. #[derive(Debug, Clone)] pub enum ExprOrSource { Expr(Box), Source(SourceExpr), } #[derive(Debug, Clone)] pub struct SourceExpr { pub text: String, pub binding_strength: i32, /// True for expressions that support (and need) the window frame (OVER ...) pub window_frame: bool, } impl ExprOrSource { pub fn into_ast(self) -> sql_ast::Expr { match self { ExprOrSource::Expr(ast) => *ast, ExprOrSource::Source(SourceExpr { text: source, .. }) => { // The s-string hack sql_ast::Expr::Identifier(sql_ast::Ident::new(source)) } } } pub fn into_source(self) -> String { match self { ExprOrSource::Expr(e) => e.to_string(), ExprOrSource::Source(SourceExpr { text, .. }) => text, } } fn wrap_in_parenthesis(self) -> Self { match self { ExprOrSource::Expr(expr) => ExprOrSource::Expr(Box::new(sql_ast::Expr::Nested(expr))), ExprOrSource::Source(SourceExpr { text, window_frame, .. }) => { let text = format!("({text})"); ExprOrSource::Source(SourceExpr { text, binding_strength: 100, window_frame, }) } } } } impl SQLExpression for ExprOrSource { fn binding_strength(&self) -> i32 { match self { ExprOrSource::Expr(expr) => expr.binding_strength(), ExprOrSource::Source(SourceExpr { binding_strength, .. }) => *binding_strength, } } } impl From for ExprOrSource { fn from(value: sql_ast::Expr) -> Self { ExprOrSource::Expr(Box::new(value)) } } #[cfg(test)] mod test { use insta::assert_yaml_snapshot; use super::*; #[test] fn test_range_of_ranges() -> Result<()> { fn from_ints(start: Option, end: Option) -> Range { let start = start.map(|x| rq::Expr { kind: rq::ExprKind::Literal(Literal::Integer(x)), span: None, }); let end = end.map(|x| rq::Expr { kind: rq::ExprKind::Literal(Literal::Integer(x)), span: None, }); Range { start, end } } let range_1_10 = from_ints(Some(1), Some(10)); let range_5_6 = from_ints(Some(5), Some(6)); let range_5_inf = from_ints(Some(5), None); let range_inf_8 = from_ints(None, Some(8)); let range_5_5 = from_ints(Some(5), Some(5)); assert!(range_of_ranges(vec![range_1_10.clone()])?.end.is_some()); assert_yaml_snapshot!(range_of_ranges(vec![range_1_10.clone()])?, @r" start: 1 end: 10 "); assert_yaml_snapshot!(range_of_ranges(vec![range_1_10.clone(), range_1_10.clone()])?, @r" start: 1 end: 10 "); assert_yaml_snapshot!(range_of_ranges(vec![range_1_10.clone(), range_5_6.clone()])?, @r" start: 5 end: 6 "); assert_yaml_snapshot!(range_of_ranges(vec![range_5_6.clone(), range_1_10.clone()])?, @r" start: 5 end: 6 "); // empty range assert_yaml_snapshot!(range_of_ranges(vec![range_5_6.clone(), range_5_6.clone()])?, @r" start: ~ end: 0 "); assert_yaml_snapshot!(range_of_ranges(vec![range_5_inf.clone(), range_5_inf.clone()])?, @r" start: 9 end: ~ "); assert_yaml_snapshot!(range_of_ranges(vec![range_1_10, range_5_inf])?, @r" start: 5 end: 10 "); assert_yaml_snapshot!(range_of_ranges(vec![range_5_6, range_inf_8.clone()])?, @r" start: 5 end: 6 "); assert_yaml_snapshot!(range_of_ranges(vec![range_inf_8.clone(), range_inf_8])?, @r" start: ~ end: 8 "); assert_yaml_snapshot!(range_of_ranges(vec![range_5_5])?, @r" start: 5 end: 5 "); Ok(()) } } ================================================ FILE: prqlc/prqlc/src/sql/gen_projection.rs ================================================ use std::collections::{HashMap, HashSet}; use itertools::Itertools; use sqlparser::ast::{ self as sql_ast, ExceptSelectItem, ExcludeSelectItem, ObjectName, SelectItem, WildcardAdditionalOptions, }; use super::dialect::ColumnExclude; use super::gen_expr::*; use super::pq::context::{AnchorContext, ColumnDecl}; use super::Context; use crate::ir::pl::Ident; use crate::ir::rq::{CId, RelationColumn}; use crate::Result; use crate::{Error, Span, WithErrorInfo}; pub(super) fn try_into_exprs( cids: Vec, ctx: &mut Context, span: Option, ) -> Result> { let (cids, excluded) = translate_wildcards(&ctx.anchor, cids); let mut res = Vec::new(); for cid in cids { let decl = ctx.anchor.column_decls.get(&cid).unwrap(); let ColumnDecl::RelationColumn(riid, _, RelationColumn::Wildcard) = decl else { // base case res.push(translate_cid(cid, ctx)?.into_ast()); continue; }; // star let t = &ctx.anchor.relation_instances[riid]; let table_name = t.table_ref.name.clone().map(Ident::from_name); let ident = translate_star(ctx, span)?; if let Some(excluded) = excluded.get(&cid) { if !excluded.is_empty() { return Err( Error::new_simple("Excluding columns not supported as this position") .with_span(span), ); } } let ident = translate_ident(table_name, Some(ident), ctx); res.push(sql_ast::Expr::CompoundIdentifier(ident)); } Ok(res) } type Excluded = HashMap>; /// Convert RQ wildcards to SQL stars. /// Note that they don't have the same semantics: /// - wildcard means "other columns that we don't have the knowledge of" /// - star means "all columns of the table" /// pub(super) fn translate_wildcards(ctx: &AnchorContext, cols: Vec) -> (Vec, Excluded) { let mut star = None; let mut excluded: Excluded = HashMap::new(); // When compiling: // from employees | group department (take 3) // Row number will be computed in a CTE that also contains a star. // In the main query, star will also include row number, which was not // requested. // This function adds that column to the exclusion tuple. fn exclude(star: &mut Option<(CId, HashSet)>, excluded: &mut Excluded) { let Some((cid, in_star)) = star.take() else { return; }; if in_star.is_empty() { return; } excluded.insert(cid, in_star); } let mut output = Vec::new(); for cid in cols { // don't use cols that have been included by preceding star let in_star = star .as_mut() .map(|s: &mut (CId, HashSet)| s.1.remove(&cid)) .unwrap_or_default(); if in_star { continue; } if let ColumnDecl::RelationColumn(riid, _, col) = &ctx.column_decls[&cid] { if matches!(col, RelationColumn::Wildcard) { exclude(&mut star, &mut excluded); let relation_instance = &ctx.relation_instances[riid]; let mut in_star: HashSet<_> = relation_instance.original_cids.iter().cloned().collect(); in_star.remove(&cid); star = Some((cid, in_star)); // remove preceding cols that will be included with this star if let Some((_, in_star)) = &mut star { while let Some(prev) = output.pop() { if !in_star.remove(&prev) { output.push(prev); break; } } } } } output.push(cid); } exclude(&mut star, &mut excluded); (output, excluded) } fn deduplicate_select_items(items: &mut Vec) { // Dropping all duplicated identifiers let mut seen = HashSet::new(); items.retain(|select_item| match select_item { SelectItem::UnnamedExpr(sql_ast::Expr::CompoundIdentifier(idents)) => { // If any of the identifiers hadn't been seen yet, retain the expr idents.iter().any(|ident| seen.insert(ident.clone())) } SelectItem::ExprWithAlias { alias, .. } => seen.insert(alias.clone()), _ => true, }); } pub(super) fn translate_select_items( cols: Vec, mut excluded: Excluded, ctx: &mut Context, ) -> Result> { let mut res: Vec<_> = cols .into_iter() .map(|cid| { let decl = ctx.anchor.column_decls.get(&cid).unwrap(); let ColumnDecl::RelationColumn(riid, _, RelationColumn::Wildcard) = decl else { // general case return translate_select_item(cid, ctx); }; // wildcard case let t = &ctx.anchor.relation_instances[riid]; let table_name = t.table_ref.name.clone().map(Ident::from_name); let ident = translate_ident(table_name, Some("*".to_string()), ctx); // excluded columns let opts = (excluded.remove(&cid)) .and_then(|excluded| translate_exclude(ctx, excluded)) .unwrap_or_default(); Ok(if ident.len() > 1 { let mut object_name = ident; object_name.pop(); SelectItem::QualifiedWildcard( sqlparser::ast::SelectItemQualifiedWildcardKind::ObjectName(ObjectName( object_name .into_iter() .map(sqlparser::ast::ObjectNamePart::Identifier) .collect(), )), opts, ) } else { SelectItem::Wildcard(opts) }) }) .try_collect()?; deduplicate_select_items(&mut res); if res.is_empty() && !ctx.dialect.supports_zero_columns() { // In some cases, no columns will appear in the projection // for SQL to parse correctly, we inject a `NULL`. // This is not strictly correct and should probably generate an error // instead. // Example: `from x | take 10 | aggregate { count this }`. // Here, first SELECT does not need to emit any columns as we don't need // any since we just count the number of rows. res.push(SelectItem::UnnamedExpr(sql_ast::Expr::Value( sql_ast::Value::Null.into(), ))); } Ok(res) } fn translate_exclude( ctx: &mut Context, excluded: HashSet, ) -> Option { let excluded = as_col_names(&excluded, &ctx.anchor); let Some(supported) = ctx.dialect.column_exclude() else { // TODO: eventually this should throw an error // I don't want to do this now, because we have no way around it. // We could also ask the user to add table definitions. if log::log_enabled!(log::Level::Warn) { let excluded = excluded.join(", "); log::warn!("Columns {excluded} will be included with *, but were not requested.") } return None; }; let mut excluded = excluded .into_iter() .map(|name| translate_ident_part(name.to_string(), ctx)) .collect_vec(); Some(match supported { ColumnExclude::Exclude => WildcardAdditionalOptions { opt_exclude: Some(ExcludeSelectItem::Multiple(excluded)), ..Default::default() }, ColumnExclude::Except => WildcardAdditionalOptions { opt_except: Some(ExceptSelectItem { first_element: excluded.remove(0), additional_elements: excluded, }), ..Default::default() }, }) } fn as_col_names<'a>(cids: &'a HashSet, ctx: &'a AnchorContext) -> Vec<&'a str> { cids.iter() .sorted_by_key(|c| c.get()) .map(|c| { ctx.column_decls .get(c) .and_then(|c| match c { ColumnDecl::RelationColumn(_, _, rc) => rc.as_single().map(|o| o.as_ref()), _ => None, }) .flatten() .map(|n| n.as_str()) .unwrap_or("") }) .collect_vec() } ================================================ FILE: prqlc/prqlc/src/sql/gen_query.rs ================================================ //! This module is responsible for translating PRQL AST to sqlparser AST, and //! then to a String. We use sqlparser because it's trivial to create the string //! once it's in their AST (it's just `.to_string()`). It also lets us support a //! few dialects of SQL immediately. use itertools::Itertools; use regex::Regex; use sqlparser::ast::{ self as sql_ast, Join, JoinConstraint, JoinOperator, Select, SelectItem, SetExpr, TableAlias, TableFactor, TableWithJoins, }; use super::gen_expr::*; use super::gen_projection::*; use super::operators::translate_operator; use super::pq::ast::{Cte, CteKind, RelationExpr, RelationExprKind, SqlRelation, SqlTransform}; use super::{Context, Dialect}; use crate::debug; use crate::ir::pl::{JoinSide, Literal}; use crate::ir::rq::{CId, Expr, ExprKind, RelationLiteral, RelationalQuery}; use crate::utils::{BreakUp, Pluck}; use crate::{Error, Result, WithErrorInfo}; use prqlc_parser::generic::InterpolateItem; type Transform = SqlTransform; pub fn translate_query(query: RelationalQuery, dialect: Option) -> Result { // compile from RQ to PQ let (pq_query, mut ctx) = super::pq::compile_query(query, dialect)?; debug::log_stage(debug::Stage::Sql(debug::StageSql::Main)); let mut query = translate_relation(pq_query.main_relation, &mut ctx)?; if !pq_query.ctes.is_empty() { // attach CTEs let mut cte_tables = Vec::new(); let mut recursive = false; for cte in pq_query.ctes { let (cte, rec) = translate_cte(cte, &mut ctx)?; cte_tables.push(cte); recursive = recursive || rec; } query.with = Some(sql_ast::With { recursive, cte_tables, with_token: sqlparser::ast::helpers::attached_token::AttachedToken::empty(), }); } debug::log_entry(|| debug::DebugEntryKind::ReprSqlParser(Box::new(query.clone()))); Ok(query) } fn translate_relation(relation: SqlRelation, ctx: &mut Context) -> Result { match relation { SqlRelation::AtomicPipeline(pipeline) => translate_pipeline(pipeline, ctx), SqlRelation::Literal(data) => translate_relation_literal(data, ctx), SqlRelation::SString(items) => translate_query_sstring(items, ctx), SqlRelation::Operator { name, args } => translate_query_operator(name, args, ctx), } } fn translate_pipeline(pipeline: Vec, ctx: &mut Context) -> Result { use SqlTransform::*; let (select, set_ops) = pipeline.break_up(|t| matches!(t, Union { .. } | Except { .. } | Intersect { .. })); let select = translate_select_pipeline(select, ctx)?; translate_set_ops_pipeline(select, set_ops, ctx) } fn translate_select_pipeline( mut pipeline: Vec, ctx: &mut Context, ) -> Result { let table_count = count_tables(&pipeline); log::debug!("atomic query contains {table_count} tables"); ctx.push_query(); ctx.query.omit_ident_prefix = table_count == 1; ctx.query.pre_projection = true; let mut from: Vec<_> = pipeline .pluck(|t| t.into_from()) .into_iter() .map(|source| -> Result { Ok(TableWithJoins { relation: translate_relation_expr(source, ctx)?, joins: vec![], }) }) .try_collect()?; let joins = pipeline .pluck(|t| t.into_join()) .into_iter() .map(|j| translate_join(j, ctx)) .collect::>>()?; if !joins.is_empty() { if let Some(from) = from.last_mut() { from.joins = joins; } else { unreachable!() } } let projection = pipeline .pluck(|t| t.into_select()) .into_iter() .exactly_one() .unwrap(); let projection = translate_wildcards(&ctx.anchor, projection); let mut projection = translate_select_items(projection.0, projection.1, ctx)?; let order_by = pipeline.pluck(|t| t.into_sort()); let takes = pipeline.pluck(|t| t.into_take()); let is_distinct = pipeline.iter().any(|t| matches!(t, SqlTransform::Distinct)); let distinct_ons = pipeline.pluck(|t| t.into_distinct_on()); let distinct = if is_distinct { Some(sql_ast::Distinct::Distinct) } else if !distinct_ons.is_empty() { Some(sql_ast::Distinct::On( distinct_ons .into_iter() .exactly_one() .unwrap() .into_iter() .map(|id| translate_cid(id, ctx).map(|x| x.into_ast())) .collect::>>()?, )) } else { None }; // When we have DISTINCT ON, we must have at least a wildcard in the projection // (PostgreSQL requires DISTINCT ON to have a non-empty SELECT list) // Replace NULL placeholder with wildcard if present, or add wildcard if empty if matches!(distinct, Some(sql_ast::Distinct::On(_))) { if projection.len() == 1 { if let SelectItem::UnnamedExpr(sql_ast::Expr::Value(ref v)) = projection[0] { if matches!(v.value, sql_ast::Value::Null) { projection[0] = SelectItem::Wildcard(sql_ast::WildcardAdditionalOptions::default()); } } } else if projection.is_empty() { projection.push(SelectItem::Wildcard( sql_ast::WildcardAdditionalOptions::default(), )); } } // Split the pipeline into before & after the aggregate let (mut before_agg, mut after_agg) = pipeline.break_up(|t| matches!(t, Transform::Aggregate { .. } | Transform::Union { .. })); // WHERE and HAVING let where_ = filter_of_conditions(before_agg.pluck(|t| t.into_filter()), ctx)?; let having = filter_of_conditions(after_agg.pluck(|t| t.into_filter()), ctx)?; // GROUP BY let aggregate = after_agg.pluck(|t| t.into_aggregate()).into_iter().next(); let group_by: Vec = aggregate.map(|(part, _)| part).unwrap_or_default(); ctx.query.allow_stars = ctx.dialect.stars_in_group(); let group_by = sql_ast::GroupByExpr::Expressions(try_into_exprs(group_by, ctx, None)?, vec![]); ctx.query.allow_stars = true; ctx.query.pre_projection = false; let ranges = takes.into_iter().map(|x| x.range).collect(); let take = range_of_ranges(ranges)?; let offset = take.start.map(|s| s - 1).unwrap_or(0); let limit = take.end.map(|e| e - offset); let mut offset = if offset == 0 { None } else { let kind = ExprKind::Literal(Literal::Integer(offset)); let expr = Expr { kind, span: None }; Some(sqlparser::ast::Offset { value: translate_expr(expr, ctx)?.into_ast(), rows: if ctx.dialect.use_fetch() { sqlparser::ast::OffsetRows::Rows } else { sqlparser::ast::OffsetRows::None }, }) }; // Use sorting from the frame let mut order_by: Vec = order_by .last() .map(|sorts| { sorts .iter() .map(|s| translate_column_sort(s, ctx)) .try_collect() }) .transpose()? .unwrap_or_default(); let (fetch, limit) = if ctx.dialect.use_fetch() { (limit.map(|l| fetch_of_i64(l, ctx)), None) } else { (None, limit.map(expr_of_i64)) }; // If we have a FETCH we need to make sure that: // - we have an OFFSET (set to 0) // - we have an ORDER BY (see https://stackoverflow.com/a/44919325) if fetch.is_some() { if offset.is_none() { let kind = ExprKind::Literal(Literal::Integer(0)); let expr = Expr { kind, span: None }; offset = Some(sqlparser::ast::Offset { value: translate_expr(expr, ctx)?.into_ast(), rows: sqlparser::ast::OffsetRows::Rows, }) } if order_by.is_empty() { // When DISTINCT is used, MSSQL requires ORDER BY items to appear // in the SELECT list. Use the first column from the projection // instead of (SELECT NULL). let order_expr = is_distinct .then(|| first_expr_from_projection(&projection)) .flatten() .unwrap_or_else(|| { sql_ast::Expr::Value( sql_ast::Value::Placeholder("(SELECT NULL)".to_string()).into(), ) }); order_by.push(sql_ast::OrderByExpr { expr: order_expr, options: sqlparser::ast::OrderByOptions { asc: None, nulls_first: None, }, with_fill: None, }); } } ctx.pop_query(); Ok(sql_ast::Query { order_by: if order_by.is_empty() { None } else { Some(sql_ast::OrderBy { kind: sqlparser::ast::OrderByKind::Expressions(order_by), interpolate: None, }) }, limit_clause: if limit.is_some() || offset.is_some() { Some(sql_ast::LimitClause::LimitOffset { limit, offset, limit_by: Vec::new(), }) } else { None }, fetch, ..default_query(SetExpr::Select(Box::new(Select { distinct, projection, from, selection: where_, group_by, having, ..default_select() }))) }) } fn translate_set_ops_pipeline( mut top: sql_ast::Query, mut pipeline: Vec, context: &mut Context, ) -> Result { // reverse, so it's easier (and O(1)) to pop pipeline.reverse(); while let Some(transform) = pipeline.pop() { use SqlTransform::*; let op = match &transform { Union { .. } => sql_ast::SetOperator::Union, Except { .. } => sql_ast::SetOperator::Except, Intersect { .. } => sql_ast::SetOperator::Intersect, Sort(_) => continue, _ => unreachable!(), }; let (distinct, bottom) = match transform { Union { distinct, bottom } | Except { distinct, bottom } | Intersect { distinct, bottom } => (distinct, bottom), _ => unreachable!(), }; // Some engines (like SQLite) do not support subqueries between simple parentheses, so we keep // the general `SELECT * FROM (query)` or `SELECT * FROM cte` by default for complex cases. // // Some engines (like Postgres) need the subquery directly to properly match column type on // both sides. For those: // 1. we need the subquery as-is or in parentheses // 2. we need to avoid CTEs // Left hand side (aka top) let left = query_to_set_expr(top, context); // Right hand side (aka bottom) let right_rel = translate_relation_expr(bottom, context)?; let right = if let TableFactor::Derived { subquery, .. } = right_rel { query_to_set_expr(*subquery, context) } else { Box::new(SetExpr::Select(Box::new(sql_ast::Select { projection: vec![SelectItem::Wildcard( sql_ast::WildcardAdditionalOptions::default(), )], from: vec![TableWithJoins { relation: right_rel, joins: vec![], }], ..default_select() }))) }; top = default_query(SetExpr::SetOperation { left, right, set_quantifier: if distinct { if context.dialect.set_ops_distinct() { sql_ast::SetQuantifier::Distinct } else { sql_ast::SetQuantifier::None } } else { sql_ast::SetQuantifier::All }, op, }); } Ok(top) } fn translate_relation_expr(relation_expr: RelationExpr, ctx: &mut Context) -> Result { let alias = Some(&relation_expr.riid) .and_then(|riid| ctx.anchor.relation_instances.get(riid)) .and_then(|ri| ri.table_ref.name.clone()); Ok(match relation_expr.kind { RelationExprKind::Ref(tid) => { let decl = ctx.anchor.lookup_table_decl(&tid).unwrap(); // prepare names let table_name = decl.name.clone().unwrap(); let name = sql_ast::ObjectName( translate_ident(Some(table_name.clone()), None, ctx) .into_iter() .map(sqlparser::ast::ObjectNamePart::Identifier) .collect(), ); TableFactor::Table { name, alias: if Some(table_name.name) == alias { None } else { translate_table_alias(alias, ctx) }, args: None, with_hints: vec![], with_ordinality: false, version: None, partitions: vec![], json_path: None, sample: None, index_hints: vec![], } } RelationExprKind::SubQuery(query) => { let query = translate_relation(query, ctx)?; let alias = translate_table_alias(alias, ctx); TableFactor::Derived { lateral: false, subquery: Box::new(query), alias, } } }) } fn translate_table_alias(alias: Option, ctx: &mut Context) -> Option { alias .map(|ident| translate_ident_part(ident, ctx)) .map(simple_table_alias) } fn translate_join( (side, with, filter): (JoinSide, RelationExpr, Expr), ctx: &mut Context, ) -> Result { let relation = translate_relation_expr(with, ctx)?; let constraint = JoinConstraint::On(translate_expr(filter, ctx)?.into_ast()); Ok(Join { relation, join_operator: match side { JoinSide::Inner => JoinOperator::Inner(constraint), JoinSide::Left => JoinOperator::LeftOuter(constraint), JoinSide::Right => JoinOperator::RightOuter(constraint), JoinSide::Full => JoinOperator::FullOuter(constraint), }, global: false, }) } fn translate_cte(cte: Cte, ctx: &mut Context) -> Result<(sql_ast::Cte, bool)> { let decl = ctx.anchor.lookup_table_decl(&cte.tid).unwrap(); let cte_name = decl.name.clone().unwrap(); let cte_name = translate_ident(Some(cte_name), None, ctx).pop().unwrap(); let (query, recursive) = match cte.kind { // base case CteKind::Normal(rel) => (translate_relation(rel, ctx)?, false), // special: WITH RECURSIVE CteKind::Loop { initial, step } => { // compile initial let initial = query_to_set_expr(translate_relation(initial, ctx)?, ctx); let step = query_to_set_expr(translate_relation(step, ctx)?, ctx); // build CTE and its SELECT let inner_query = default_query(SetExpr::SetOperation { op: sql_ast::SetOperator::Union, set_quantifier: sql_ast::SetQuantifier::All, left: initial, right: step, }); (inner_query, true) // RECURSIVE can only follow WITH directly. // Initial implementation assumed that it applies only to the first CTE. // This meant that it had to wrap any-non-first CTE into a *nested* WITH, so the inner // WITH could be RECURSIVE. // This is implementation of that, in case some dialect requires it. // let inner_cte = sql_ast::Cte { // alias: simple_table_alias(cte_name.clone()), // query: Box::new(inner_query), // from: None, // }; // let outer_query = sql_ast::Query { // with: Some(sql_ast::With { // recursive: true, // cte_tables: vec![inner_cte], // }), // ..default_query(sql_ast::SetExpr::Select(Box::new(sql_ast::Select { // projection: vec![SelectItem::Wildcard( // sql_ast::WildcardAdditionalOptions::default(), // )], // from: vec![TableWithJoins { // relation: TableFactor::Table { // name: sql_ast::ObjectName(vec![cte_name.clone()]), // alias: None, // args: None, // with_hints: Vec::new(), // }, // joins: vec![], // }], // ..default_select() // }))) // }; // (outer_query, false) } }; let cte = sql_ast::Cte { alias: cte_table_alias(cte_name), query: Box::new(query), from: None, materialized: None, closing_paren_token: sqlparser::ast::helpers::attached_token::AttachedToken::empty(), }; Ok((cte, recursive)) } fn translate_relation_literal(data: RelationLiteral, ctx: &Context) -> Result { // TODO: this could be made to use VALUES instead of SELECT UNION ALL SELECT // I'm not sure about compatibility though. // edit: probably not, because VALUES has no way of setting names of the columns // Postgres will just name them column1, column2 as so on. // Which means we can use VALUES, but only if this is not the top-level statement, // where they really matter. if data.rows.is_empty() { let mut nulls: Vec<_> = (data.columns.iter()) .map(|col_name| SelectItem::ExprWithAlias { expr: sql_ast::Expr::Value(sql_ast::Value::Null.into()), alias: translate_ident_part(col_name.clone(), ctx), }) .collect(); // empty projection is a parse error in some dialects, let's inject a NULL if nulls.is_empty() { nulls.push(SelectItem::UnnamedExpr(sql_ast::Expr::Value( sql_ast::Value::Null.into(), ))); } return Ok(default_query(sql_ast::SetExpr::Select(Box::new(Select { projection: nulls, selection: Some(sql_ast::Expr::Value(sql_ast::Value::Boolean(false).into())), ..default_select() })))); } let mut selects = Vec::with_capacity(data.rows.len()); for row in data.rows { let body = sql_ast::SetExpr::Select(Box::new(Select { projection: std::iter::zip(data.columns.clone(), row) .map(|(col, value)| -> Result<_> { Ok(SelectItem::ExprWithAlias { expr: translate_literal(value, ctx)?, alias: translate_ident_part(col, ctx), }) }) .try_collect()?, ..default_select() })); selects.push(body) } let mut body = selects.remove(0); for select in selects { body = SetExpr::SetOperation { op: sql_ast::SetOperator::Union, set_quantifier: sql_ast::SetQuantifier::All, left: Box::new(body), right: Box::new(select), } } Ok(default_query(body)) } pub(super) fn translate_query_sstring( items: Vec>, ctx: &mut Context, ) -> Result { let string = translate_sstring(items, ctx)?; let re = Regex::new(r"(?i)^SELECT\b").unwrap(); let prefix = string.trim().get(0..7).unwrap_or_default(); if re.is_match(prefix) { if let Some(string) = string.trim().strip_prefix(prefix) { return Ok(default_query(sql_ast::SetExpr::Select(Box::new( sql_ast::Select { projection: vec![sql_ast::SelectItem::UnnamedExpr(sql_ast::Expr::Identifier( sql_ast::Ident::new(string), ))], ..default_select() }, )))); } } Err( Error::new_simple("s-strings representing a table must start with `SELECT `".to_string()) .push_hint("this is a limitation by current compiler implementation"), ) } pub(super) fn translate_query_operator( name: String, args: Vec, ctx: &mut Context, ) -> Result { let from_s_string = translate_operator(name, args, ctx)?; let s_string = format!(" * FROM {}", from_s_string.text); Ok(default_query(sql_ast::SetExpr::Select(Box::new( sql_ast::Select { projection: vec![sql_ast::SelectItem::UnnamedExpr(sql_ast::Expr::Identifier( sql_ast::Ident::new(s_string), ))], ..default_select() }, )))) } fn filter_of_conditions(exprs: Vec, context: &mut Context) -> Result> { Ok(if let Some(cond) = all(exprs) { Some(translate_expr(cond, context)?.into_ast()) } else { None }) } fn all(mut exprs: Vec) -> Option { let mut condition = exprs.pop()?; while let Some(expr) = exprs.pop() { condition = Expr { kind: ExprKind::Operator { name: "std.and".to_string(), args: vec![expr, condition], }, span: None, }; } Some(condition) } fn default_query(body: sql_ast::SetExpr) -> sql_ast::Query { sql_ast::Query { with: None, body: Box::new(body), order_by: None, limit_clause: None, fetch: None, locks: Vec::new(), for_clause: None, settings: None, format_clause: None, pipe_operators: Vec::new(), } } fn default_select() -> Select { Select { distinct: None, top: None, top_before_distinct: false, projection: Vec::new(), into: None, from: Vec::new(), lateral_views: Vec::new(), selection: None, group_by: sql_ast::GroupByExpr::Expressions(vec![], vec![]), cluster_by: Vec::new(), distribute_by: Vec::new(), sort_by: Vec::new(), having: None, named_window: vec![], qualify: None, value_table_mode: None, window_before_qualify: false, connect_by: None, prewhere: None, exclude: None, select_token: sqlparser::ast::helpers::attached_token::AttachedToken::empty(), flavor: sqlparser::ast::SelectFlavor::Standard, } } fn simple_table_alias(name: sql_ast::Ident) -> TableAlias { TableAlias { name, columns: Vec::new(), explicit: true, } } fn cte_table_alias(name: sql_ast::Ident) -> TableAlias { TableAlias { name, columns: Vec::new(), explicit: false, } } fn query_to_set_expr(query: sql_ast::Query, context: &mut Context) -> Box { let is_simple = query.with.is_none() && query.order_by.is_none() && query.limit_clause.is_none() && query.fetch.is_none() && query.locks.is_empty(); if is_simple { return query.body; } // Query is not simple, so we need to wrap it. // // Some engines (like SQLite) do not support subqueries between simple parentheses, so we keep // the general `SELECT * FROM (query)` by default. // // Some engines (like Postgres) may need the subquery directly as-is or between parentheses // to properly match columns for syntaxes like `UNION`, `EXCEPT`, and `INTERSECT`. // Incidentally, the parenthesis syntax `(query)` allows for complex queries. let set_expr = if context.dialect.prefers_subquery_parentheses_shorthand() { SetExpr::Query(query.into()) } else { SetExpr::Select(Box::new(Select { projection: vec![SelectItem::Wildcard( sql_ast::WildcardAdditionalOptions::default(), )], from: vec![TableWithJoins { relation: TableFactor::Derived { lateral: false, subquery: Box::new(query), alias: Some(simple_table_alias(sql_ast::Ident::new( context.anchor.table_name.gen(), ))), }, joins: vec![], }], ..default_select() })) }; Box::new(set_expr) } fn count_tables(transforms: &[Transform]) -> usize { let mut count = 0; for transform in transforms { if let Transform::Join { .. } | Transform::From(_) = transform { count += 1; } } count } /// Extract the first expression from a projection for use in ORDER BY. /// Returns None if the projection is empty or only contains wildcards. fn first_expr_from_projection(projection: &[SelectItem]) -> Option { for item in projection { match item { SelectItem::UnnamedExpr(expr) => return Some(expr.clone()), SelectItem::ExprWithAlias { alias, .. } => { return Some(sql_ast::Expr::Identifier(alias.clone())); } SelectItem::Wildcard(_) | SelectItem::QualifiedWildcard(_, _) => continue, } } None } #[cfg(test)] mod test { use insta::assert_snapshot; #[test] fn test_variable_after_aggregate() { let query = &r#" from employees group {title, emp_no} ( aggregate {emp_salary = average salary} ) group {title} ( aggregate {avg_salary = average emp_salary} ) "#; let sql_ast = crate::tests::compile(query).unwrap(); assert_snapshot!(sql_ast, @r" WITH table_0 AS ( SELECT title, AVG(salary) AS _expr_0 FROM employees GROUP BY title, emp_no ) SELECT title, AVG(_expr_0) AS avg_salary FROM table_0 GROUP BY title "); } #[test] fn test_derive_filter() { // I suspect that the anchoring algorithm has a architectural flaw: // it assumes that it can materialize all columns, even if their // Compute is in a prior CTE. The problem is that because anchoring is // computed back-to-front, we don't know where Compute will end up when // materializing following transforms. // // If algorithm is changed to be front-to-back, preprocess_reorder can // be (must be) removed. let query = &r#" from employees derive {global_rank = rank country} filter country == "USA" derive {rank = rank country} "#; let sql_ast = crate::tests::compile(query).unwrap(); assert_snapshot!(sql_ast, @r" WITH table_0 AS ( SELECT *, RANK() OVER () AS global_rank FROM employees ) SELECT *, RANK() OVER () AS rank FROM table_0 WHERE country = 'USA' "); } #[test] fn test_filter_windowed() { // #806 let query = &r#" from tbl1 filter (average bar) > 3 "#; assert_snapshot!(crate::tests::compile(query).unwrap(), @r" WITH table_0 AS ( SELECT *, AVG(bar) OVER () AS _expr_0 FROM tbl1 ) SELECT * FROM table_0 WHERE _expr_0 > 3 "); } #[test] fn test_distinct_on_with_aggregate() { // #5556: DISTINCT ON with aggregate should include wildcard let query = &r#" prql target:sql.postgres from t1 group {id, name} (take 1) aggregate {c=count this} "#; assert_snapshot!(crate::tests::compile(query).unwrap(), @r" WITH table_0 AS ( SELECT DISTINCT ON (id, name) * FROM t1 ) SELECT COUNT(*) AS c FROM table_0 "); } #[test] fn test_join_with_inaccessible_table() { // issue #5280: join referencing table not accessible in current scope let query = r#" from c = companies join ca = companies_addresses (c.tax_code == ca.company) group c.tax_code ( join a = addresses (a.id == ca.address) sort {-ca.created_at} take 2.. ) sort tax_code "#; let err = crate::tests::compile(query).unwrap_err(); assert!(err.to_string().contains("not accessible in this context")); } } ================================================ FILE: prqlc/prqlc/src/sql/keywords.rs ================================================ use std::collections::{HashMap, HashSet}; use std::sync::OnceLock; use sqlparser::keywords::{ Keyword, ALL_KEYWORDS, ALL_KEYWORDS_INDEX, RESERVED_FOR_COLUMN_ALIAS, RESERVED_FOR_TABLE_ALIAS, }; use crate::sql::dialect::Dialect; /// True for keywords which we want to quote when translating to SQL. /// /// Currently we're being fairly permissive (over-quoting is not a big concern). // We're not including the full list from `SQL_KEYWORDS`, as that has terms such // as `ID`, instead we bring a few dialects' keywords in. pub(super) fn is_keyword(ident: &str, dialect: &Dialect) -> bool { let ident = ident.to_ascii_uppercase(); sql_keywords().contains(ident.as_str()) || dialect_keywords(dialect).contains(ident.as_str()) } fn dialect_keywords(dialect: &Dialect) -> &'static HashSet<&'static str> { match dialect { Dialect::Redshift => redshift_keywords(), _ => empty_keywords(), } } fn empty_keywords() -> &'static HashSet<&'static str> { static EMPTY: OnceLock> = OnceLock::new(); EMPTY.get_or_init(HashSet::new) } fn redshift_keywords() -> &'static HashSet<&'static str> { static REDSHIFT: OnceLock> = OnceLock::new(); REDSHIFT.get_or_init(|| { let mut m = HashSet::new(); m.extend(REDSHIFT_KEYWORDS); m }) } fn sql_keywords() -> &'static HashSet<&'static str> { static SQL_KEYWORDS: OnceLock> = OnceLock::new(); SQL_KEYWORDS.get_or_init(|| { let mut m = HashSet::new(); m.extend(SQLITE_KEYWORDS); m.extend(POSTGRES_KEYWORDS); m.extend(DUCKDB_KEYWORDS); m.extend(BIGQUERY_KEYWORDS); let reverse_index: HashMap<&Keyword, usize> = ALL_KEYWORDS_INDEX .iter() .enumerate() .map(|(idx, kw)| (kw, idx)) .collect(); m.extend( RESERVED_FOR_COLUMN_ALIAS .iter() .map(|x| ALL_KEYWORDS[reverse_index[x]]), ); m.extend( RESERVED_FOR_TABLE_ALIAS .iter() .map(|x| ALL_KEYWORDS[reverse_index[x]]), ); m }) } const SQLITE_KEYWORDS: &[&str] = &[ "ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", "AND", "AS", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", "BETWEEN", "BY", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE", "CROSS", "CURRENT", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT", "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", "DISTINCT", "DO", "DROP", "EACH", "ELSE", "END", "ESCAPE", "EXCEPT", "EXCLUDE", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", "FILTER", "FIRST", "FOLLOWING", "FOR", "FOREIGN", "FROM", "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", "HAVING", "IF", "IGNORE", "IMMEDIATE", "IN", "INDEX", "INDEXED", "INITIALLY", "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO", "IS", "ISNULL", "JOIN", "KEY", "LAST", "LEFT", "LIKE", "LIMIT", "MATCH", "MATERIALIZED", "NATURAL", "NO", "NOT", "NOTHING", "NOTNULL", "NULL", "NULLS", "OF", "OFFSET", "ON", "OR", "ORDER", "OTHERS", "OUTER", "OVER", "PARTITION", "PLAN", "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", "RANGE", "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", "RENAME", "REPLACE", "RESTRICT", "RETURNING", "RIGHT", "ROLLBACK", "ROW", "ROWS", "SAVEPOINT", "SELECT", "SET", "TABLE", "TEMP", "TEMPORARY", "THEN", "TIES", "TO", "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN", "WHERE", "WINDOW", "WITH", "WITHOUT", ]; // Copy table from // , then run: // // pbpaste | rg '^\w+ *\treserved' | choose 0 | rg '(.*)' -r '"$1",' | pbcopy const POSTGRES_KEYWORDS: &[&str] = &[ "ALL", "ANALYSE", "ANALYZE", "AND", "ANY", "ARRAY", "AS", "ASC", "ASYMMETRIC", "AUTHORIZATION", "BINARY", "BOTH", "CASE", "CAST", "CHECK", "COLLATE", "COLLATION", "COLUMN", "CONCURRENTLY", "CONSTRAINT", "CREATE", "CROSS", "CURRENT_CATALOG", "CURRENT_DATE", "CURRENT_ROLE", "CURRENT_SCHEMA", "CURRENT_TIME", "CURRENT_TIMESTAMP", "CURRENT_USER", "DEFAULT", "DEFERRABLE", "DESC", "DISTINCT", "DO", "ELSE", "END", "EXCEPT", "FALSE", "FETCH", "FOR", "FOREIGN", "FREEZE", "FROM", "FULL", "GRANT", "GROUP", "HAVING", "ILIKE", "IN", "INITIALLY", "INNER", "INTERSECT", "INTO", "IS", "ISNULL", "JOIN", "LATERAL", "LEADING", "LEFT", "LIKE", "LIMIT", "LOCALTIME", "LOCALTIMESTAMP", "NATURAL", "NOT", "NOTNULL", "NULL", "OFFSET", "ON", "ONLY", "OR", "ORDER", "OUTER", "OVERLAPS", "PLACING", "PRIMARY", "REFERENCES", "RETURNING", "RIGHT", "SELECT", "SESSION_USER", "SIMILAR", "SOME", "SYMMETRIC", "SYSTEM_USER", "TABLE", "TABLESAMPLE", "THEN", "TO", "TRAILING", "TRUE", "UNION", "UNIQUE", "USER", "USING", "VARIADIC", "VERBOSE", "WHEN", "WHERE", "WINDOW", "WITH", ]; // In duckdb: // // .output // .mode list // .headers off // .output keywords.txt // SELECT '"'||UPPER(keyword_name)||'",' FROM duckdb_keywords() WHERE keyword_category='reserved'; // .output // const DUCKDB_KEYWORDS: &[&str] = &[ "ALL", "ANALYSE", "ANALYZE", "AND", "ANY", "ARRAY", "AS", "ASC", "ASYMMETRIC", "BOTH", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", "CONSTRAINT", "CREATE", "DEFAULT", "DEFERRABLE", "DESC", "DESCRIBE", "DISTINCT", "DO", "ELSE", "END", "EXCEPT", "FALSE", "FETCH", "FOR", "FOREIGN", "FROM", "GRANT", "GROUP", "HAVING", "IN", "INITIALLY", "INTERSECT", "INTO", "LATERAL", "LEADING", "LIMIT", "NOT", "NULL", "OFFSET", "ON", "ONLY", "OR", "ORDER", "PIVOT", "PIVOT_LONGER", "PIVOT_WIDER", "PLACING", "PRIMARY", "QUALIFY", "REFERENCES", "RETURNING", "SELECT", "SHOW", "SOME", "SUMMARIZE", "SYMMETRIC", "TABLE", "THEN", "TO", "TRAILING", "TRUE", "UNION", "UNIQUE", "UNPIVOT", "USING", "VARIADIC", "WHEN", "WHERE", "WINDOW", "WITH", ]; // source reserved keywords GoogleSQL // const BIGQUERY_KEYWORDS: &[&str] = &[ "ALL", "AND", "ANY", "ARRAY", "AS", "ASC", "ASSERT_ROWS_MODIFIED", "AT", "BETWEEN", "BY", "CASE", "CAST", "COLLATE", "CONTAINS", "CREATE", "CROSS", "CUBE", "CURRENT", "DEFAULT", "DEFINE", "DESC", "DISTINCT", "ELSE", "END", "ENUM", "ESCAPE", "EXCEPT", "EXCLUDE", "EXISTS", "EXTRACT", "FALSE", "FETCH", "FOLLOWING", "FOR", "FROM", "FULL", "GROUP", "GROUPING", "GROUPS", "HASH", "HAVING", "IF", "IGNORE", "IN", "INNER", "INTERSECT", "INTERVAL", "INTO", "IS", "JOIN", "LATERAL", "LEFT", "LIKE", "LIMIT", "LOOKUP", "MERGE", "NATURAL", "NEW", "NO", "NOT", "NULL", "NULLS", "OF", "ON", "OR", "ORDER", "OUTER", "OVER", "PARTITION", "PRECEDING", "PROTO", "QUALIFY", "RANGE", "RECURSIVE", "RESPECT", "RIGHT", "ROLLUP", "ROWS", "SELECT", "SET", "SOME", "STRUCT", "TABLESAMPLE", "THEN", "TO", "TREAT", "TRUE", "UNBOUNDED", "UNION", "UNNEST", "USING", "WHEN", "WHERE", "WINDOW", "WITH", "WITHIN", ]; // Source reserved keywords from Amazon Redshift // https://docs.aws.amazon.com/redshift/latest/dg/r_pg_keywords.html // Only including keywords that are NOT in the common SQL keywords above const REDSHIFT_KEYWORDS: &[&str] = &[ "AES128", "AES256", "ALLOWOVERWRITE", "BACKUP", "BLANKSASNULL", "BYTEDICT", "BZIP2", "CREDENTIALS", "DEFRAG", "DEFLATE", "DELTA", "DELTA32K", "EMPTYASNULL", "ENCODE", "ENCRYPT", "ENCRYPTION", "EXPLICIT", "GLOBALDICT256", "GLOBALDICT64K", "GZIP", "IDENTITY", "LUN", "LUNS", "LZO", "LZOP", "MINUS", "MOSTLY13", "MOSTLY32", "MOSTLY8", "OFFLINE", "OID", "PARALLEL", "PERCENT", "PERMISSIONS", "RAW", "READRATIO", "RECOVER", "REJECTLOG", "RESORT", "RESTORE", "SNAPSHOT", "SYSDATE", "SYSTEM", "TAG", "TDES", "TEXT255", "TEXT32K", "TIME", "TIMESTAMP", "TOP", "TRUNCATECOLUMNS", "WALLET", ]; #[test] fn test_sql_keywords() { assert!(is_keyword("from", &Dialect::Generic)); assert!(is_keyword("user", &Dialect::Generic)); // Redshift-specific keywords should only be keywords for Redshift assert!(is_keyword("time", &Dialect::Redshift)); assert!(!is_keyword("time", &Dialect::Postgres)); assert!(!is_keyword("time", &Dialect::Generic)); } ================================================ FILE: prqlc/prqlc/src/sql/mod.rs ================================================ //! Backend for translating RQ into SQL mod dialect; mod gen_expr; mod gen_projection; mod gen_query; mod keywords; mod operators; mod pq; pub use dialect::{Dialect, SupportLevel}; pub use pq::ast as pq_ast; use self::dialect::DialectHandler; use self::pq::ast::Cte; use self::pq::context::AnchorContext; use crate::debug; use crate::ir::rq; use crate::Result; use crate::{compiler_version, Options}; /// Translate a PRQL AST into a SQL string. pub fn compile(query: rq::RelationalQuery, options: &Options) -> Result { let crate::Target::Sql(dialect) = options.target; let sql_ast = gen_query::translate_query(query, dialect)?; let sql = sql_ast.to_string(); // formatting let sql = if options.format { let formatted = sqlformat::format( &sql, &sqlformat::QueryParams::default(), &sqlformat::FormatOptions::default(), ); formatted + "\n" } else { sql }; debug::log_entry(|| debug::DebugEntryKind::ReprSql(sql.clone())); // signature let sql = if options.signature_comment { let pre = if options.format { "\n" } else { " " }; let post = if options.format { "\n" } else { "" }; let target = dialect .map(|d| format!("target:sql.{d} ")) .unwrap_or_default(); let signature = format!( "{pre}-- Generated by PRQL compiler version:{} {}(https://prql-lang.org){post}", compiler_version(), target, ); sql + &signature } else { sql }; Ok(sql) } #[derive(Debug)] struct Context { pub dialect: Box, pub dialect_enum: Dialect, pub anchor: AnchorContext, // stuff regarding current query query: QueryOpts, // stuff regarding parent queries query_stack: Vec, pub ctes: Vec, } #[derive(Clone, Debug)] struct QueryOpts { /// When true, column references will not include table names prefixes. pub omit_ident_prefix: bool, /// True iff codegen should generate expressions before SELECT's projection is applied. /// For example: /// - WHERE needs `pre_projection=true`, but /// - ORDER BY needs `pre_projection=false`. pub pre_projection: bool, /// When false, queries will contain nested sub-queries instead of WITH CTEs. pub allow_ctes: bool, /// When false, * are not allowed. pub allow_stars: bool, /// True when translating function that will have an OVER clause. pub window_function: bool, } impl Default for QueryOpts { fn default() -> Self { QueryOpts { omit_ident_prefix: false, pre_projection: false, allow_ctes: true, allow_stars: true, window_function: false, } } } impl Context { fn new(dialect: Dialect, anchor: AnchorContext) -> Self { Context { dialect: dialect.handler(), dialect_enum: dialect, anchor, query: QueryOpts::default(), query_stack: Vec::new(), ctes: Vec::new(), } } fn push_query(&mut self) { self.query_stack.push(self.query.clone()); } fn pop_query(&mut self) { self.query = self.query_stack.pop().unwrap(); } } #[cfg(test)] mod test { use crate::compile; use crate::Options; #[test] fn test_end_with_new_line() { let sql = compile("from a", &Options::default().no_signature()).unwrap(); assert_eq!(sql, "SELECT\n *\nFROM\n a\n") } } ================================================ FILE: prqlc/prqlc/src/sql/operators.rs ================================================ use std::collections::HashMap; use std::iter::zip; use std::path::PathBuf; use std::sync::OnceLock; use itertools::Itertools; use super::gen_expr::{translate_operand, ExprOrSource, SourceExpr}; use super::{Context, Dialect}; use crate::ir::{decl, pl, rq}; use crate::utils::Pluck; use crate::Result; use crate::{debug, semantic}; use crate::{Error, WithErrorInfo}; fn std() -> &'static decl::Module { static STD: OnceLock = OnceLock::new(); STD.get_or_init(|| { let _suppressed = debug::log_suppress(); let std_lib = crate::SourceTree::new( [( PathBuf::from("std.prql"), include_str!("./std.sql.prql").to_string(), )], None, ); let ast = crate::parser::parse(&std_lib).unwrap(); let context = semantic::resolve(ast).unwrap(); context.module }) } pub(super) fn translate_operator_expr(expr: rq::Expr, ctx: &mut Context) -> Result { let (name, args) = expr.kind.into_operator().unwrap(); let source = translate_operator(name, args, ctx).with_span(expr.span)?; Ok(ExprOrSource::Source(source)) } pub(super) fn translate_operator( name: String, args: Vec, ctx: &mut Context, ) -> Result { let (func_def, binding_strength, window_frame, coalesce) = find_operator_impl(&name, ctx.dialect_enum).unwrap(); let parent_binding_strength = binding_strength.unwrap_or(100); let params = func_def .named_params .iter() .chain(func_def.params.iter()) .map(|x| x.name.split('.').next_back().unwrap_or(x.name.as_str())); let args: HashMap<&str, _> = zip(params, args).collect(); // body can only be an s-string let body = match &func_def.body.kind { pl::ExprKind::Literal(pl::Literal::Null) => { return Err(Error::new_simple(format!( "operator {} is not supported for dialect {}", name, ctx.dialect_enum ))) } pl::ExprKind::SString(items) => items, _ => panic!("Bad RQ operator implementation. Expected s-string or null"), }; let mut text = String::new(); for item in body { match item { pl::InterpolateItem::Expr { expr, format } => { // s-string exprs can only contain idents let ident = expr.kind.as_ident(); let ident = ident.as_ref().unwrap(); // lookup args let arg = args.get(ident.name.as_str()).unwrap().clone(); // binding strength let required_strength = format .as_ref() .and_then(|f| f.parse::().ok()) .unwrap_or(parent_binding_strength); // translate args let arg = translate_operand( arg, false, required_strength, super::gen_expr::Associativity::Both, ctx, )?; text += &arg.into_source(); } pl::InterpolateItem::String(s) => { text += s; } } } let mut binding_strength = parent_binding_strength; if !ctx.query.window_function { if let Some(default) = coalesce { text = format!("COALESCE({text}, {default})"); binding_strength = 100; } } Ok(SourceExpr { text, binding_strength, window_frame, }) } fn find_operator_impl( operator_name: &str, dialect: Dialect, ) -> Option<(&pl::Func, Option, bool, Option)> { let operator_name = operator_name.strip_prefix("std.").unwrap(); let operator_ident = pl::Ident::from_path( operator_name .split('.') .map(String::from) .collect::>(), ); let dialect_module = std().get(&pl::Ident::from_name(dialect.to_string())); let mut func_def = None; if let Some(dialect_module) = dialect_module { let module = dialect_module.kind.as_module().unwrap(); func_def = module.get(&operator_ident); } if func_def.is_none() { func_def = std().get(&operator_ident); } let decl = func_def?; let func_def = decl.kind.as_expr().unwrap(); let func_def = func_def.kind.as_func().unwrap(); let annotation = decl.annotations.iter().exactly_one().ok(); let mut annotation = annotation .and_then(|x| into_tuple_items(*x.expr.clone()).ok()) .unwrap_or_default(); let binding_strength = pluck_annotation(&mut annotation, "binding_strength") .and_then(|literal| literal.into_integer().ok()) .map(|int| int as i32); let window_frame = pluck_annotation(&mut annotation, "window_frame") .and_then(|literal| literal.into_boolean().ok()) .unwrap_or_default(); let coalesce = pluck_annotation(&mut annotation, "coalesce").and_then(|val| val.into_string().ok()); Some((func_def.as_ref(), binding_strength, window_frame, coalesce)) } fn pluck_annotation( annotation: &mut Vec<(String, pl::ExprKind)>, name: &str, ) -> Option { annotation .pluck(|(n, val)| if n == name { Ok(val) } else { Err((n, val)) }) .into_iter() .next() .and_then(|val| val.into_literal().ok()) } /// Find the items in a `@{a=b}`. We're only using annotations with tuples; /// we can consider formalizing this constraint. fn into_tuple_items(expr: pl::Expr) -> Result, pl::Expr> { match expr.kind { pl::ExprKind::Tuple(items) => items .into_iter() .map(|item| Ok((item.alias.clone().unwrap(), item.kind))) .collect(), _ => Err(expr), } } ================================================ FILE: prqlc/prqlc/src/sql/pq/anchor.rs ================================================ use std::collections::{HashMap, HashSet}; use std::fmt; use std::ops::Deref; use itertools::Itertools; use super::ast::{PqMapper, SqlTransform}; use super::context::{AnchorContext, ColumnDecl, RIId, RelationStatus, SqlTableDecl}; use crate::ir::generic::ColumnSort; use crate::ir::rq::{ self, fold_column_sorts, fold_transform, CId, Compute, Expr, RelationColumn, RqFold, TableRef, Transform, }; use crate::sql::pq::context::RelationAdapter; use crate::sql::pq::positional_mapping::compute_positional_mappings; use crate::Result; /// Extract last part of pipeline that is able to "fit" into a single SELECT statement. /// Remaining proceeding pipeline is declared as a table and stored in AnchorContext. pub(super) fn extract_atomic( pipeline: Vec, ctx: &mut AnchorContext, ) -> Vec { let output = ctx.determine_select_columns(&pipeline); let output = ctx.positional_mapping.apply_active_mapping(output); let (preceding, atomic) = split_off_back(pipeline, output.clone(), ctx); let atomic = if let Some(preceding) = preceding { log::debug!( "pipeline split after {}", preceding.last().unwrap().as_str() ); anchor_split(ctx, preceding, atomic) } else { atomic }; // sometimes, additional columns will be added into select, because they are needed for // other clauses. To filter them out, we use an additional limiting SELECT. let output: Vec<_> = CidRedirector::redirect_cids(output, &atomic, ctx); let select_cols = atomic .iter() .find_map(|x| x.as_super().and_then(|y| y.as_select())) .unwrap(); if select_cols.iter().any(|c| !output.contains(c)) { log::debug!( "appending a projection SELECT, because previous one contained un-selected columns" ); // duplicate Select for purposes of anchor_split let duplicated_select = SqlTransform::Super(Transform::Select(select_cols.clone())); let mut atomic = atomic; atomic.push(duplicated_select); // construct the new SELECT let limited_view = vec![SqlTransform::Super(Transform::Select(output))]; return anchor_split(ctx, atomic, limited_view); } atomic } /// Splits pipeline into two parts, such that the second part contains /// maximum number of transforms while "fitting" into a SELECT query. /// /// Returns optional remaining preceding pipeline and the atomic pipeline. pub(super) fn split_off_back( mut pipeline: Vec, output: Vec, ctx: &mut AnchorContext, ) -> (Option>, Vec) { if pipeline.is_empty() { return (None, Vec::new()); } let mapping_before = compute_positional_mappings(&pipeline, None); log::debug!("traversing pipeline to obtain columns: {output:?}"); let mut following_transforms: HashSet = HashSet::new(); let mut inputs_required = Requirements::from_cids(output.iter()) .allow_up_to(Complexity::highest()) .should_select(true); let mut inputs_avail = HashSet::new(); // iterate backwards let mut curr_pipeline_rev = Vec::new(); 'pipeline: while let Some(transform) = pipeline.pop() { // stop if split is needed let split = is_split_required(&transform, &mut following_transforms); if split { log::debug!("split required after {}", transform.as_str()); log::debug!(".. following={following_transforms:?}"); pipeline.push(transform); break; } // anchor and record all requirements let required = get_requirements(&transform, &following_transforms, &inputs_required); log::debug!(".. transform {} requires {required:?}", transform.as_str(),); inputs_required = inputs_required.append(required.clone()); match &transform { SqlTransform::Super(Transform::Compute(compute)) => { let (can_mat, max_complexity) = can_materialize(compute, &inputs_required); if can_mat { log::debug!("materializing {:?}", compute.id); inputs_avail.insert(compute.id); // add transitive dependencies inputs_required = inputs_required .append(required.allow_up_to(max_complexity).should_select(false)); } else { pipeline.push(transform); break; } } SqlTransform::Super(Transform::Aggregate { compute, .. }) => { for cid in compute { let decl = &ctx.column_decls[cid]; if let ColumnDecl::Compute(compute) = decl { if !can_materialize(compute, &inputs_required).0 { pipeline.push(transform); break 'pipeline; } } } } SqlTransform::From(with) | SqlTransform::Join { with, .. } => { let relation = ctx.relation_instances.get_mut(with).unwrap(); for (_, cid) in &relation.table_ref.columns { inputs_avail.insert(*cid); } } _ => (), } // push into current pipeline if !matches!(transform, SqlTransform::Super(Transform::Select(_))) { curr_pipeline_rev.push(transform); } } let selected = inputs_required .iter() .filter(|r| r.selected) .map(|r| r.col) .collect_vec(); log::debug!("finished table:"); log::debug!(".. avail={inputs_avail:?}"); let required = inputs_required.iter().map(|r| r.col).unique().collect_vec(); log::debug!(".. required={required:?}"); let missing = required .into_iter() .filter(|i| !inputs_avail.contains(i)) .collect_vec(); log::debug!(".. missing={missing:?}"); // figure out SELECT columns { // output cols must preserve duplicates, but selected inputs has to be deduplicated let mut output = output; for c in selected { if !output.contains(&c) { output.push(c); } } curr_pipeline_rev.push(SqlTransform::Super(Transform::Select(output))); } let remaining_pipeline = if pipeline.is_empty() { None } else { // drop inputs that were satisfied in current pipeline pipeline.push(SqlTransform::Super(Transform::Select(missing))); Some(pipeline) }; curr_pipeline_rev.reverse(); // This will compare columns for order sensitive transform and correct it in subsequent relation. let mapping_after = compute_positional_mappings(&curr_pipeline_rev, Some(&inputs_required)); for (riid, after) in mapping_after { if let Some((_, before)) = mapping_before.iter().find(|(r, _)| &riid == r) { ctx.positional_mapping .compute_and_store_mapping(before, &after, &riid); } } (remaining_pipeline, curr_pipeline_rev) } fn can_materialize(compute: &Compute, inputs_required: &[Requirement]) -> (bool, Complexity) { let complexity = infer_complexity(compute); let required = inputs_required .iter() .filter(|r| r.col == compute.id) .fold(Complexity::highest(), |c, r| { Complexity::min(c, r.max_complexity) }); let can_materialize = complexity <= required; if !can_materialize { // cannot materialize here, complexity is greater than what's required here log::debug!( "{:?} has complexity {complexity:?}, but is required to have at most {required:?}", compute.id ); } (can_materialize, required) } /// Applies adjustments to second part of a pipeline when it's split: /// - append Select to proceeding pipeline /// - prepend From to atomic pipeline /// - redefine columns materialized in atomic pipeline /// - redirect all references to original columns to the new ones pub(super) fn anchor_split( ctx: &mut AnchorContext, preceding: Vec, atomic: Vec, ) -> Vec { let new_tid = ctx.tid.gen(); let preceding_select = &preceding.last().unwrap().as_super().unwrap(); let cols_at_split = preceding_select.as_select().unwrap(); log::debug!("split pipeline, first pipeline output: {cols_at_split:?}"); // redefine columns of the atomic pipeline let mut cid_redirects = HashMap::::new(); let mut new_columns = Vec::new(); let mut used_new_names = HashSet::new(); for old_cid in cols_at_split { let new_cid = ctx.cid.gen(); let old_name = ctx.ensure_column_name(*old_cid).cloned(); let mut new_name = old_name; if let Some(new) = &mut new_name { if used_new_names.contains(new) { *new = ctx.col_name.gen(); ctx.column_names.insert(*old_cid, new.clone()); } used_new_names.insert(new.clone()); ctx.column_names.insert(new_cid, new.clone()); } let old_def = ctx.column_decls.get(old_cid).unwrap(); let col = match old_def { ColumnDecl::RelationColumn(_, _, RelationColumn::Wildcard) => RelationColumn::Wildcard, _ => RelationColumn::Single(new_name), }; new_columns.push((col, new_cid)); cid_redirects.insert(*old_cid, new_cid); } // define a new table let columns = cols_at_split .iter() .map(|_| RelationColumn::Single(None)) .collect_vec(); ctx.table_decls.insert( new_tid, SqlTableDecl { id: new_tid, name: None, relation: RelationStatus::NotYetDefined(RelationAdapter::Preprocessed( preceding, columns, )), redirect_to: None, }, ); // define instance of that table let riid = ctx.create_relation_instance( TableRef { source: new_tid, name: None, columns: new_columns, prefer_cte: true, }, cid_redirects, ); // adjust second part: prepend from and rewrite expressions to use new columns let mut second = atomic; second.insert(0, SqlTransform::From(riid)); CidRedirector::redirect_pipeline(second, ctx) } /// Determines whether a pipeline must be split at a transform to /// fit into one SELECT statement. /// /// `following` contain names of following transforms in the pipeline. fn is_split_required(transform: &SqlTransform, following: &mut HashSet) -> bool { // Pipeline must be split when there is a transform that is out of order: // - from (max 1x), // - join (no limit), // - filters (for WHERE) // - aggregate (max 1x) // - filters (for HAVING) // - compute (no limit) // - sort (no limit) // - take (no limit) // - distinct // - append/except/intersect (no limit) // - loop (max 1x) // // Select is not affected by the order. use SqlTransform::Super; use Transform::*; // Compute for aggregation does not count as a real compute, // because it's done within the aggregation if let Super(Compute(decl)) = transform { if decl.is_aggregation { return false; } } fn contains_any(set: &HashSet, elements: [&'static str; C]) -> bool { for t in elements { if set.contains(t) { return true; } } false } let split = match transform { SqlTransform::From(_) => contains_any(following, ["From"]), SqlTransform::Join { .. } => contains_any(following, ["From"]), Super(Aggregate { .. }) => { contains_any(following, ["From", "Join", "Aggregate", "Compute"]) } Super(Filter(_)) => contains_any(following, ["From", "Join"]), Super(Compute(_)) => { // Don't split between Compute and Filter when there's an Aggregate in following. // Filter after Aggregate becomes HAVING in the same SELECT, so all preceding // Computes (including those with aggregation functions like SUM) must stay // with the Aggregate to get proper GROUP BY. if following.contains("Aggregate") { contains_any(following, ["From", "Join"]) } else { contains_any(following, ["From", "Join", /* "Aggregate" */ "Filter"]) } } // Sort will be pushed down the CTEs, so there is no point in splitting for it. // Super(Sort(_)) => contains_any(following, ["From", "Join", "Compute", "Aggregate"]), Super(Take(_)) => contains_any( following, ["From", "Join", "Compute", "Filter", "Aggregate", "Sort"], ), SqlTransform::DistinctOn(_) => contains_any( following, [ "From", "Join", "Compute", "Filter", "Aggregate", "Sort", "Take", "DistinctOn", ], ), SqlTransform::Distinct => contains_any( following, [ "From", "Join", "Compute", "Filter", "Aggregate", "Sort", "Take", ], ), SqlTransform::Union { .. } | SqlTransform::Except { .. } | SqlTransform::Intersect { .. } => contains_any( following, [ "From", "Join", "Compute", "Filter", "Aggregate", "Sort", "Take", "Distinct", ], ), Super(Loop(_)) => !following.is_empty(), _ => false, }; if !split { following.insert(transform.as_str().to_string()); } split } /// An input requirement of a transform. #[derive(Clone)] pub struct Requirement { pub col: CId, /// Maximum complexity with which this column can be expressed in this transform pub max_complexity: Complexity, /// True iff this column needs to be SELECTTed so it can be referenced in this transform pub selected: bool, } #[derive(Clone, Default)] pub struct Requirements(Vec); /// To iter on `Requirements` as if it's a simple `Vec`. impl Deref for Requirements { type Target = [Requirement]; fn deref(&self) -> &[Requirement] { self.0.as_slice() } } /// To debug `Requirements` as if it's a simple `Vec`. impl fmt::Debug for Requirements { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:?}", self.0) } } impl Requirements { /// Turns a list of `CId` into requirements with the least allowed complexity /// and unselected by default. pub fn from_cids<'a, I>(cids: I) -> Requirements where I: Iterator, { Requirements( cids.cloned() .map(|col| Requirement { col, max_complexity: Complexity::lowest(), selected: false, }) .collect(), ) } /// Collect columns from the given `Expr` into requirements with /// the least allowed complexity and unselected by default. pub fn from_expr(expr: &Expr) -> Requirements { let cids = CidCollector::collect(expr.clone()); Requirements::from_cids(cids.iter()) } /// Moves all the elements of `other` into `self`, leaving `other` empty, /// then return `self` for chainability. pub fn append(mut self, mut other: Requirements) -> Requirements { self.0.append(&mut other.0); self } /// Set a maximum complexity to all stored requirements. pub fn allow_up_to(mut self, max_complexity: Complexity) -> Self { for r in &mut self.0 { r.max_complexity = max_complexity; } self } /// Set a SELECT status to all stored requirements. pub fn should_select(mut self, selected: bool) -> Self { for r in &mut self.0 { r.selected = selected; } self } pub fn is_selected(&self, id: &CId) -> bool { self.0.iter().any(|r| r.selected && &r.col == id) } pub fn is_required(&self, id: &CId) -> bool { self.0.iter().any(|r| &r.col == id) } } impl std::fmt::Debug for Requirement { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Debug::fmt(&self.col, f)?; f.write_str("-as-")?; std::fmt::Debug::fmt(&self.max_complexity, f) } } pub(super) fn get_requirements( transform: &SqlTransform, following: &HashSet, previous_requirements: &Requirements, ) -> Requirements { use SqlTransform::Super; match transform { Super(Transform::Aggregate { partition, .. }) => Requirements::from_cids(partition.iter()), Super(Transform::Compute(compute)) if previous_requirements.is_required(&compute.id) => { let requirements = Requirements::from_expr(&compute.expr).allow_up_to( match infer_complexity(compute) { // plain expressions can be included in anything less complex than Aggregation Complexity::Plain => Complexity::Aggregation, // anything more complex can only use included in other plain expressions. // in other words: complex expressions (aggregation, window functions) cannot // be defined within other expressions. _ => Complexity::Plain, }, ); if let Some(window) = &compute.window { // TODO: what kind of exprs can be in window frame? // window.frame let window_cids = window .partition .iter() .chain(window.sort.iter().map(|s| &s.column)); requirements.append(Requirements::from_cids(window_cids)) } else { requirements } } Super(Transform::Filter(expr)) => { Requirements::from_expr(expr).allow_up_to(if !following.contains("Aggregate") { Complexity::Aggregation } else { Complexity::Plain }) } // Aggregations require that all selected columns be wrapped in aggregate functions (e.g., SUM, COUNT). Super(Transform::Sort(sorts)) if !following.contains("Aggregate") => { Requirements::from_cids(sorts.iter().map(|s| &s.column)) // we only use SELECTTed columns in ORDER BY, so the columns can have high complexity .allow_up_to(Complexity::Aggregation) .should_select(true) } SqlTransform::Sort(sorts) if !following.contains("Aggregate") => { Requirements::from_cids(sorts.iter().map(|s| &s.column)) } SqlTransform::DistinctOn(partition) => Requirements::from_cids(partition.iter()) // Since there is aggregation anyway, columns can have any complexity .allow_up_to(Complexity::highest()), Super(Transform::Take(rq::Take { range, .. })) => [&range.start, &range.end] .into_iter() .flatten() .map(Requirements::from_expr) .fold(Requirements::default(), Requirements::append), SqlTransform::Join { filter, .. } => Requirements::from_expr(filter), _ => Requirements::default(), } } /// Complexity of a column expressions. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum Complexity { /// Simple non-aggregated and non-windowed expressions Plain, /// Expressions that cannot be used in GROUP BY (CASE) NonGroup, /// Non-aggregated expressions Windowed, /// Everything Aggregation, } impl Complexity { const fn lowest() -> Self { Self::Plain } const fn highest() -> Self { Self::Aggregation } } pub fn infer_complexity(compute: &Compute) -> Complexity { use Complexity::*; if compute.window.is_some() { Windowed } else if compute.is_aggregation { Aggregation } else { infer_complexity_expr(&compute.expr) } } pub fn infer_complexity_expr(expr: &Expr) -> Complexity { match &expr.kind { rq::ExprKind::Case(_) => Complexity::NonGroup, rq::ExprKind::Operator { args, .. } => args .iter() .map(infer_complexity_expr) .max() .unwrap_or(Complexity::Plain), rq::ExprKind::ColumnRef(_) | rq::ExprKind::Literal(_) | rq::ExprKind::SString(_) | rq::ExprKind::Param(_) => Complexity::Plain, rq::ExprKind::Array(elements) => elements .iter() .map(infer_complexity_expr) .max() .unwrap_or(Complexity::Plain), } } #[derive(Default)] pub struct CidCollector { // we could use HashSet instead of Vec, but this caused nondeterministic // results downstream cids: Vec, } impl CidCollector { pub fn collect(expr: Expr) -> Vec { let mut collector = CidCollector::default(); collector.fold_expr(expr).unwrap(); collector.cids } pub fn collect_t(t: Transform) -> (Transform, Vec) { let mut collector = CidCollector::default(); let t = collector.fold_transform(t).unwrap(); (t, collector.cids) } } impl RqFold for CidCollector { fn fold_cid(&mut self, cid: CId) -> Result { self.cids.push(cid); Ok(cid) } } pub(super) struct CidRedirector<'a> { ctx: &'a mut AnchorContext, cid_redirects: HashMap, } impl<'a> CidRedirector<'a> { pub fn of_first_from(pipeline: &[SqlTransform], ctx: &'a mut AnchorContext) -> Option { let from = pipeline.first()?.as_from()?; let relation_instance = &ctx.relation_instances[from]; let cid_redirects = relation_instance.cid_redirects.clone(); Some(CidRedirector { ctx, cid_redirects }) } pub fn redirect_pipeline( pipeline: Vec, ctx: &'a mut AnchorContext, ) -> Vec { let Some(mut redirector) = Self::of_first_from(&pipeline, ctx) else { return pipeline; }; redirector.fold_sql_transforms(pipeline).unwrap() } /// Redirects cids within a context of a pipeline. /// This will find cid_redirects of the first From in the pipeline. pub fn redirect_cids( cids: Vec, pipeline: &[SqlTransform], ctx: &'a mut AnchorContext, ) -> Vec { // find cid_redirects let Some(mut redirector) = Self::of_first_from(pipeline, ctx) else { return cids; }; redirector.fold_cids(cids).unwrap() } pub fn redirect_sorts( sorts: Vec>, riid: &RIId, ctx: &'a mut AnchorContext, ) -> Vec> { let cid_redirects = ctx.relation_instances[riid].cid_redirects.clone(); log::debug!("redirect sorts {sorts:?} {riid:?} cid_redirects {cid_redirects:?}"); let mut redirector = CidRedirector { ctx, cid_redirects }; fold_column_sorts(&mut redirector, sorts).unwrap() } } impl RqFold for CidRedirector<'_> { fn fold_cid(&mut self, cid: CId) -> Result { let v = self.cid_redirects.get(&cid).cloned().unwrap_or(cid); log::debug!("mapping {cid:?} via {0:?} to {v:?}", self.cid_redirects); Ok(v) } fn fold_transform(&mut self, transform: Transform) -> Result { match transform { Transform::Compute(compute) => { let compute = self.fold_compute(compute)?; self.ctx.register_compute(compute.clone()); Ok(Transform::Compute(compute)) } _ => fold_transform(self, transform), } } } impl PqMapper for CidRedirector<'_> { fn fold_rel(&mut self, rel: RIId) -> Result { Ok(rel) } fn fold_super(&mut self, sup: Transform) -> Result { self.fold_transform(sup) } } ================================================ FILE: prqlc/prqlc/src/sql/pq/ast.rs ================================================ //! Sql Relational Query AST //! //! This IR dictates the structure of the resulting SQL query. This includes number of CTEs, //! position of sub-queries and set operations. use enum_as_inner::EnumAsInner; use itertools::Itertools; use prqlc_parser::generic::InterpolateItem; use serde::Serialize; use super::context::RIId; use crate::ir::generic::ColumnSort; use crate::ir::pl::JoinSide; use crate::ir::rq::{self, fold_column_sorts, RelationLiteral, RqFold}; use crate::Result; #[derive(Debug, Clone, Serialize)] pub struct SqlQuery { /// Common Table Expression (WITH clause) pub ctes: Vec, /// The body of SELECT query. pub main_relation: SqlRelation, } #[derive(Debug, Clone, EnumAsInner, Serialize)] pub enum SqlRelation { AtomicPipeline(Vec>), Literal(RelationLiteral), SString(Vec>), Operator { name: String, args: Vec }, } #[derive(Debug, Clone, Serialize)] pub struct RelationExpr { pub kind: RelationExprKind, pub riid: RIId, } #[derive(Debug, Clone, Serialize)] pub enum RelationExprKind { Ref(rq::TId), SubQuery(SqlRelation), } #[derive(Debug, Clone, Serialize)] pub struct Cte { pub tid: rq::TId, pub kind: CteKind, } #[derive(Debug, Clone, Serialize)] pub enum CteKind { Normal(SqlRelation), Loop { initial: SqlRelation, step: SqlRelation, }, } /// Similar to [rq::Transform], but closer to a SQL clause. /// /// Uses two generic args that allows the compiler to work in multiple stages: /// - the first converts RQ to [SqlTransform], /// - the second compiles that to [SqlTransform]. #[derive(Debug, Clone, EnumAsInner, strum::AsRefStr, Serialize)] pub enum SqlTransform { /// Contains [rq::Transform] during compilation. After finishing, this is emptied. /// /// For example, initial an RQ Append transform is wrapped as such: /// /// ```ignore /// rq::Transform::Append(x) -> pq::SqlTransform::Super(rq::Transform::Append(x)) /// ``` /// /// During preprocessing it is compiled to: /// ```ignore /// pq::SqlTransform::Super(rq::Transform::Append(_)) -> pq::SqlTransform::Union { .. } /// ``` /// /// At the end of PQ compilation, all `Super()` are either discarded or converted to their /// PQ equivalents. Super(Super), From(Rel), Select(Vec), Filter(rq::Expr), Aggregate { partition: Vec, compute: Vec, }, Sort(Vec>), Take(rq::Take), Join { side: JoinSide, with: Rel, filter: rq::Expr, }, Distinct, DistinctOn(Vec), Except { bottom: Rel, distinct: bool, }, Intersect { bottom: Rel, distinct: bool, }, Union { bottom: Rel, distinct: bool, }, } impl SqlTransform { pub fn as_str(&self) -> &str { match self { SqlTransform::Super(t) => t.as_ref(), _ => self.as_ref(), } } pub fn into_super_and Result>( self, f: F, ) -> Result { self.into_super() .and_then(|t| f(t).map_err(SqlTransform::Super)) } } pub trait PqMapper: RqFold { fn fold_rel(&mut self, rel: RelIn) -> Result; fn fold_super(&mut self, sup: SuperIn) -> Result; fn fold_sql_transforms( &mut self, transforms: Vec>, ) -> Result>> { transforms .into_iter() .map(|t| self.fold_sql_transform(t)) .try_collect() } fn fold_sql_transform( &mut self, transform: SqlTransform, ) -> Result> { fold_sql_transform::(self, transform) } } pub fn fold_sql_transform< RelIn, RelOut, SuperIn, SuperOut, F: ?Sized + PqMapper, >( fold: &mut F, transform: SqlTransform, ) -> Result> { Ok(match transform { SqlTransform::Super(t) => SqlTransform::Super(fold.fold_super(t)?), SqlTransform::From(rel) => SqlTransform::From(fold.fold_rel(rel)?), SqlTransform::Join { side, with, filter } => SqlTransform::Join { side, with: fold.fold_rel(with)?, filter: fold.fold_expr(filter)?, }, SqlTransform::Distinct => SqlTransform::Distinct, SqlTransform::DistinctOn(ids) => SqlTransform::DistinctOn(fold.fold_cids(ids)?), SqlTransform::Union { bottom, distinct } => SqlTransform::Union { bottom: fold.fold_rel(bottom)?, distinct, }, SqlTransform::Except { bottom, distinct } => SqlTransform::Except { bottom: fold.fold_rel(bottom)?, distinct, }, SqlTransform::Intersect { bottom, distinct } => SqlTransform::Intersect { bottom: fold.fold_rel(bottom)?, distinct, }, SqlTransform::Select(v) => SqlTransform::Select(fold.fold_cids(v)?), SqlTransform::Filter(v) => SqlTransform::Filter(fold.fold_expr(v)?), SqlTransform::Aggregate { partition, compute } => SqlTransform::Aggregate { partition: fold.fold_cids(partition)?, compute: fold.fold_cids(compute)?, }, SqlTransform::Sort(v) => SqlTransform::Sort(fold_column_sorts(fold, v)?), SqlTransform::Take(take) => SqlTransform::Take(rq::Take { partition: fold.fold_cids(take.partition)?, sort: fold_column_sorts(fold, take.sort)?, range: take.range, }), }) } pub trait PqFold: PqMapper { fn fold_sql_query(&mut self, query: SqlQuery) -> Result { Ok(SqlQuery { ctes: query .ctes .into_iter() .map(|c| self.fold_cte(c)) .try_collect()?, main_relation: self.fold_sql_relation(query.main_relation)?, }) } fn fold_sql_relation(&mut self, relation: SqlRelation) -> Result { Ok(match relation { SqlRelation::AtomicPipeline(pipeline) => { SqlRelation::AtomicPipeline(self.fold_sql_transforms(pipeline)?) } _ => relation, }) } fn fold_cte(&mut self, cte: Cte) -> Result { Ok(Cte { tid: cte.tid, kind: match cte.kind { CteKind::Normal(rel) => CteKind::Normal(self.fold_sql_relation(rel)?), CteKind::Loop { initial, step } => CteKind::Loop { initial: self.fold_sql_relation(initial)?, step: self.fold_sql_relation(step)?, }, }, }) } } ================================================ FILE: prqlc/prqlc/src/sql/pq/context.rs ================================================ //! Transform the parsed AST into a "materialized" AST, by executing functions and //! replacing variables. The materialized AST is "flat", in the sense that it //! contains no query-specific logic. use std::collections::HashMap; use std::iter::zip; use enum_as_inner::EnumAsInner; use serde::Serialize; use super::ast::{SqlRelation, SqlTransform}; use crate::ir::pl::Ident; use crate::ir::rq::{ fold_table, CId, Compute, Relation, RelationColumn, RelationKind, RelationalQuery, RqFold, TId, TableDecl, TableRef, Transform, }; use crate::sql::pq::positional_mapping::PositionalMapper; use crate::utils::{IdGenerator, NameGenerator}; use crate::{ir::pl::TableExternRef::LocalTable, Result}; /// The AnchorContext struct stores information about tables and columns, and /// is used to generate new IDs and names. #[derive(Default, Debug)] pub struct AnchorContext { pub column_decls: HashMap, pub column_names: HashMap, pub table_decls: HashMap, pub relation_instances: HashMap, pub positional_mapping: PositionalMapper, pub col_name: NameGenerator, pub table_name: NameGenerator, pub cid: IdGenerator, pub tid: IdGenerator, pub riid: IdGenerator, } #[derive(Debug, Clone)] pub struct SqlTableDecl { #[allow(dead_code)] pub id: TId, /// Name of the table. Sometimes pull-in from RQ name hints (or database table names). /// Generated in postprocessing. pub name: Option, /// When set, any references to this decl will be redirected to the set TId. pub redirect_to: Option, /// Relation that still needs to be defined (usually as CTE) so it can be referenced by name. /// None means that it has already been defined, or was not needed to be defined in the /// first place. pub relation: RelationStatus, } #[derive(Debug, Clone)] pub enum RelationStatus { /// Table or a common table expression. It can be referenced by name. Defined, /// Relation expression which is yet to be defined. NotYetDefined(RelationAdapter), } #[derive(Debug)] pub struct RelationInstance { pub table_ref: TableRef, /// When a pipeline is split, [CId]s from first pipeline are assigned a new /// [CId] in the second pipeline. pub cid_redirects: HashMap, /// All of cids pulled in when using a wildcard pub original_cids: Vec, } impl RelationStatus { /// Analogous to [Option::take] pub fn take_to_define(&mut self) -> RelationStatus { std::mem::replace(self, RelationStatus::Defined) } } /// A relation which may have already been preprocessed. #[derive(Debug, Clone)] pub enum RelationAdapter { Rq(Relation), Preprocessed(Vec, Vec), Pq(SqlRelation), } impl From for RelationAdapter { fn from(rel: SqlRelation) -> Self { RelationAdapter::Pq(rel) } } impl From for RelationAdapter { fn from(rel: Relation) -> Self { RelationAdapter::Rq(rel) } } /// Table instance id #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] pub struct RIId(usize); impl From for RIId { fn from(id: usize) -> Self { RIId(id) } } /// Column declaration. #[derive(Debug, PartialEq, Clone, strum::AsRefStr, EnumAsInner)] pub enum ColumnDecl { RelationColumn(RIId, CId, RelationColumn), Compute(Box), } impl AnchorContext { /// Returns a new AnchorContext object based on a Query object. This method /// generates new IDs and names for tables and columns as needed. pub fn of(query: RelationalQuery) -> (Self, Relation) { let (cid, tid, query) = IdGenerator::load(query); let context = AnchorContext { cid, tid, riid: IdGenerator::new(), col_name: NameGenerator::new("_expr_"), table_name: NameGenerator::new("table_"), ..Default::default() }; QueryLoader::load(context, query) } // /// Generates a new ID and name for a wildcard column and registers it in the // /// AnchorContext's column_decls HashMap. // pub fn register_wildcard(&mut self, riid: RIId) -> CId { // let id = self.cid.gen(); // let kind = ColumnDecl::RelationColumn(riid, id, RelationColumn::Wildcard); // self.column_decls.insert(id, kind); // id // } pub fn register_compute(&mut self, compute: Compute) { let id = compute.id; let decl = ColumnDecl::Compute(Box::new(compute)); self.column_decls.insert(id, decl); } /// Creates a new table instance and registers it in the AnchorContext's /// table_instances HashMap. Also generates new IDs and names for columns /// as needed. pub fn create_relation_instance( &mut self, table_ref: TableRef, cid_redirects: HashMap, ) -> RIId { let riid = self.riid.gen(); for (col, cid) in &table_ref.columns { let def = ColumnDecl::RelationColumn(riid, *cid, col.clone()); self.column_decls.insert(*cid, def); } let original_cids = table_ref.columns.iter().map(|(_, c)| *c).collect(); let relation_instance = RelationInstance { table_ref, cid_redirects, original_cids, }; self.relation_instances.insert(riid, relation_instance); riid } /// Returns the name of a column if it has been given a name already, or generates /// a new name for it and registers it in the AnchorContext's column_names HashMap. pub(crate) fn ensure_column_name(&mut self, cid: CId) -> Option<&String> { // don't name wildcards & named RelationColumns let decl = &self.column_decls[&cid]; if let ColumnDecl::RelationColumn(_, _, col) = decl { match col { RelationColumn::Single(Some(name)) => { let entry = self.column_names.entry(cid); return Some(entry.or_insert_with(|| name.clone())); } RelationColumn::Wildcard => return None, _ => {} } } let entry = self.column_names.entry(cid); Some(entry.or_insert_with(|| self.col_name.gen())) } pub(super) fn load_names( &mut self, pipeline: &[SqlTransform], output_cols: Vec, ) { let output_cids = self.determine_select_columns(pipeline); assert_eq!(output_cids.len(), output_cols.len()); for (cid, col) in zip(output_cids.iter(), output_cols) { if let RelationColumn::Single(Some(name)) = col { self.column_names.insert(*cid, name); } } } pub(super) fn determine_select_columns(&self, pipeline: &[SqlTransform]) -> Vec { use SqlTransform::Super; if let Some((last, remaining)) = pipeline.split_last() { match last { SqlTransform::From(table) => { let rel = self.relation_instances.get(table).unwrap(); rel.table_ref.columns.iter().map(|(_, cid)| *cid).collect() } SqlTransform::Join { with, .. } => { let mut cols = self.determine_select_columns(remaining); let with = self.relation_instances.get(with).unwrap(); let with = &with.table_ref.columns; cols.extend(with.iter().map(|(_, cid)| *cid)); cols } Super(Transform::Select(cols)) => cols.clone(), Super(Transform::Aggregate { partition, compute }) => { [partition.clone(), compute.clone()].concat() } _ => self.determine_select_columns(remaining), } } else { Vec::new() } } // /// Returns a set of all columns of all tables in a pipeline // pub(super) fn collect_pipeline_inputs( // &self, // pipeline: &[SqlTransform], // ) -> Result<(Vec, HashSet)> { // let mut tables = Vec::new(); // let mut columns = HashSet::new(); // for t in pipeline { // if let SqlTransform::From(riid) | SqlTransform::Join { with: riid, .. } = t { // tables.push(*riid); // let rel = self.relation_instances.get(riid).unwrap(); // columns.extend(rel.table_ref.columns.iter().map(|(_, cid)| cid)); // } // } // Ok((tables, columns)) // } pub(super) fn contains_wildcard(&self, cids: &[CId]) -> bool { for cid in cids { let decl = &self.column_decls[cid]; if let ColumnDecl::RelationColumn(_, _, RelationColumn::Wildcard) = decl { return true; } } false } pub fn lookup_table_decl(&self, tid: &TId) -> Option<&SqlTableDecl> { let mut tid = tid; loop { let res = self.table_decls.get(tid)?; match &res.redirect_to { Some(redirect) => tid = redirect, None => return Some(res), } } } } /// Loads info about [Query] into [AnchorContext] struct QueryLoader { context: AnchorContext, } impl QueryLoader { fn load(context: AnchorContext, query: RelationalQuery) -> (AnchorContext, Relation) { let mut loader = QueryLoader { context }; for t in query.tables { loader.load_table(t).unwrap(); } let relation = loader.fold_relation(query.relation).unwrap(); (loader.context, relation) } fn load_table(&mut self, table: TableDecl) -> Result<()> { let decl = fold_table(self, table)?; let mut name = decl.name.clone().map(Ident::from_name); // assume name of the LocalTable that the relation is referencing if let RelationKind::ExternRef(LocalTable(table)) = &decl.relation.kind { name = Some(table.clone()); } let sql_decl = SqlTableDecl { id: decl.id, name, relation: if matches!(decl.relation.kind, RelationKind::ExternRef(_)) { // this relation can be materialized by just using table name as a reference // ... i.e. it's already defined. RelationStatus::Defined } else { // this relation should be defined when needed RelationStatus::NotYetDefined(decl.relation.into()) }, redirect_to: None, }; self.context.table_decls.insert(decl.id, sql_decl); Ok(()) } } impl RqFold for QueryLoader { fn fold_compute(&mut self, compute: Compute) -> Result { self.context.register_compute(compute.clone()); Ok(compute) } fn fold_table_ref(&mut self, table_ref: TableRef) -> Result { Ok(table_ref) } } ================================================ FILE: prqlc/prqlc/src/sql/pq/gen_query.rs ================================================ //! This module is responsible for translating RQ to PQ. use std::str::FromStr; use itertools::Itertools; use super::super::{Context, Dialect}; use super::anchor::{self, anchor_split}; use super::ast::{self as pq, fold_sql_transform, PqMapper}; use super::context::{AnchorContext, RIId, RelationAdapter, RelationStatus}; use super::{postprocess, preprocess}; use crate::debug; use crate::ir::rq::{self, RqFold}; use crate::utils::BreakUp; use crate::{Result, Target}; pub(in super::super) fn compile_query( query: rq::RelationalQuery, dialect: Option, ) -> Result<(pq::SqlQuery, Context)> { debug::log_stage(debug::Stage::Sql(debug::StageSql::Anchor)); let dialect = if let Some(dialect) = dialect { dialect } else { let target = query.def.other.get("target"); let Target::Sql(maybe_dialect) = target .map(|s| Target::from_str(s)) .transpose()? .unwrap_or_default(); maybe_dialect.unwrap_or_default() }; let (anchor, main_relation) = AnchorContext::of(query); let mut ctx = Context::new(dialect, anchor); // compile main relation that will recursively compile CTEs let main_relation = compile_relation(main_relation.into(), &mut ctx)?; // attach CTEs let ctes = ctx.ctes.drain(..).collect_vec(); let query = pq::SqlQuery { main_relation, ctes, }; debug::log_entry(|| debug::DebugEntryKind::ReprPq(query.clone())); debug::log_stage(debug::Stage::Sql(debug::StageSql::Postprocess)); let query = postprocess::postprocess(query, &mut ctx); debug::log_entry(|| debug::DebugEntryKind::ReprPq(query.clone())); Ok((query, ctx)) } fn compile_relation(relation: RelationAdapter, ctx: &mut Context) -> Result { log::trace!("compiling relation {relation:#?}"); Ok(match relation { RelationAdapter::Rq(rel) => { match rel.kind { // base case rq::RelationKind::Pipeline(pipeline) => { // preprocess let pipeline = preprocess::preprocess(pipeline, ctx)?; // load names of output columns ctx.anchor.load_names(&pipeline, rel.columns); compile_pipeline(pipeline, ctx)? } rq::RelationKind::Literal(lit) => pq::SqlRelation::Literal(lit), rq::RelationKind::SString(items) => pq::SqlRelation::SString(items), rq::RelationKind::BuiltInFunction { name, args } => { pq::SqlRelation::Operator { name, args } } // ref cannot be converted directly into query and does not need it's own CTE rq::RelationKind::ExternRef(_) => unreachable!(), } } RelationAdapter::Preprocessed(pipeline, columns) => { // load names of output columns ctx.anchor.load_names(&pipeline, columns); compile_pipeline(pipeline, ctx)? } RelationAdapter::Pq(rel) => rel, }) } fn compile_pipeline( mut pipeline: Vec, ctx: &mut Context, ) -> Result { use pq::SqlTransform::Super; // special case: loop if pipeline .iter() .any(|t| matches!(t, Super(rq::Transform::Loop(_)))) { pipeline = compile_loop(pipeline, ctx)?; } // extract an atomic pipeline from back of the pipeline and stash preceding part into context let pipeline = anchor::extract_atomic(pipeline, &mut ctx.anchor); // ensure names for all columns that need it ensure_names(&pipeline, &mut ctx.anchor); log::trace!("compiling pipeline {pipeline:#?}"); let mut c = TransformCompiler { ctx }; let pipeline = c.fold_sql_transforms(pipeline)?; Ok(pq::SqlRelation::AtomicPipeline(pipeline)) } struct TransformCompiler<'a> { ctx: &'a mut Context, } impl RqFold for TransformCompiler<'_> {} impl PqMapper for TransformCompiler<'_> { fn fold_rel(&mut self, rel: RIId) -> Result { compile_relation_instance(rel, self.ctx) } fn fold_super(&mut self, _: rq::Transform) -> Result<()> { unreachable!() } fn fold_sql_transforms( &mut self, transforms: Vec>, ) -> Result>> { transforms .into_iter() .map(|transform| { Ok(Some(match transform { pq::SqlTransform::From(v) => pq::SqlTransform::From(self.fold_rel(v)?), pq::SqlTransform::Join { side, with, filter } => pq::SqlTransform::Join { side, with: self.fold_rel(with)?, filter, }, pq::SqlTransform::Super(sup) => { match sup { rq::Transform::Select(v) => pq::SqlTransform::Select(v), rq::Transform::Filter(v) => pq::SqlTransform::Filter(v), rq::Transform::Aggregate { partition, compute } => { pq::SqlTransform::Aggregate { partition, compute } } rq::Transform::Sort(v) => pq::SqlTransform::Sort(v), rq::Transform::Take(v) => pq::SqlTransform::Take(v), rq::Transform::Compute(_) | rq::Transform::Append(_) | rq::Transform::Loop(_) => { // these are not used from here on return Ok(None); } rq::Transform::From(_) | rq::Transform::Join { .. } => unreachable!(), } } _ => fold_sql_transform(self, transform)?, })) }) .flat_map(|x| x.transpose()) .try_collect() } } pub(super) fn compile_relation_instance(riid: RIId, ctx: &mut Context) -> Result { ctx.anchor.positional_mapping.activate_mapping(&riid); let rel_instance = &ctx.anchor.relation_instances[&riid]; let nb_redirects = rel_instance.cid_redirects.len(); let table_ref = &rel_instance.table_ref; let source = table_ref.source; let decl = ctx.anchor.table_decls.get_mut(&table_ref.source).unwrap(); // ensure that the table is declared if let RelationStatus::NotYetDefined(sql_relation) = decl.relation.take_to_define() { // if we cannot use CTEs (probably because we are within RECURSIVE) if !(ctx.query.allow_ctes && table_ref.prefer_cte) { // restore relation for other references decl.relation = RelationStatus::NotYetDefined(sql_relation.clone()); // return a sub-query let relation = compile_relation(sql_relation, ctx)?; return Ok(pq::RelationExpr { kind: pq::RelationExprKind::SubQuery(relation), riid, }); } let relation = compile_relation(sql_relation, ctx)?; if let pq::SqlRelation::AtomicPipeline(pipeline) = &relation { // Finding the last select statement of the pipeline let last_select_columns = pipeline.iter().rev().find_map(|transform| match transform { pq::SqlTransform::Select(cids) => Some(cids), _ => None, }); log::debug!("last select CIds for {riid:?}: {last_select_columns:?}"); // If the pipeline ends with a select, we must recompute its CId redirects if let Some(cids) = last_select_columns { // Only recompute the CId redirects if there are exactly as many columns in the // SELECT as there are CId redirects. This probably means that it is a projecting // select added by `anchor_split` if nb_redirects == cids.len() { log::debug!( "recomputing cid_redirects for {riid:?}. current redirects: {:?}", ctx.anchor.relation_instances[&riid].cid_redirects ); // Inefficient but only way to ensure that the new redirects match the original cids let new_redirects = cids .iter() .zip(&ctx.anchor.relation_instances[&riid].original_cids) .map(|(new_cid, original_cid)| { let key_for_value = ctx.anchor.relation_instances[&riid] .cid_redirects .iter() .find_map(|(k, v)| if v == original_cid { Some(k) } else { None }) .unwrap(); ( *new_cid, ctx.anchor.relation_instances[&riid].cid_redirects[key_for_value], ) }) .collect(); log::debug!( "recomputed cid_redirects for {riid:?}. new redirects: {new_redirects:?}", ); ctx.anchor .relation_instances .get_mut(&riid) .unwrap() .cid_redirects = new_redirects; } } } ctx.ctes.push(pq::Cte { tid: source, kind: pq::CteKind::Normal(relation), }); } Ok(pq::RelationExpr { kind: pq::RelationExprKind::Ref(source), riid, }) } fn compile_loop( pipeline: Vec, ctx: &mut Context, ) -> Result> { // split the pipeline let (mut initial, mut following) = pipeline.break_up(|t| matches!(t, pq::SqlTransform::Super(rq::Transform::Loop(_)))); let loop_ = following.remove(0); let step = loop_.into_super_and(|t| t.into_loop()).unwrap(); let step = preprocess::preprocess(step, ctx)?; // determine columns of the initial table let recursive_columns = ctx.anchor.determine_select_columns(&initial); // do the same thing we do when splitting a pipeline // (defining new columns, redirecting cids) let recursive_columns = pq::SqlTransform::Super(rq::Transform::Select(recursive_columns)); initial.push(recursive_columns.clone()); let step = anchor_split(&mut ctx.anchor, initial, step); let from = step.first().unwrap().as_from().unwrap(); let from = ctx.anchor.relation_instances.get(from).unwrap(); let initial_tid = from.table_ref.source; let initial = ctx.anchor.table_decls.get_mut(&initial_tid).unwrap(); // compile initial let initial = if let RelationStatus::NotYetDefined(rel) = initial.relation.take_to_define() { compile_relation(rel, ctx)? } else { unreachable!() }; // compile step (without producing CTEs) ctx.push_query(); ctx.query.allow_ctes = false; let step = compile_pipeline(step, ctx)?; ctx.pop_query(); // create a split between the loop SELECT statement and the following pipeline let mut following = anchor_split(&mut ctx.anchor, vec![recursive_columns], following); let from = following.first_mut().unwrap(); let from = from.as_from().unwrap(); let from = ctx.anchor.relation_instances.get(from).unwrap(); let from = &from.table_ref; // this will be table decl that references the whole loop expression let loop_decl = ctx.anchor.table_decls.get_mut(&from.source).unwrap(); loop_decl.redirect_to = Some(initial_tid); loop_decl.relation = RelationStatus::Defined; // push the whole thing into WITH of the main query ctx.ctes.push(pq::Cte { tid: from.source, kind: pq::CteKind::Loop { initial, step }, }); Ok(following) } fn ensure_names(transforms: &[pq::SqlTransform], ctx: &mut AnchorContext) { for t in transforms { if let pq::SqlTransform::Super(rq::Transform::Sort(columns)) | pq::SqlTransform::Sort(columns) = t { for r in columns { ctx.ensure_column_name(r.column); } } } } ================================================ FILE: prqlc/prqlc/src/sql/pq/mod.rs ================================================ //! Partitioned Query //! //! This in an internal intermediate representation between RQ and SQL AST. //! //! For example, RQ does not have a separate node for DISTINCT, but uses [crate::pr::rq::Take] 1 with //! `partition`. In [super::pq::preprocess] module, [crate::pr::rq::Transform] take is wrapped into //! [ast::SqlTransform], which does have [`ast::SqlTransform::Distinct`]. //! //! This module also contains the compiler from RQ to PQ. mod anchor; pub mod ast; pub mod context; mod gen_query; mod positional_mapping; mod postprocess; pub mod preprocess; pub(super) use gen_query::compile_query; #[cfg(test)] mod test { use super::ast::SqlQuery; use super::*; use crate::sql::Dialect; use crate::{Errors, Result}; fn parse_and_resolve(source: &str) -> Result { let query = crate::semantic::test::parse_resolve_and_lower(source)?; let (sql, _) = compile_query(query, Some(Dialect::Generic))?; Ok(sql) } fn count_atomics(prql: &str) -> Result { let query = parse_and_resolve(prql)?; Ok(query.ctes.len() + 1) } #[test] fn test_ctes_of_pipeline() { // One aggregate, take at the end let prql: &str = r#" from employees filter country == "USA" aggregate {sal = average salary} sort sal take 20 "#; assert!(count_atomics(prql).unwrap() == 1); // One aggregate, but take at the top let prql: &str = r#" from employees take 20 filter country == "USA" aggregate {sal = average salary} sort sal "#; assert!(count_atomics(prql).unwrap() == 2); // A take, then two aggregates let prql: &str = r#" from employees take 20 filter country == "USA" aggregate {sal = average salary} aggregate {sal2 = average sal} sort sal2 "#; assert!(count_atomics(prql).unwrap() == 3); // A take, then a select let prql: &str = r###" from employees take 20 select first_name "###; assert!(count_atomics(prql).unwrap() == 1); } } ================================================ FILE: prqlc/prqlc/src/sql/pq/positional_mapping.rs ================================================ use std::collections::HashMap; use crate::{ ir::rq::{CId, Compute, Transform}, sql::{ pq::{anchor::Requirements, context::RIId}, pq_ast::SqlTransform, }, }; /// State required to properly handle the transforms that are order sensitive like `Union`. #[derive(Default, Debug)] pub struct PositionalMapper { pub relation_positional_mapping: HashMap>, pub active_positional_mapping: Option>, } impl PositionalMapper { /// Remember the mapping for this `RIId` to know what to apply for `apply_positional_mapping`. pub(crate) fn activate_mapping(&mut self, riid: &RIId) { self.active_positional_mapping = self.relation_positional_mapping.remove(riid); log::trace!( "loading remapping for {riid:?}: {:?}", self.active_positional_mapping ); } /// Reorder or remove columns to make `Union` happy. pub(crate) fn apply_active_mapping(&mut self, output: Vec) -> Vec { if let Some(mapping) = &self.active_positional_mapping { // Check if the mapping indices are valid for the output if mapping.iter().any(|idx| *idx >= output.len()) { log::warn!( "positional mapping indices out of bounds: mapping={mapping:?}, output_len={}", output.len() ); // If mapping is invalid, don't apply it return output; } let new_output = mapping.iter().map(|idx| output[*idx]).collect(); log::debug!("remapping {output:?} to {new_output:?} via {mapping:?}"); new_output } else { output } } pub fn compute_and_store_mapping(&mut self, before: &[CId], after: &[CId], riid: &RIId) { let mapping: Vec<_> = after .iter() .flat_map(|a| match before.iter().position(|b| b == a) { Some(idx) => Some(idx), None => { log::warn!(".. no counterpart for column {a:?}"); None } }) .collect(); // Only store the mapping if it's complete (all columns matched) // If mapping is incomplete, it means the bottom relation hasn't been fully // compiled yet, so we shouldn't apply any mapping if mapping.len() == after.len() && !self.relation_positional_mapping.contains_key(riid) { log::debug!(".. relation {riid:?} will be mapped: {mapping:?}"); self.relation_positional_mapping.insert(*riid, mapping); } else if mapping.len() != after.len() { log::debug!( ".. skipping incomplete mapping for {riid:?}: {}/{} columns matched", mapping.len(), after.len() ); } } } /// Outputs the columns required for position sensitive transforms in the pipeline. pub fn compute_positional_mappings( pipeline: &[SqlTransform], requirements: Option<&Requirements>, ) -> Vec<(RIId, Vec)> { let mut constraints = vec![]; let mut columns = vec![]; log::trace!("traversing pipeline to obtain positional mapping:"); // Only process selected columns to avoid surnumerary one let add_columns = |columns: &mut Vec, cids: &[CId]| { if let Some(requirements) = requirements { columns.extend(cids.iter().filter(|cid| requirements.is_selected(cid))); } else { columns.extend_from_slice(cids); } }; for transform in pipeline { match transform { SqlTransform::Super(s) => match s { Transform::Compute(Compute { id, .. }) => { if !columns.contains(id) { add_columns(&mut columns, &[*id]); } } Transform::Select(cids) => { columns.clear(); add_columns(&mut columns, cids); } Transform::Aggregate { compute, .. } => { columns.clear(); add_columns(&mut columns, compute); } _ => (), }, SqlTransform::Except { bottom, .. } | SqlTransform::Intersect { bottom, .. } | SqlTransform::Union { bottom, .. } => { constraints.push((*bottom, columns.clone())); log::trace!( ".. mapping for {}/{bottom:?}: {columns:?}", transform.as_str() ); } _ => (), } log::trace!( ".. selected columns after {}: {columns:?}", transform.as_str() ); } constraints } ================================================ FILE: prqlc/prqlc/src/sql/pq/postprocess.rs ================================================ //! An AST pass after compilation to PQ. //! //! Currently only moves [SqlTransform::Sort]s. use std::collections::{HashMap, HashSet, VecDeque}; use itertools::Itertools; use super::anchor::CidRedirector; use super::ast::*; use crate::ir::generic::ColumnSort; use crate::ir::pl::Ident; use crate::ir::rq::{CId, ExprKind, RelationColumn, RqFold, TId}; use crate::sql::pq::context::{ColumnDecl, RIId}; use crate::sql::Context; use crate::Result; type Sorting = Vec>; pub(super) fn postprocess(query: SqlQuery, ctx: &mut Context) -> SqlQuery { let query = infer_sorts(query, ctx); assign_names(query, ctx) } /// Pushes sorts down the pipelines and materializes them only where they are needed. fn infer_sorts(query: SqlQuery, ctx: &mut Context) -> SqlQuery { let mut s = SortingInference { last_sorting: Vec::new(), last_sorting_from_distinct_on: false, ctes_sorting: HashMap::new(), main_relation: false, ctx, }; s.fold_sql_query(query).unwrap() } struct SortingInference<'a> { last_sorting: Sorting, /// True if last_sorting originated from DISTINCT ON (used for row selection). /// /// Per the PRQL spec, `group` resets the order - any `sort` inside a group /// is for internal row selection, not output ordering. This flag tracks such /// internal sorting so it doesn't propagate past transforms like `join`. last_sorting_from_distinct_on: bool, ctes_sorting: HashMap, main_relation: bool, ctx: &'a mut Context, } impl SortingInference<'_> { /// Prepares the last sorting that will be appended to the pipeline of the `SqlQuery` by /// `fold_sql_query`. It does so by reverting all columns in the sorting to their very first /// form, and then transforming their value in the final select, while applying /// renaming/aliasing when possible. This cannot be done directly in `fold_sql_transforms` /// because renames are not considered to be SQL transforms. fn alias_last_sorting(&mut self, mut last_sorting: Sorting, final_select: &[CId]) -> Sorting { log::debug!("unaliasing last sorting: {last_sorting:?}"); let redirects = self .ctx .anchor .relation_instances .iter() .map(|(riid, rel_inst)| (riid, &rel_inst.cid_redirects)) .collect::>(); // a map of column -> alias let column_aliases = self .ctx .anchor .column_decls .values() .filter_map(|col| { if let ColumnDecl::Compute(compute) = col { if let ExprKind::ColumnRef(referenced_id) = compute.expr.kind { Some((referenced_id, compute.id)) } else { None } } else { None } }) .collect::>(); log::debug!(".. column aliases: {column_aliases:?}"); // column -> list of tables that did a revert let mut reverts: HashMap> = HashMap::new(); log::debug!(".. reverting all columns to their original value"); last_sorting.iter_mut().for_each(|sort| { let mut riids = VecDeque::new(); let mut changed = true; while changed { changed = false; if let Some(ColumnDecl::RelationColumn(riid, cid, _)) = self.ctx.anchor.column_decls.get(&sort.column) { let cid_redirects = redirects[riid]; for (source, target) in cid_redirects.iter() { if target == cid { log::debug!( ".. reverting {target:?} back to {source:?} via redirects of {riid:?}" ); sort.column = *source; changed = true; riids.push_front(*riid); break; } } } } reverts.insert(sort.column, riids); }); log::debug!(".. done reverting all columns to their original value: {last_sorting:?}"); log::debug!(".. reverting columns forward and aliasing them"); // reverting forward last_sorting.iter_mut().for_each(|sort| { let col_reverts = &reverts[&sort.column]; for riid in col_reverts { if final_select.contains(&sort.column) { log::debug!( ".. sort column {:?} is in the final select columns, skip reverting", &sort.column ); return; } // try renaming if column_aliases.contains_key(&sort.column) { let alias = column_aliases[&sort.column]; log::debug!("..aliasing {:?} as {alias:?}", &sort.column); sort.column = alias; } // try de-reverting with the target table let cid_mappings = redirects[riid]; if cid_mappings.contains_key(&sort.column) { log::debug!( ".. reverting {:?} forward to {:?} via redirects of {riid:?} ({:?})", &sort.column, &cid_mappings[&sort.column], &cid_mappings ); sort.column = cid_mappings[&sort.column]; } } }); log::debug!("aliased and reverted last sorting forward: {last_sorting:?}"); last_sorting } } #[derive(Debug)] struct CteSorting { sorting: Sorting, /// True if the CTE's sorting originated from DISTINCT ON (row selection). /// /// Per the PRQL spec, `group` resets the order. DISTINCT ON sorting is /// internal to the group - it determines which row to keep, not output /// ordering. This flag ensures such sorting doesn't leak to outer queries. from_distinct_on: bool, } impl RqFold for SortingInference<'_> {} /// Returns the CIds for the last SELECT statement in an AtomicPipeline CTE, or None if no such /// statement is found. fn find_last_select_for_cte(cte: &Cte) -> Option<&Vec> { match &cte.kind { CteKind::Normal(SqlRelation::AtomicPipeline(pipeline)) => { pipeline.iter().rev().find_map(|transform| { if let SqlTransform::Select(cids) = transform { Some(cids) } else { None } }) } _ => None, } } impl PqFold for SortingInference<'_> { fn fold_sql_query(&mut self, query: SqlQuery) -> Result { let mut ctes = Vec::with_capacity(query.ctes.len()); for cte in query.ctes { log::debug!("infer_sorts: {0:?}", cte.tid); let last_select_before_fold = find_last_select_for_cte(&cte).cloned(); let cte = self.fold_cte(cte)?; let last_select_after_fold = find_last_select_for_cte(&cte); // Checking if CTE folding has added some new columns to the CTE match (last_select_before_fold.as_ref(), last_select_after_fold) { (Some(before), Some(after)) if before != after => { let new_columns = after .iter() .filter(|cid| !before.contains(cid)) .collect::>(); // if new columns are added, the relation instance needs to be updated if !new_columns.is_empty() { let mut cid_redirects_to_add = Vec::new(); for old_cid in new_columns { let new_cid = self.ctx.anchor.cid.gen(); let name = self.ctx.anchor.ensure_column_name(*old_cid).cloned(); if let Some(name) = name.clone() { self.ctx.anchor.column_names.insert(new_cid, name); } let old_def = self.ctx.anchor.column_decls.get(old_cid).unwrap(); let col = match old_def { ColumnDecl::RelationColumn(_, _, RelationColumn::Wildcard) => { RelationColumn::Wildcard } _ => RelationColumn::Single(name), }; cid_redirects_to_add.push((*old_cid, new_cid, col)); } let (riid, relation_instance) = self .ctx .anchor .relation_instances .iter_mut() .find(|(_riid, rel_inst)| rel_inst.table_ref.source == cte.tid) .unwrap(); cid_redirects_to_add .into_iter() .for_each(|(old_cid, new_cid, col)| { let def = ColumnDecl::RelationColumn(*riid, new_cid, col); self.ctx.anchor.column_decls.insert(new_cid, def); log::debug!( "-- redirecting {old_cid:?} to {new_cid:?} for CTE {cte:?} (RIId: {riid:?})", cte = cte.tid, riid = riid ); relation_instance.cid_redirects.insert(old_cid, new_cid); }); } } _ => {} } // store sorting to be used later in From references let sorting = self.last_sorting.drain(..).collect(); log::debug!("--- sorting {sorting:?}"); let sorting = CteSorting { sorting, from_distinct_on: self.last_sorting_from_distinct_on, }; self.last_sorting_from_distinct_on = false; self.ctes_sorting.insert(cte.tid, sorting); ctes.push(cte); } // fold main_relation log::debug!("infer_sorts: main relation"); self.main_relation = true; let mut main_relation = self.fold_sql_relation(query.main_relation)?; log::debug!("--== last_sorting {0:?}", self.last_sorting); let last_sorting = self.last_sorting.drain(..).collect::>(); // push a sort at the back of the main pipeline if let SqlRelation::AtomicPipeline(pipeline) = &mut main_relation { let from_id = pipeline .iter() .find_map(|transform| match transform { SqlTransform::From(rel) => Some(rel.riid), _ => None, }) .unwrap(); let final_select = pipeline .iter() .rev() .find_map(|transform| match transform { SqlTransform::Select(select) => Some(select), _ => None, }) .unwrap(); log::debug!("--== final select: {final_select:?}"); let unaliased_last_sorting = self.alias_last_sorting(last_sorting, final_select); log::debug!("--== unaliased last sorting: {unaliased_last_sorting:?}"); let redirected_last_sorting = CidRedirector::redirect_sorts( unaliased_last_sorting, &from_id, &mut self.ctx.anchor, ); log::debug!("--== redirected last sorting: {redirected_last_sorting:?}"); pipeline.push(SqlTransform::Sort(redirected_last_sorting)); } Ok(SqlQuery { ctes, main_relation, }) } } impl PqMapper for SortingInference<'_> { fn fold_rel(&mut self, rel: RelationExpr) -> Result { Ok(rel) } fn fold_super(&mut self, sup: ()) -> Result<()> { Ok(sup) } fn fold_sql_transforms( &mut self, transforms: Vec>, ) -> Result>> { let mut sorting = Vec::new(); // Track whether sorting originated from DISTINCT ON (internal row selection). // Per PRQL spec, `group` resets order - internal sorts don't define output order. let mut sorting_from_distinct_on = false; let mut result = Vec::with_capacity(transforms.len() + 1); for mut transform in transforms { match transform { SqlTransform::From(mut expr) => { match expr.kind { RelationExprKind::Ref(ref tid) => { // infer sorting from referenced pipeline if let Some(cte_sorting) = self.ctes_sorting.get(tid) { sorting.clone_from(&cte_sorting.sorting); sorting_from_distinct_on = cte_sorting.from_distinct_on; } else { sorting.clear(); sorting_from_distinct_on = false; }; } RelationExprKind::SubQuery(rel) => { let rel = self.fold_sql_relation(rel)?; // infer sorting from sub-query sorting = self.last_sorting.drain(..).collect(); sorting_from_distinct_on = self.last_sorting_from_distinct_on; expr.kind = RelationExprKind::SubQuery(rel); } } sorting = CidRedirector::redirect_sorts(sorting, &expr.riid, &mut self.ctx.anchor); transform = SqlTransform::From(expr); } // just store sorting and don't emit Sort SqlTransform::Sort(expr) => { sorting.clone_from(&expr); // A new explicit Sort clears the DISTINCT ON flag - this is a // user-requested ordering, not an internal DISTINCT ON sort. sorting_from_distinct_on = false; continue; } // clear sorting SqlTransform::Distinct | SqlTransform::Aggregate { .. } => { sorting.clear(); sorting_from_distinct_on = false; } // Per PRQL spec: `group` resets order, `join` retains left's order. // DISTINCT ON sorting is internal to the group (for row selection), // so it must not propagate past joins. Explicit user sorts are preserved. // See issue #4633. SqlTransform::Join { .. } => { if sorting_from_distinct_on { sorting.clear(); sorting_from_distinct_on = false; } } // emit Sort before Take SqlTransform::Take(ref take) => { // For simple takes (no partition), the sort may be embedded in the Take // struct from RQ lowering (sort | take -> Take { sort: [...] }). // Use the embedded sort if present, otherwise use accumulated sorting. // // Invariant: For simple takes, only one of take.sort or sorting should // be non-empty, never both. RQ lowering merges `sort | take` into a // single Take with embedded sort, while explicit SqlTransform::Sort // only appears when sort is not immediately followed by take. let sort_to_emit = if take.partition.is_empty() && !take.sort.is_empty() { take.sort.clone() } else { sorting.clone() }; result.push(SqlTransform::Sort(sort_to_emit)); } SqlTransform::DistinctOn(_) => { // DISTINCT ON uses sorting for row selection within the group. // Mark it so this internal sorting doesn't leak to outer queries. // Note: ROW_NUMBER (used for take > 1 or non-Postgres) doesn't have // this issue because its sorting is embedded in the window function. sorting_from_distinct_on = true; result.push(SqlTransform::Sort(sorting.clone())); } _ => {} } result.push(transform) } log::debug!("-- relation sorting {sorting:?}"); if !self.main_relation { // if this is a CTE, make sure that its SELECT includes the // columns from the sort let select = result.iter_mut().find_map(|x| x.as_select_mut()).unwrap(); for column_sort in &sorting { let cid = column_sort.column; let is_selected = select.contains(&cid); if !is_selected { log::debug!("adding {cid:?} to {select:?}"); select.push(cid); } } } // remember sorting for this pipeline self.last_sorting = sorting; self.last_sorting_from_distinct_on = sorting_from_distinct_on; Ok(result) } } /// Makes sure all relation instances have assigned names. Tries to infer from table references. fn assign_names(query: SqlQuery, ctx: &mut Context) -> SqlQuery { // generate CTE names, make sure they don't clash let decls = ctx.anchor.table_decls.values_mut(); let mut names = HashSet::new(); for decl in decls.sorted_by_key(|d| d.id.get()) { while decl.name.is_none() || names.contains(decl.name.as_ref().unwrap()) { decl.name = Some(Ident::from_name(ctx.anchor.table_name.gen())); } names.insert(decl.name.clone().unwrap()); } // generate relation variable names RelVarNameAssigner { ctx, relation_instance_names: Default::default(), } .fold_sql_query(query) .unwrap() } struct RelVarNameAssigner<'a> { relation_instance_names: HashSet, ctx: &'a mut Context, } impl RqFold for RelVarNameAssigner<'_> {} impl PqFold for RelVarNameAssigner<'_> { fn fold_sql_relation(&mut self, relation: SqlRelation) -> Result { // only fold AtomicPipelines Ok(match relation { SqlRelation::AtomicPipeline(pipeline) => { // save outer names, so they are not affected by the inner pipeline // (this matters for loop, where you have nested pipelines) let outer_names = std::mem::take(&mut self.relation_instance_names); let res = self.fold_sql_transforms(pipeline)?; self.relation_instance_names = outer_names; SqlRelation::AtomicPipeline(res) } _ => relation, }) } } impl PqMapper for RelVarNameAssigner<'_> { fn fold_rel(&mut self, mut rel: RelationExpr) -> Result { // normal fold rel.kind = match rel.kind { RelationExprKind::Ref(tid) => RelationExprKind::Ref(tid), RelationExprKind::SubQuery(sub) => { RelationExprKind::SubQuery(self.fold_sql_relation(sub)?) } }; // make sure that table_ref has a name let riid = &rel.riid; let instance = self.ctx.anchor.relation_instances.get_mut(riid).unwrap(); let name = &mut instance.table_ref.name; if name.is_none() { // it does not // infer from table name *name = match &rel.kind { RelationExprKind::Ref(tid) => { let table_decl = &self.ctx.anchor.table_decls[tid]; table_decl.name.as_ref().map(|i| i.name.clone()) } _ => None, }; } // make sure it is not already present in current query while name .as_ref() .map_or(true, |n| self.relation_instance_names.contains(n)) { *name = Some(self.ctx.anchor.table_name.gen()); } // mark name as used self.relation_instance_names.insert(name.clone().unwrap()); Ok(rel) } fn fold_super(&mut self, sup: ()) -> Result<()> { Ok(sup) } } ================================================ FILE: prqlc/prqlc/src/sql/pq/preprocess.rs ================================================ use std::collections::hash_map::RandomState; use std::collections::{HashMap, HashSet}; use itertools::Itertools; use super::anchor::{infer_complexity, CidCollector, Complexity}; use super::ast::*; use crate::ir::generic::{ColumnSort, SortDirection, WindowFrame, WindowKind}; use crate::ir::pl::{JoinSide, Literal}; use crate::ir::rq::{ self, maybe_binop, new_binop, CId, Compute, Expr, ExprKind, RqFold, Transform, Window, }; use crate::sql::Context; use crate::{debug, Error, Result, WithErrorInfo}; use prqlc_parser::generic::{InterpolateItem, Range}; /// Converts RQ AST into SqlRQ AST and applies a few preprocessing operations. /// /// Note that some SQL translation mechanisms depend on behavior of some of these /// functions (i.e. reorder). pub(in crate::sql) fn preprocess( pipeline: Vec, ctx: &mut Context, ) -> Result> { Ok(pipeline) .and_then(normalize) .and_then(|p| wrap(p, ctx)) .and_then(|p| prune_inputs(p, ctx)) .and_then(|p| distinct(p, ctx)) .and_then(|p| union(p, ctx)) .and_then(|p| except(p, ctx)) .and_then(|p| intersect(p, ctx)) .map(reorder) .map(|p| { debug::log_entry(|| debug::DebugEntryKind::ReprPqEarly(p.clone())); p }) } // This function was disabled because it changes semantics of the pipeline in some cases. // /// Pushes all [Transform::Select]s to the back of the pipeline. // pub(in crate::sql) fn push_down_selects(pipeline: Vec) -> Vec { // let mut select = None; // let mut res = Vec::with_capacity(pipeline.len()); // for t in pipeline { // if let Transform::Select(_) = t { // select = Some(t); // } else { // res.push(t); // } // } // if let Some(select) = select { // res.push(select); // } // res // } /// Removes unused relation inputs pub(in crate::sql) fn prune_inputs( mut pipeline: Vec, ctx: &mut Context, ) -> Result> { use SqlTransform::Super; let mut used_cids = HashSet::new(); let mut res = Vec::new(); while let Some(mut transform) = pipeline.pop() { // collect cids (special case for Join & From) match transform { SqlTransform::Join { ref filter, .. } => { used_cids.extend(CidCollector::collect(filter.clone())); } SqlTransform::From(_) => {} Super(t) => { let (t, cids) = CidCollector::collect_t(t); used_cids.extend(cids); transform = Super(t); } _ => unreachable!(), } // prune unused inputs if let SqlTransform::From(with) | SqlTransform::Join { with, .. } = &mut transform { let relation = ctx.anchor.relation_instances.get_mut(with).unwrap(); (relation.table_ref.columns).retain(|(_, cid)| used_cids.contains(cid)); } res.push(transform); } res.reverse(); Ok(res) } pub(in crate::sql) fn wrap(pipe: Vec, ctx: &mut Context) -> Result> { // We map From and Join into SqlTransforms, because we need to change their RIIds. // Others we just wrap into SqlTransform::Super. pipe.into_iter() .map(|x| { Ok(match x { Transform::From(table_ref) => { let riid = ctx .anchor .create_relation_instance(table_ref, HashMap::new()); SqlTransform::From(riid) } Transform::Join { with, side, filter } => { let with = ctx.anchor.create_relation_instance(with, HashMap::new()); SqlTransform::Join { with, side, filter } } x => SqlTransform::Super(x), }) }) .try_collect() } fn vecs_contain_same_elements(a: &[T], b: &[T]) -> bool { let a: HashSet<&T, RandomState> = a.iter().collect(); let b: HashSet<&T, RandomState> = b.iter().collect(); a == b } /// Creates [SqlTransform::Distinct] from [Transform::Take] pub(in crate::sql) fn distinct( pipeline: Vec, ctx: &mut Context, ) -> Result> { use SqlTransform::Super; use Transform::*; let mut res = Vec::new(); for transform in pipeline.clone() { match transform { Super(Take(rq::Take { ref partition, .. })) if partition.is_empty() => { res.push(transform); } Super(Take(rq::Take { range, partition, sort, })) => { let range_int = range .clone() .try_map(as_int) .map_err(|_| Error::new_simple("Invalid take arguments"))?; let take_only_first = range_int.start.unwrap_or(1) == 1 && matches!(range_int.end, Some(1)); // Check whether the columns within the partition are the same // as the columns in the table; otherwise we can't use DISTINCT. let columns_in_frame = ctx.anchor.determine_select_columns(&pipeline.clone()); let matching_columns = vecs_contain_same_elements(&columns_in_frame, &partition); if take_only_first && sort.is_empty() && matching_columns { // DISTINCT res.push(SqlTransform::Distinct); } else if ctx.dialect.supports_distinct_on() && range_int.end == Some(1) { // DISTINCT ON (only if we want to select only one row per group) let sort = if sort.is_empty() { vec![] } else { [into_column_sort(&partition), sort].concat() }; res.push(SqlTransform::Sort(sort)); res.push(SqlTransform::DistinctOn(partition)); } else { // convert `take range` into: // derive _rn = s"ROW NUMBER" // filter (_rn | in range) res.extend(create_filter_by_row_number(range, sort, partition, ctx)); } } _ => { res.push(transform); } } } Ok(res) } fn into_column_sort(partition: &[CId]) -> Vec> { partition .iter() .map(|cid| ColumnSort { direction: SortDirection::Asc, column: *cid, }) .collect_vec() } fn create_filter_by_row_number( range: Range, sort: Vec>, partition: Vec, ctx: &mut Context, ) -> Vec { // declare new column let expr = Expr { kind: ExprKind::SString(vec![InterpolateItem::String("ROW_NUMBER()".to_string())]), span: None, }; let is_unsorted = sort.is_empty(); let window = Window { frame: if is_unsorted { WindowFrame { kind: WindowKind::Rows, range: Range::unbounded(), } } else { WindowFrame { kind: WindowKind::Range, range: Range { start: None, end: Some(int_expr(0)), }, } }, partition, sort, }; let compute = Compute { id: ctx.anchor.cid.gen(), expr, window: Some(window), is_aggregation: false, }; ctx.anchor.register_compute(compute.clone()); let col_ref = Expr { kind: ExprKind::ColumnRef(compute.id), span: None, }; // add the two transforms let range_int = range.try_map(as_int).unwrap(); let compute = SqlTransform::Super(Transform::Compute(compute)); let filter = SqlTransform::Super(Transform::Filter(match (range_int.start, range_int.end) { (Some(s), Some(e)) if s == e => new_binop(col_ref, "std.eq", int_expr(s)), (start, end) => { let start = start.map(|start| new_binop(col_ref.clone(), "std.gte", int_expr(start))); let end = end.map(|end| new_binop(col_ref, "std.lte", int_expr(end))); maybe_binop(start, "std.and", end).unwrap_or(Expr { kind: ExprKind::Literal(Literal::Boolean(true)), span: None, }) } })); vec![compute, filter] } fn as_int(expr: Expr) -> Result { let lit = expr.kind.as_literal().ok_or(())?; lit.as_integer().cloned().ok_or(()) } fn int_expr(i: i64) -> Expr { Expr { span: None, kind: ExprKind::Literal(Literal::Integer(i)), } } /// Creates [SqlTransform::Union] from [Transform::Append] pub(in crate::sql) fn union( pipeline: Vec, ctx: &mut Context, ) -> Result> { use SqlTransform::*; use Transform::*; let mut res = Vec::with_capacity(pipeline.len()); let mut pipeline = pipeline.into_iter().peekable(); while let Some(t) = pipeline.next() { let Super(Append(bottom)) = t else { res.push(t); continue; }; let bottom = ctx.anchor.create_relation_instance(bottom, HashMap::new()); let distinct = if let Some(Distinct) = &pipeline.peek() { pipeline.next(); true } else { false }; res.push(SqlTransform::Union { bottom, distinct }); } Ok(res) } /// Creates [SqlTransform::Except] from [Transform::Join] and [Transform::Filter] pub(in crate::sql) fn except( pipeline: Vec, ctx: &mut Context, ) -> Result> { use SqlTransform::*; let output = ctx.anchor.determine_select_columns(&pipeline); let output: HashSet = HashSet::from_iter(output); let mut res = Vec::with_capacity(pipeline.len()); for t in pipeline { res.push(t); if res.len() < 2 { continue; } let SqlTransform::Join { side: JoinSide::Left, filter: join_cond, with, } = &res[res.len() - 2] else { continue; }; let Super(Transform::Filter(filter)) = &res[res.len() - 1] else { continue; }; let with = ctx.anchor.relation_instances.get(with).unwrap(); let top = ctx.anchor.determine_select_columns(&res[0..res.len() - 2]); let bottom = with.table_ref.columns.iter().map(|(_, c)| *c).collect_vec(); // join_cond must be a join over all columns // (this could be loosened to check only the relation key) let (join_left, join_right) = collect_equals(join_cond)?; if !all_in(&top, join_left) || !all_in(&bottom, join_right) { continue; } // filter has to check for nullability of bottom // (this could be loosened to check only for nulls in a previously non-nullable column) let (filter_left, filter_right) = collect_equals(filter)?; if !(all_in(&bottom, filter_left) && all_null(filter_right)) { continue; } // select must not contain things from bottom if bottom.iter().any(|c| output.contains(c)) { continue; } // determine DISTINCT let mut distinct = false; // EXCEPT ALL can become except EXCEPT DISTINCT, if top is DISTINCT. // DISTINCT-ness of bottom has no effect on the output. if res.len() >= 3 { if let Distinct = &res[res.len() - 3] { distinct = true; } } if !distinct && !ctx.dialect.except_all() { // EXCEPT ALL is not supported // can we fall back to anti-join? if ctx.anchor.contains_wildcard(&top) || ctx.anchor.contains_wildcard(&bottom) { return Err(Error::new_simple(format!("The dialect {:?} does not support EXCEPT ALL", ctx.dialect)) .push_hint("providing more column information will allow the query to be translated to an anti-join.")); } else { // Don't create Except, fallback to anti-join. continue; } } res.pop(); // filter let join = res.pop(); // join let (_, with, _) = join.unwrap().into_join().unwrap(); if distinct { if let Some(Distinct) = &res.last() { res.pop(); } } res.push(SqlTransform::Except { bottom: with, distinct, }); } Ok(res) } /// Creates [SqlTransform::Intersect] from [Transform::Join] pub(in crate::sql) fn intersect( pipeline: Vec, ctx: &mut Context, ) -> Result> { use SqlTransform::*; let output = ctx.anchor.determine_select_columns(&pipeline); let output: HashSet = HashSet::from_iter(output); let mut res = Vec::with_capacity(pipeline.len()); let mut pipeline = pipeline.into_iter().peekable(); while let Some(t) = pipeline.next() { res.push(t); if res.is_empty() { continue; } let Join { side: JoinSide::Inner, filter: join_cond, with, } = &res[res.len() - 1] else { continue; }; let with = ctx.anchor.relation_instances.get_mut(with).unwrap(); let bottom = with.table_ref.columns.iter().map(|(_, c)| *c).collect_vec(); let top = ctx.anchor.determine_select_columns(&res[0..res.len() - 1]); // join_cond must be a join over all columns // (this could be loosened to check only the relation key) let (left, right) = collect_equals(join_cond)?; if !(all_in(&top, left) && all_in(&bottom, right)) { continue; } // select must not contain things from bottom if bottom.iter().any(|c| output.contains(c)) { continue; } // select must contain at least one thing from top if top.iter().all(|c| !output.contains(c)) { continue; } // determine DISTINCT let mut distinct = false; // INTERSECT ALL can become except INTERSECT DISTINCT // - if top is DISTINCT or // - if output is DISTINCT if res.len() > 1 { if let Distinct = &res[res.len() - 2] { distinct = true; } } if let Some(SqlTransform::Distinct) = pipeline.peek() { distinct = true; } if !distinct && !ctx.dialect.intersect_all() { // INTERCEPT ALL is not supported // can we fall back to anti-join? if ctx.anchor.contains_wildcard(&top) || ctx.anchor.contains_wildcard(&bottom) { return Err(Error::new_simple(format!("The dialect {:?} does not support INTERSECT ALL", ctx.dialect)) .push_hint("providing more column information will allow the query to be translated to an anti-join.")); } else { // Don't create Intercept, fallback to inner join. continue; } } // remove "used up transforms" let join = res.pop(); // join let (_, with, _) = join.unwrap().into_join().unwrap(); if distinct { if let Some(Distinct) = &res.last() { res.pop(); } if let Some(SqlTransform::Distinct) = pipeline.peek() { pipeline.next(); } } // push the new transform res.push(SqlTransform::Intersect { bottom: with, distinct, }); } Ok(res) } /// Returns true if all cids are in exprs fn all_in(cids: &[CId], exprs: Vec<&Expr>) -> bool { let exprs = col_refs(exprs); cids.iter().all(|c| exprs.contains(c)) } fn all_null(exprs: Vec<&Expr>) -> bool { exprs .iter() .all(|e| matches!(e.kind, ExprKind::Literal(Literal::Null))) } /// Converts `(a == b) and ((c == d) and (e == f))` /// into `([a, c, e], [b, d, f])` fn collect_equals(expr: &Expr) -> Result<(Vec<&Expr>, Vec<&Expr>)> { let mut lefts = Vec::new(); let mut rights = Vec::new(); match &expr.kind { ExprKind::Operator { name, args } if name == "std.eq" && args.len() == 2 => { lefts.push(&args[0]); rights.push(&args[1]); } ExprKind::Operator { name, args } if name == "std.and" && args.len() == 2 => { let (l, r) = collect_equals(&args[0])?; lefts.extend(l); rights.extend(r); let (l, r) = collect_equals(&args[1])?; lefts.extend(l); rights.extend(r); } _ => (), } Ok((lefts, rights)) } fn col_refs(exprs: Vec<&Expr>) -> Vec { exprs .into_iter() .flat_map(|expr| expr.kind.as_column_ref().cloned()) .collect() } /// Pull Compute transforms in front of other transforms if possible. /// Position of Compute is important for two reasons: /// - when splitting pipelines, they provide information in which pipeline the /// column is computed and subsequently, with which table name should be used /// for name materialization. /// - the transform order in SQL requires Computes to be before Filter. This /// can be circumvented by materializing the column earlier in the pipeline, /// which is done in this function. pub(in crate::sql) fn reorder(mut pipeline: Vec) -> Vec { use SqlTransform::Super; use Transform::*; // iter over Computes for i in 1..pipeline.len() { if !matches!(&pipeline[i], Super(Compute(_))) { continue; } // iter all preceding transforms for j in 0..(i - 1) { let compute_i = i - j; let prev_i = compute_i - 1; let compute = pipeline[compute_i] .as_super() .unwrap() .as_compute() .unwrap(); let prev = &pipeline[prev_i]; let should_swap = match prev { // don't reorder with From or Join or another Compute SqlTransform::From(_) | SqlTransform::Join { .. } | Super(Compute(_)) => false, // reorder always Super(Sort(_)) => true, // reorder if col decl is plain Super(Take(_)) if infer_complexity(compute) == Complexity::Plain => true, // don't reorder by default _ => false, }; if should_swap { pipeline.swap(compute_i, prev_i); } else { break; } } } pipeline } /// Normalize query: /// - Swap null checks such that null is always on the right side. /// This is needed to simplify code for Except and for compiling to IS NULL. pub(in crate::sql) fn normalize(pipeline: Vec) -> Result> { Normalizer {}.fold_transforms(pipeline) } struct Normalizer {} impl RqFold for Normalizer { fn fold_expr(&mut self, expr: Expr) -> Result { let expr = Expr { kind: rq::fold_expr_kind(self, expr.kind)?, ..expr }; if let ExprKind::Operator { name, args } = &expr.kind { if name == "std.eq" && args.len() == 2 { let (left, right) = (&args[0], &args[1]); let span = expr.span; let new_args = if let ExprKind::Literal(Literal::Null) = &left.kind { vec![right.clone(), left.clone()] } else { vec![left.clone(), right.clone()] }; let new_kind = ExprKind::Operator { name: name.clone(), args: new_args, }; return Ok(Expr { kind: new_kind, span, }); } } Ok(expr) } } ================================================ FILE: prqlc/prqlc/src/sql/std.sql.prql ================================================ #! Implementation of `std` module. #! #! This file is not really PRQL. #! It can contain only: #! - functions declarations that don't have named params and s-string-only body, #! - module declarations whose names correspond to sql dialect names. #! #! Functions can define `binding_strength` annotation, which signifies how much #! precedence does the top-level operation in the s-string provide. #! This value defaults to 100 (high precedence). #! #! S-strings can define required binding strength of the interpolated expression. #! This value defaults to binding strength of the function. # Aggregation functions @{window_frame=true} let min = column -> s"MIN({column:0})" @{window_frame=true} let max = column -> s"MAX({column:0})" @{window_frame=true, coalesce="0"} let sum = column -> s"SUM({column:0})" @{window_frame=true} let average = column -> s"AVG({column:0})" @{window_frame=true} let stddev = column -> s"STDDEV({column:0})" @{window_frame=true, coalesce="TRUE"} let all = column -> s"BOOL_AND({column:0})" @{window_frame=true, coalesce="FALSE"} let any = column -> s"BOOL_OR({column:0})" @{window_frame=true, coalesce="''"} let concat_array = column -> s"STRING_AGG({column:0}, '')" @{window_frame=true} let count = column -> s"COUNT(*)" @{window_frame=true} let count_distinct = column -> s"COUNT(DISTINCT {column:0})" # Window functions let lag = offset column -> s"LAG({column:0}, {offset:0})" let lead = offset column -> s"LEAD({column:0}, {offset:0})" let first = column -> s"FIRST_VALUE({column:0})" let last = column -> s"LAST_VALUE({column:0})" let rank = -> s"RANK()" let rank_dense = -> s"DENSE_RANK()" let row_number = -> s"ROW_NUMBER()" # Mathematical functions module math { # Clickhouse: https://clickhouse.com/docs/en/sql-reference/functions/math-functions # DuckDB: https://duckdb.org/docs/test/functions/math.html # MariaDB: https://mariadb.com/kb/en/numeric-functions/ # MySQL: https://dev.mysql.com/doc/refman/8.4/en/mathematical-functions.html # Postgres: https://www.postgresql.org/docs/current/functions-math.html # SQLite: https://www.sqlite.org/lang_mathfunc.html # MSSQL: https://learn.microsoft.com/en-us/sql/t-sql/functions/mathematical-functions-transact-sql # BigQuery: https://cloud.google.com/bigquery/docs/reference/standard-sql/mathematical_functions # Snowflake: https://docs.snowflake.com/en/sql-reference/functions-math.html let abs = column -> s"ABS({column:0})" let floor = column -> s"FLOOR({column:0})" let ceil = column -> s"CEIL({column:0})" let pi = -> s"PI()" let exp = column -> s"EXP({column:0})" let ln = column -> s"LN({column:0})" let log10 = column -> s"LOG10({column:0})" let log = base column -> s"LOG10({column:0}) / LOG10({base:0})" let sqrt = column -> s"SQRT({column:0})" let degrees = column -> s"DEGREES({column:0})" let radians = column -> s"RADIANS({column:0})" let cos = column -> s"COS({column:0})" let acos = column -> s"ACOS({column:0})" let sin = column -> s"SIN({column:0})" let asin = column -> s"ASIN({column:0})" let tan = column -> s"TAN({column:0})" let atan = column -> s"ATAN({column:0})" # Note exponent goes first, so `pow 2 3` is 2^3 let pow = exponent column -> s"POW({column:0}, {exponent:0})" let round = n_digits column -> s"ROUND({column:0}, {n_digits:0})" } # Other functions let as = `type` column -> s"CAST({column:0} AS {type:0})" # Text functions module text { # Clickhouse: https://clickhouse.com/docs/en/sql-reference/functions/string-functions # DuckDB: https://duckdb.org/docs/sql/functions/char # MariaDB: https://mariadb.com/kb/en/string-functions/ # MySQL: https://dev.mysql.com/doc/refman/8.4/en/string-functions.html # Postgres: https://www.postgresql.org/docs/current/functions-string.html # SQLite: https://www.sqlite.org/lang_corefunc.html # MSSQL: https://learn.microsoft.com/en-us/sql/t-sql/functions/string-functions-transact-sql # BigQuery: https://cloud.google.com/bigquery/docs/reference/standard-sql/string_functions # Snowflake: https://docs.snowflake.com/en/sql-reference/functions-string let lower = column -> s"LOWER({column:0})" let upper = column -> s"UPPER({column:0})" let ltrim = column -> s"LTRIM({column:0})" let rtrim = column -> s"RTRIM({column:0})" let trim = column -> s"TRIM({column:0})" let length = column -> s"CHAR_LENGTH({column:0})" let extract = offset length column -> s"SUBSTRING({column:0}, {offset:0}, {length:0})" let replace = pattern replacement column -> s"REPLACE({column:0}, {pattern:0}, {replacement:0})" let starts_with = prefix column -> s"{column:0} LIKE CONCAT({prefix:0}, '%')" let contains = substr column -> s"{column:0} LIKE CONCAT('%', {substr:0}, '%')" let ends_with = suffix column -> s"{column:0} LIKE CONCAT('%', {suffix:0})" } # Source-reading functions, primarily for DuckDB let read_parquet = binary_as_string file_row_number hive_partitioning union_by_name source -> s"read_parquet({source:0})" let read_csv = source -> s"read_csv({source:0})" let read_json = source -> s"read_json({source:0})" @{binding_strength=11} let mul = l r -> null @{binding_strength=100} let div_i = l r -> s"FLOOR(ABS({l:11} / {r:12})) * SIGN({l:0}) * SIGN({r:0})" # We have a simple float division by default, but it can be overridden by # dialects. # Note that this uses `12` for the RHS binding strength, which is one more than # binding strength of division. That's because we don't use associativity here, # and so we need to make sure that the RHS is parenthesized when the binding # strengths are equal; e.g. in `a / (b / c)`. @{binding_strength=11} let div_f = l r -> s"{l} / {r:12}" @{binding_strength=11} let mod = l r -> s"{l} % {r:12}" @{binding_strength=10} let add = l r -> null @{binding_strength=10} let sub = l r -> null @{binding_strength=6} let eq = l r -> null @{binding_strength=6} let ne = l r -> null @{binding_strength=6} let gt = l r -> null @{binding_strength=6} let lt = l r -> null @{binding_strength=6} let gte = l r -> null @{binding_strength=6} let lte = l r -> null @{binding_strength=3} let and = l r -> null @{binding_strength=2} let or = l r -> null let coalesce = l r -> s"COALESCE({l:0}, {r:0})" let regex_search = text pattern -> s"REGEXP({text:0}, {pattern:0})" @{binding_strength=13} let neg = l -> s"-{l}" @{binding_strength=4} let not = l -> s"NOT {l}" module ansi { @{binding_strength=11} let div_f = l r -> s"({l} * 1.0 / {r:12})" } module bigquery { @{binding_strength=11} let div_f = l r -> s"({l} * 1.0 / {r:12})" # Mathematical functions module math { # BigQuery: https://cloud.google.com/bigquery/docs/reference/standard-sql/mathematical_functions let degrees = column -> s"({column:0} * 180 / PI())" let radians = column -> s"({column:0} * PI() / 180)" } module date { let to_text = format column -> s"FORMAT_TIMESTAMP({format:0}, CAST({column:0} AS TIMESTAMP))" } let regex_search = text pattern -> s"REGEXP_CONTAINS({text:0}, {pattern:0})" } module clickhouse { # https://clickhouse.com/docs/en/sql-reference/functions/arithmetic-functions#divide @{binding_strength=11} let div_f = l r -> s"({l} / {r:12})" @{binding_strength=11} let div_i = l r -> s"({l} DIV {r:12})" # Date functions module date { # https://clickhouse.com/docs/en/sql-reference/functions/date-time-functions let to_text = format column -> s"formatDateTimeInJodaSyntax({column:0}, {format:0})" } let regex_search = text pattern -> s"match({text:0}, {pattern:0})" let read_csv = source -> s"file({source:0}, 'CSV')" let read_json = source -> s"file({source:0}, 'Json')" let read_parquet = source -> s"file({source:0}, 'Parquet')" } module duckdb { @{binding_strength=11} let div_f = l r -> s"({l} / {r:12})" @{binding_strength=11} let div_i = l r -> s"TRUNC({l:11} / {r:12})" # Text functions module text { # DuckDB: https://duckdb.org/docs/sql/functions/char let length = column -> s"LENGTH({column:0})" } # Date functions module date { # https://duckdb.org/docs/sql/functions/dateformat let to_text = format column -> s"strftime({column:0}, {format:0})" } let regex_search = text pattern -> s"REGEXP_MATCHES({text:0}, {pattern:0})" let read_csv = source -> s"read_csv_auto({source:0})" let read_json = source -> s"read_json_auto({source:0})" let read_parquet = binary_as_string file_row_number hive_partitioning union_by_name source -> s"read_parquet({source:0}, binary_as_string={binary_as_string:0}, file_row_number={file_row_number:0}, hive_partitioning={hive_partitioning:0}, union_by_name={union_by_name:0})" } module mssql { @{binding_strength=11} let div_f = l r -> s"({l} * 1.0 / {r:12})" # Mathematical functions module math { # https://learn.microsoft.com/en-us/sql/t-sql/functions/mathematical-functions-transact-sql let ceil = column -> s"CEILING({column:0})" let ln = column -> s"LOG({column:0})" let pow = exponent column -> s"POWER({column:0}, {exponent:0})" } # Text functions module text { # https://learn.microsoft.com/en-us/sql/t-sql/functions/string-functions-transact-sql let length = column -> s"LEN({column:0})" } # Date functions module date { # https://learn.microsoft.com/en-us/sql/t-sql/functions/format-transact-sql let to_text = format column -> s"FORMAT({column:0}, {format:0})" } let regex_search = text pattern -> null } module mysql { @{binding_strength=11} let div_f = l r -> s"({l} / {r:12})" @{binding_strength=11} let div_i = l r -> s"({l} DIV {r:12})" @{binding_strength=100} let mod = l r -> s"ROUND(MOD({l:0}, {r:0}))" # Date functions module date { # https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html let to_text = format column -> s"DATE_FORMAT({column:0}, {format:0})" } # 'c' for case-sensitive let regex_search = text pattern -> s"REGEXP_LIKE({text:0}, {pattern:0}, 'c')" } module postgres { @{binding_strength=11} let div_f = l r -> s"({l} * 1.0 / {r:12})" @{binding_strength=100} let div_i = l r -> s"TRUNC({l:11} / {r:12})" # Mathematical functions module math { # Postgres: https://www.postgresql.org/docs/7.4/functions-math.html @{binding_strength=100} let round = n_digits column -> s"ROUND(({column:0})::numeric, {n_digits:0})" } # Text functions module text { # Postgres: https://www.postgresql.org/docs/7.4/functions-string.html let extract = offset length column -> s"SUBSTR({column:0}, {offset:0}, {length:0})" } # Date functions module date { # https://www.postgresql.org/docs/current/functions-formatting.html let to_text = format column -> s"TO_CHAR({column:0}, {format:0})" } @{binding_strength=9} let regex_search = text pattern -> s"{text} ~ {pattern}" } module redshift { @{binding_strength=11} let div_f = l r -> s"({l} * 1.0 / {r:12})" # Text functions module text { # https://docs.aws.amazon.com/redshift/latest/dg/r_concat_op.html let contains = substr column -> s"{column:0} LIKE '%' || {substr:0} || '%'" } } module glaredb { @{binding_strength=11} let div_f = l r -> s"({l} * 1.0 / {r:12})" @{binding_strength=100} let div_i = l r -> s"TRUNC({l:11} / {r:12})" # Mathematical functions module math { # GlareDB: https://glaredb.com/docs/sql-reference/functions/math-functions @{binding_strength=100} let round = n_digits column -> s"ROUND(({column:0})::numeric, {n_digits:0})" } @{binding_strength=9} let regex_search = text pattern -> s"{text} ~ {pattern}" let read_csv = source -> s"csv_scan({source:0})" let read_parquet = source -> s"parquet_scan({source:0})" } module sqlite { @{window_frame=true, coalesce="TRUE", binding_strength=6} let all = column -> s"MIN({column:0}) > 0" @{window_frame=true, coalesce="FALSE", binding_strength=6} let any = column -> s"MAX({column:0}) > 0" @{window_frame=true, coalesce="''"} let concat_array = column -> s"GROUP_CONCAT({column:0}, '')" @{binding_strength=11} let div_f = l r -> s"({l} * 1.0 / {r:12})" @{binding_strength=100} let div_i = l r -> s"ROUND(ABS({l:11} / {r:12}) - 0.5) * SIGN({l:0}) * SIGN({r:0})" # Text functions module text { # SQLite: https://www.sqlite.org/lang_corefunc.html let length = column -> s"LENGTH({column:0})" let starts_with = prefix column -> s"{column:0} LIKE {prefix:0} || '%'" let contains = substr column -> s"{column:0} LIKE '%' || {substr:0} || '%'" let ends_with = suffix column -> s"{column:0} LIKE '%' || {suffix:0}" } @{binding_strength=9} let regex_search = text pattern -> s"{text} REGEXP {pattern}" } module snowflake { # https://docs.snowflake.com/en/sql-reference/operators-arithmetic#division @{binding_strength=11} let div_f = l r -> s"({l} / {r:12})" # Text functions module text { # https://docs.snowflake.com/en/sql-reference/functions-string let length = column -> s"LENGTH({column:0})" } } ================================================ FILE: prqlc/prqlc/src/utils/id_gen.rs ================================================ use std::marker::PhantomData; use crate::ir::rq::{fold_table, CId, RelationalQuery, RqFold, TId, TableDecl}; use crate::Result; #[derive(Debug, Clone)] pub struct IdGenerator> { next_id: usize, phantom: PhantomData, } impl> IdGenerator { pub fn new() -> Self { Self::default() } fn skip(&mut self, id: usize) { self.next_id = self.next_id.max(id + 1); } pub fn gen(&mut self) -> T { let id = self.next_id; self.next_id += 1; T::from(id) } } impl> Default for IdGenerator { fn default() -> IdGenerator { IdGenerator { next_id: 0, phantom: PhantomData, } } } impl IdGenerator { /// Returns a new id generators capable of generating new ids for given query. pub fn load(query: RelationalQuery) -> (IdGenerator, IdGenerator, RelationalQuery) { let mut loader = IdLoader { cid: IdGenerator::::default(), tid: IdGenerator::::default(), }; let query = loader.fold_query(query).unwrap(); (loader.cid, loader.tid, query) } } struct IdLoader { cid: IdGenerator, tid: IdGenerator, } impl RqFold for IdLoader { fn fold_cid(&mut self, cid: CId) -> Result { self.cid.skip(cid.get()); Ok(cid) } fn fold_table(&mut self, table: TableDecl) -> Result { self.tid.skip(table.id.get()); fold_table(self, table) } } #[derive(Debug, Clone, Default)] pub struct NameGenerator { prefix: &'static str, id: IdGenerator, } impl NameGenerator { pub fn new(prefix: &'static str) -> Self { NameGenerator { prefix, id: IdGenerator::new(), } } pub fn gen(&mut self) -> String { format!("{}{}", self.prefix, self.id.gen()) } } ================================================ FILE: prqlc/prqlc/src/utils/mod.rs ================================================ mod id_gen; mod toposort; use std::{io::stderr, sync::OnceLock}; use anstream::adapter::strip_str; pub use id_gen::{IdGenerator, NameGenerator}; use itertools::Itertools; use regex::Regex; pub use toposort::toposort; use crate::Result; pub trait OrMap { /// Merges two options into one using `f`. /// If one of the options is None, results defaults to the other one. fn or_map(self, b: Self, f: F) -> Self where F: FnOnce(T, T) -> T; } impl OrMap for Option { fn or_map(self, b: Self, f: F) -> Self where F: FnOnce(T, T) -> T, { match (self, b) { (Some(a), Some(b)) => Some(f(a, b)), (a, None) => a, (None, b) => b, } } } pub trait Pluck { fn pluck(&mut self, f: F) -> Vec where F: Fn(T) -> Result; } impl Pluck for Vec { fn pluck(&mut self, f: F) -> Vec where F: Fn(T) -> Result, { let mut matched = Vec::new(); let mut not_matched = Vec::new(); for transform in self.drain(..) { match f(transform) { Ok(t) => matched.push(t), Err(transform) => not_matched.push(transform), } } self.extend(not_matched); matched } } /// Breaks up a [Vec] into two parts at the position of first matching element. /// Matching element is placed into the second part. /// /// Zero clones. pub trait BreakUp { fn break_up(self, f: F) -> (Vec, Vec) where F: FnMut(&T) -> bool; } impl BreakUp for Vec { fn break_up(mut self, f: F) -> (Vec, Vec) where F: FnMut(&T) -> bool, { let position = self.iter().position(f).unwrap_or(self.len()); let after = self.drain(position..).collect_vec(); (self, after) } } pub(crate) fn valid_ident() -> &'static Regex { static VALID_IDENT: OnceLock = OnceLock::new(); VALID_IDENT.get_or_init(|| { // One of: // - `*` // - An ident starting with `a-z_\$` and containing other characters `a-z0-9_\$` // // We could replace this with pomsky (regex<>pomsky : sql<>prql) // ^ ('*' | [ascii_lower '_$'] [ascii_lower ascii_digit '_$']* ) $ Regex::new(r"^((\*)|(^[a-z_\$][a-z0-9_\$]*))$").unwrap() }) } fn should_use_color() -> bool { match anstream::AutoStream::choice(&stderr()) { anstream::ColorChoice::Auto => true, anstream::ColorChoice::Always => true, anstream::ColorChoice::AlwaysAnsi => true, anstream::ColorChoice::Never => false, } } /// Strip colors, for external libraries which don't yet strip themselves, and /// for insta snapshot tests. This will respond to environment variables such as /// `CLI_COLOR`. pub fn maybe_strip_colors(s: &str) -> String { if !should_use_color() { strip_str(s).to_string() } else { s.to_string() } } #[test] fn test_write_ident_part() { assert!(!valid_ident().is_match("")); } ================================================ FILE: prqlc/prqlc/src/utils/toposort.rs ================================================ use std::collections::HashMap; type Dag = Vec>; struct Toposort { nodes: Vec, order: Vec, } #[derive(Clone, Copy)] struct Node { visiting: bool, done: bool, } pub fn toposort<'a, Key: Eq + std::hash::Hash + Clone>( dependencies: &'a [(Key, Vec)], start: Option<&'_ Key>, ) -> Option> { // create mapping from Key to usize let index: HashMap<&Key, usize> = dependencies .iter() .enumerate() .map(|(index, (key, _))| (key, index)) .collect(); // map DAG from Key to usize let dag: Dag = dependencies .iter() .map(|(_, deps)| deps.iter().flat_map(|d| index.get(d).cloned()).collect()) .collect(); // init toposort let empty = Node { visiting: false, done: false, }; let mut toposort = Toposort { nodes: vec![empty; index.len()], order: Vec::with_capacity(index.len()), }; if let Some(start) = start.map(|s| index.get(s).unwrap()) { // use only the provided visit start toposort.visit(&dag, *start).ok()?; } else { // start visits from all nodes while toposort.order.len() < dependencies.len() { for start_at in 0..index.len() { toposort.visit(&dag, start_at).ok()?; } } } // unmap Some(toposort.order.iter().map(|i| &dependencies[*i].0).collect()) } impl Toposort { fn visit(&mut self, dag: &Dag, n: usize) -> Result<(), ()> { let node = self.nodes.get_mut(n).unwrap(); if node.done { return Ok(()); } if node.visiting { return Err(()); } node.visiting = true; for m in &dag[n] { self.visit(dag, *m)?; } let node = self.nodes.get_mut(n).unwrap(); node.visiting = false; node.done = true; self.order.push(n); Ok(()) } } #[cfg(test)] mod tests { use itertools::Itertools; use super::toposort; #[test] fn normal_sort() { let dependencies = vec![ ("a", vec!["b"]), ("b", vec!["c"]), ("c", vec![]), ("d", vec![]), ]; let order = toposort(&dependencies, None).unwrap(); let order = order.into_iter().copied().collect_vec(); assert_eq!(order, vec!["c", "b", "a", "d"]); } #[test] fn normal_sort_2() { let dependencies = vec![ ("a", vec![]), ("b", vec![]), ("c", vec!["b"]), ("d", vec!["c"]), ]; let order = toposort(&dependencies, None).unwrap(); let order = order.into_iter().copied().collect_vec(); assert_eq!(order, vec!["a", "b", "c", "d"]); } #[test] fn dag_with_cycle() { let dependencies = vec![ ("a", vec!["b"]), ("b", vec!["c", "d"]), ("c", vec![]), ("d", vec!["a"]), ]; let order = toposort(&dependencies, None); assert!(order.is_none()); } #[test] fn parallel_when_ambiguous() { let dependencies = vec![ ("a", vec!["b"]), ("b", vec![]), ("c", vec!["b"]), ("d", vec!["b"]), ]; let order = toposort(&dependencies, None).unwrap(); let order = order.into_iter().copied().collect_vec(); assert_eq!(order, vec!["b", "a", "c", "d"]); } #[test] fn with_root() { let dependencies = vec![ ("a", vec!["b"]), ("b", vec![]), ("c", vec!["b"]), ("d", vec!["b"]), ]; let root = "c"; let order = toposort(&dependencies, Some(&root)).unwrap(); let order = order.into_iter().copied().collect_vec(); assert_eq!(order, vec!["b", "c"]); } } ================================================ FILE: prqlc/prqlc/tests/CLAUDE.md ================================================ # Tests ## Structure - **`integration/sql.rs`** — Unit tests for PRQL→SQL compilation. Fast, focused, preferred for most changes. - **`integration/queries/*.prql`** — Integration tests generating 6 snapshots each (different SQL dialects). Use sparingly. - **`integration/error_messages.rs`** — Tests for compiler error diagnostics. - **`integration/dbs/`** — Database runners for executing queries against real databases. ## Guidelines **80% unit tests, 20% integration tests.** Integration tests in `integration/queries/` are verbose — each `.prql` file generates six snapshot files. Prefer unit tests in [`integration/sql.rs`](./integration/sql.rs): ```rust #[test] fn test_my_feature() { assert_snapshot!(compile(r#" from foo select bar "#).unwrap(), @"..."); } ``` New integration test files are appropriate when validating against real databases or testing behavior across compilation stages. Extend existing tests when possible. ================================================ FILE: prqlc/prqlc/tests/integration/bad_error_messages.rs ================================================ //! Record bad error messages here which we should improve. //! //! Some of these will be good issues for new contributors, or for more //! experienced contributors who would like a quick issue to fix: //! - Find where the error is being raised now, generally just search for a part //! of the message. //! - Add `` macros to the code to see what's going on. //! - Write a better message / find a better place to raise a message. //! - Run `cargo insta test --accept`, and move the test out of this file into //! `test_error_messages.rs`. If it's only partially solved, add a TODO and //! make a call for where it should go. //! //! Adding bad error messages here is also a welcome contribution. Probably //! one-issue-per-error-message is not a good way of managing them — there would //! be a huge number of issues, and it would be difficult to see what's current. //! So instead, add the error message as a test here. use insta::assert_snapshot; use super::sql::compile; #[test] fn test_bad_error_messages() { assert_snapshot!(compile(r###" from film group "###).unwrap_err(), @r" Error: ╭─[ :3:5 ] │ 3 │ group │ ──┬── │ ╰──── main expected type `relation`, but found type `func transform relation -> relation` │ │ Help: Argument might be missing to function std.group? │ │ Note: Type `relation` expands to `[{..}]` ───╯ "); // This should suggest parentheses (this might not be an easy one to solve) assert_snapshot!(compile(r#" let f = country -> country == "Canada" from employees filter f location "#).unwrap_err(), @r" Error: ╭─[ :5:14 ] │ 5 │ filter f location │ ────┬─── │ ╰───── Unknown name `location` ───╯ "); // Really complicated error message for something so fundamental assert_snapshot!(compile(r###" select tracks from artists "###).unwrap_err(), @r" Error: ╭─[ :3:5 ] │ 3 │ from artists │ ──────┬───── │ ╰─────── expected a function, but found `default_db.artists` ───╯ "); } #[test] fn interpolation_end() { use insta::assert_debug_snapshot; // This test demonstrates error reporting for an unclosed f-string: `f"{}` (no closing quote). // The input ends at position 20 after the `}`, so the closing quote is missing at position 20. // // The lexer correctly reports the error at position 20 (end of input) with `found: ""`. // The parser reports the error at position 21 (character position in line 1) with "unexpected". let source = r#"from x | select f"{}"#; // LEXER output (for comparison with parser output below): assert_debug_snapshot!(prqlc_parser::lexer::lex_source(source).unwrap_err(), @r#" [ Error { kind: Error, span: Some( 0:20-20, ), reason: Unexpected { found: "end of input", }, hints: [], code: None, }, ] "#); // PARSER output (full compilation error): assert_snapshot!(compile(source).unwrap_err(), @r#" Error: ╭─[ :1:21 ] │ 1 │ from x | select f"{} │ │ │ ╰─ unexpected end of input ───╯ "#); } #[test] fn select_with_extra_fstr() { // Should complain in the same way as `select lower "mooo"` assert_snapshot!(compile(r#" from foo select lower f"{x}/{y}" "#).unwrap_err(), @r#" Error: ╭─[ :3:21 ] │ 3 │ select lower f"{x}/{y}" │ ┬ │ ╰── Unknown name `x` ───╯ "#); } // See also test_error_messages::test_type_error_placement #[test] fn misplaced_type_error() { // This one should point at `foo` in `select (... foo)` // (preferably in addition to the error that is currently generated) assert_snapshot!(compile(r###" let foo = 123 from t select (true && foo) "###).unwrap_err(), @r" Error: ╭─[ :2:15 ] │ 2 │ let foo = 123 │ ─┬─ │ ╰─── function std.and, param `right` expected type `bool`, but found type `int` ───╯ "); } #[test] fn test_hint_missing_args() { assert_snapshot!(compile(r###" from film select {film_id, lag film_id} "###).unwrap_err(), @r" Error: ╭─[ :3:22 ] │ 3 │ select {film_id, lag film_id} │ ─────┬───── │ ╰─────── unexpected `(func offset column -> internal std.lag) film_id` │ │ Help: this is probably a 'bad type' error (we are working on that) ───╯ ") } #[test] fn test_relation_literal_contains_literals() { assert_snapshot!(compile(r###" [{a=(1+1)}] "###).unwrap_err(), @r" Error: ╭─[ :2:9 ] │ 2 │ [{a=(1+1)}] │ ──┬── │ ╰──── relation literal expected literals, but found ``(std.add ...)`` ───╯ ") } #[test] fn nested_groups() { // Nested `group` gives a very abstract & internally-focused error message assert_snapshot!(compile(r###" from invoices select {inv = this} join item = invoice_items (==invoice_id) group { inv.billing_city } ( group { item.name } ( aggregate { ct1 = count inv.name, } ) ) "###).unwrap_err(), @r" Error: ╭─[ :9:9 ] │ 9 │ ╭─▶ aggregate { ┆ ┆ 11 │ ├─▶ } │ │ │ ╰─────────────── internal compiler error; tracked at https://github.com/PRQL/prql/issues/3870 ────╯ "); } #[test] fn just_std() { assert_snapshot!(compile(r###" std "###).unwrap_err(), @r" Error: ╭─[ :1:1 ] │ 1 │ ╭─▶ 2 │ ├─▶ std │ │ │ ╰───────────── internal compiler error; tracked at https://github.com/PRQL/prql/issues/4474 ───╯ "); } ================================================ FILE: prqlc/prqlc/tests/integration/data/chinook/albums.csv ================================================ album_id,title,artist_id 1,For Those About To Rock We Salute You,1 2,Balls to the Wall,2 3,Restless and Wild,2 4,Let There Be Rock,1 5,Big Ones,3 6,Jagged Little Pill,4 7,Facelift,5 8,Warner 25 Anos,6 9,Plays Metallica By Four Cellos,7 10,Audioslave,8 11,Out Of Exile,8 12,BackBeat Soundtrack,9 13,The Best Of Billy Cobham,10 14,Alcohol Fueled Brewtality Live! [Disc 1],11 15,Alcohol Fueled Brewtality Live! [Disc 2],11 16,Black Sabbath,12 17,Black Sabbath Vol. 4 (Remaster),12 18,Body Count,13 19,Chemical Wedding,14 20,The Best Of Buddy Guy - The Millenium Collection,15 21,Prenda Minha,16 22,Sozinho Remix Ao Vivo,16 23,Minha Historia,17 24,Afrociberdelia,18 25,Da Lama Ao Caos,18 26,Acústico MTV [Live],19 27,Cidade Negra - Hits,19 28,Na Pista,20 29,Axé Bahia 2001,21 30,BBC Sessions [Disc 1] [Live],22 31,Bongo Fury,23 32,Carnaval 2001,21 33,Chill: Brazil (Disc 1),24 34,Chill: Brazil (Disc 2),6 35,Garage Inc. (Disc 1),50 36,Greatest Hits II,51 37,Greatest Kiss,52 38,Heart of the Night,53 39,International Superhits,54 40,Into The Light,55 41,Meus Momentos,56 42,Minha História,57 43,MK III The Final Concerts [Disc 1],58 44,Physical Graffiti [Disc 1],22 45,Sambas De Enredo 2001,21 46,Supernatural,59 47,The Best of Ed Motta,37 48,The Essential Miles Davis [Disc 1],68 49,The Essential Miles Davis [Disc 2],68 50,The Final Concerts (Disc 2),58 51,Up An' Atom,69 52,Vinícius De Moraes - Sem Limite,70 53,Vozes do MPB,21 54,"Chronicle, Vol. 1",76 55,"Chronicle, Vol. 2",76 56,Cássia Eller - Coleção Sem Limite [Disc 2],77 57,Cássia Eller - Sem Limite [Disc 1],77 58,Come Taste The Band,58 59,Deep Purple In Rock,58 60,Fireball,58 61,Knocking at Your Back Door: The Best Of Deep Purple in the 80's,58 62,Machine Head,58 63,Purpendicular,58 64,Slaves And Masters,58 65,Stormbringer,58 66,The Battle Rages On,58 67,Vault: Def Leppard's Greatest Hits,78 68,Outbreak,79 69,Djavan Ao Vivo - Vol. 02,80 70,Djavan Ao Vivo - Vol. 1,80 71,Elis Regina-Minha História,41 72,The Cream Of Clapton,81 73,Unplugged,81 74,Album Of The Year,82 75,Angel Dust,82 76,King For A Day Fool For A Lifetime,82 77,The Real Thing,82 78,Deixa Entrar,83 79,In Your Honor [Disc 1],84 80,In Your Honor [Disc 2],84 81,One By One,84 82,The Colour And The Shape,84 83,My Way: The Best Of Frank Sinatra [Disc 1],85 84,Roda De Funk,86 85,As Canções de Eu Tu Eles,27 86,Quanta Gente Veio Ver (Live),27 87,Quanta Gente Veio ver--Bônus De Carnaval,27 88,Faceless,87 89,American Idiot,54 90,Appetite for Destruction,88 91,Use Your Illusion I,88 92,Use Your Illusion II,88 93,Blue Moods,89 94,A Matter of Life and Death,90 95,A Real Dead One,90 96,A Real Live One,90 97,Brave New World,90 98,Dance Of Death,90 99,Fear Of The Dark,90 100,Iron Maiden,90 101,Killers,90 102,Live After Death,90 103,Live At Donington 1992 (Disc 1),90 104,Live At Donington 1992 (Disc 2),90 105,No Prayer For The Dying,90 106,Piece Of Mind,90 107,Powerslave,90 108,Rock In Rio [CD1],90 109,Rock In Rio [CD2],90 110,Seventh Son of a Seventh Son,90 111,Somewhere in Time,90 112,The Number of The Beast,90 113,The X Factor,90 114,Virtual XI,90 115,Sex Machine,91 116,Emergency On Planet Earth,92 117,Synkronized,92 118,The Return Of The Space Cowboy,92 119,Get Born,93 120,Are You Experienced?,94 121,Surfing with the Alien (Remastered),95 122,Jorge Ben Jor 25 Anos,46 123,Jota Quest-1995,96 124,Cafezinho,97 125,Living After Midnight,98 126,Unplugged [Live],52 127,BBC Sessions [Disc 2] [Live],22 128,Coda,22 129,Houses Of The Holy,22 130,In Through The Out Door,22 131,IV,22 132,Led Zeppelin I,22 133,Led Zeppelin II,22 134,Led Zeppelin III,22 135,Physical Graffiti [Disc 2],22 136,Presence,22 137,The Song Remains The Same (Disc 1),22 138,The Song Remains The Same (Disc 2),22 139,A TempestadeTempestade Ou O Livro Dos Dias,99 140,Mais Do Mesmo,99 141,Greatest Hits,100 142,Lulu Santos - RCA 100 Anos De Música - Álbum 01,101 143,Lulu Santos - RCA 100 Anos De Música - Álbum 02,101 144,Misplaced Childhood,102 145,Barulhinho Bom,103 146,Seek And Shall Find: More Of The Best (1963-1981),104 147,The Best Of Men At Work,105 148,Black Album,50 149,Garage Inc. (Disc 2),50 150,Kill 'Em All,50 151,Load,50 152,Master Of Puppets,50 153,ReLoad,50 154,Ride The Lightning,50 155,St. Anger,50 156,...And Justice For All,50 157,Miles Ahead,68 158,Milton Nascimento Ao Vivo,42 159,Minas,42 160,Ace Of Spades,106 161,Demorou...,108 162,Motley Crue Greatest Hits,109 163,From The Muddy Banks Of The Wishkah [Live],110 164,Nevermind,110 165,Compositores,111 166,Olodum,112 167,Acústico MTV,113 168,Arquivo II,113 169,Arquivo Os Paralamas Do Sucesso,113 170,Bark at the Moon (Remastered),114 171,Blizzard of Ozz,114 172,Diary of a Madman (Remastered),114 173,No More Tears (Remastered),114 174,Tribute,114 175,Walking Into Clarksdale,115 176,Original Soundtracks 1,116 177,The Beast Live,117 178,Live On Two Legs [Live],118 179,Pearl Jam,118 180,Riot Act,118 181,Ten,118 182,Vs.,118 183,Dark Side Of The Moon,120 184,Os Cães Ladram Mas A Caravana Não Pára,121 185,Greatest Hits I,51 186,News Of The World,51 187,Out Of Time,122 188,Green,124 189,New Adventures In Hi-Fi,124 190,The Best Of R.E.M.: The IRS Years,124 191,Cesta Básica,125 192,Raul Seixas,126 193,Blood Sugar Sex Magik,127 194,By The Way,127 195,Californication,127 196,Retrospective I (1974-1980),128 197,Santana - As Years Go By,59 198,Santana Live,59 199,Maquinarama,130 200,O Samba Poconé,130 201,Judas 0: B-Sides and Rarities,131 202,Rotten Apples: Greatest Hits,131 203,A-Sides,132 204,Morning Dance,53 205,In Step,133 206,Core,134 207,Mezmerize,135 208,[1997] Black Light Syndrome,136 209,Live [Disc 1],137 210,Live [Disc 2],137 211,The Singles,138 212,Beyond Good And Evil,139 213,"Pure Cult: The Best Of The Cult (For Rockers, Ravers, Lovers & Sinners) [UK]",139 214,The Doors,140 215,The Police Greatest Hits,141 216,"Hot Rocks, 1964-1971 (Disc 1)",142 217,No Security,142 218,Voodoo Lounge,142 219,Tangents,143 220,Transmission,143 221,My Generation - The Very Best Of The Who,144 222,Serie Sem Limite (Disc 1),145 223,Serie Sem Limite (Disc 2),145 224,Acústico,146 225,Volume Dois,146 226,Battlestar Galactica: The Story So Far,147 227,"Battlestar Galactica, Season 3",147 228,"Heroes, Season 1",148 229,"Lost, Season 3",149 230,"Lost, Season 1",149 231,"Lost, Season 2",149 232,Achtung Baby,150 233,All That You Can't Leave Behind,150 234,B-Sides 1980-1990,150 235,How To Dismantle An Atomic Bomb,150 236,Pop,150 237,Rattle And Hum,150 238,The Best Of 1980-1990,150 239,War,150 240,Zooropa,150 241,UB40 The Best Of - Volume Two [UK],151 242,Diver Down,152 243,"The Best Of Van Halen, Vol. I",152 244,Van Halen,152 245,Van Halen III,152 246,Contraband,153 247,Vinicius De Moraes,72 248,Ao Vivo [IMPORT],155 249,"The Office, Season 1",156 250,"The Office, Season 2",156 251,"The Office, Season 3",156 252,Un-Led-Ed,157 253,"Battlestar Galactica (Classic), Season 1",158 254,Aquaman,159 255,Instant Karma: The Amnesty International Campaign to Save Darfur,150 256,Speak of the Devil,114 257,20th Century Masters - The Millennium Collection: The Best of Scorpions,179 258,House of Pain,180 259,Radio Brasil (O Som da Jovem Vanguarda) - Seleccao de Henrique Amaro,36 260,Cake: B-Sides and Rarities,196 261,"LOST, Season 4",149 262,Quiet Songs,197 263,Muso Ko,198 264,Realize,199 265,Every Kind of Light,200 266,Duos II,201 267,Worlds,202 268,The Best of Beethoven,203 269,Temple of the Dog,204 270,Carry On,205 271,Revelations,8 272,Adorate Deum: Gregorian Chant from the Proper of the Mass,206 273,Allegri: Miserere,207 274,Pachelbel: Canon & Gigue,208 275,Vivaldi: The Four Seasons,209 276,Bach: Violin Concertos,210 277,Bach: Goldberg Variations,211 278,Bach: The Cello Suites,212 279,Handel: The Messiah (Highlights),213 280,The World of Classical Favourites,214 281,Sir Neville Marriner: A Celebration,215 282,Mozart: Wind Concertos,216 283,Haydn: Symphonies 99 - 104,217 284,Beethoven: Symhonies Nos. 5 & 6,218 285,A Soprano Inspired,219 286,Great Opera Choruses,220 287,Wagner: Favourite Overtures,221 288,"Fauré: Requiem, Ravel: Pavane & Others",222 289,Tchaikovsky: The Nutcracker,223 290,The Last Night of the Proms,224 291,Puccini: Madama Butterfly - Highlights,225 292,"Holst: The Planets, Op. 32 & Vaughan Williams: Fantasies",226 293,Pavarotti's Opera Made Easy,227 294,Great Performances - Barber's Adagio and Other Romantic Favorites for Strings,228 295,Carmina Burana,229 296,"A Copland Celebration, Vol. I",230 297,Bach: Toccata & Fugue in D Minor,231 298,Prokofiev: Symphony No.1,232 299,Scheherazade,233 300,Bach: The Brandenburg Concertos,234 301,Chopin: Piano Concertos Nos. 1 & 2,235 302,Mascagni: Cavalleria Rusticana,236 303,Sibelius: Finlandia,237 304,Beethoven Piano Sonatas: Moonlight & Pastorale,238 305,Great Recordings of the Century - Mahler: Das Lied von der Erde,240 306,Elgar: Cello Concerto & Vaughan Williams: Fantasias,241 307,"Adams, John: The Chairman Dances",242 308,"Tchaikovsky: 1812 Festival Overture, Op.49, Capriccio Italien & Beethoven: Wellington's Victory",243 309,Palestrina: Missa Papae Marcelli & Allegri: Miserere,244 310,Prokofiev: Romeo & Juliet,245 311,Strauss: Waltzes,226 312,Berlioz: Symphonie Fantastique,245 313,Bizet: Carmen Highlights,246 314,English Renaissance,247 315,Handel: Music for the Royal Fireworks (Original Version 1749),208 316,Grieg: Peer Gynt Suites & Sibelius: Pelléas et Mélisande,248 317,Mozart Gala: Famous Arias,249 318,SCRIABIN: Vers la flamme,250 319,Armada: Music from the Courts of England and Spain,251 320,Mozart: Symphonies Nos. 40 & 41,248 321,Back to Black,252 322,Frank,252 323,Carried to Dust (Bonus Track Version),253 324,Beethoven: Symphony No. 6 'Pastoral' Etc.,254 325,Bartok: Violin & Viola Concertos,255 326,Mendelssohn: A Midsummer Night's Dream,256 327,Bach: Orchestral Suites Nos. 1 - 4,257 328,"Charpentier: Divertissements, Airs & Concerts",258 329,South American Getaway,259 330,Górecki: Symphony No. 3,260 331,Purcell: The Fairy Queen,261 332,The Ultimate Relexation Album,262 333,Purcell: Music for the Queen Mary,263 334,Weill: The Seven Deadly Sins,264 335,"J.S. Bach: Chaconne, Suite in E Minor, Partita in E Major & Prelude, Fugue and Allegro",265 336,Prokofiev: Symphony No.5 & Stravinksy: Le Sacre Du Printemps,248 337,"Szymanowski: Piano Works, Vol. 1",266 338,Nielsen: The Six Symphonies,267 339,Great Recordings of the Century: Paganini's 24 Caprices,268 340,Liszt - 12 Études D'Execution Transcendante,269 341,"Great Recordings of the Century - Shubert: Schwanengesang, 4 Lieder",270 342,"Locatelli: Concertos for Violin, Strings and Continuo, Vol. 3",271 343,Respighi:Pines of Rome,226 344,Schubert: The Late String Quartets & String Quintet (3 CD's),272 345,Monteverdi: L'Orfeo,273 346,Mozart: Chamber Music,274 347,Koyaanisqatsi (Soundtrack from the Motion Picture),275 ================================================ FILE: prqlc/prqlc/tests/integration/data/chinook/artists.csv ================================================ artist_id,name 1,AC/DC 2,Accept 3,Aerosmith 4,Alanis Morissette 5,Alice In Chains 6,Antônio Carlos Jobim 7,Apocalyptica 8,Audioslave 9,BackBeat 10,Billy Cobham 11,Black Label Society 12,Black Sabbath 13,Body Count 14,Bruce Dickinson 15,Buddy Guy 16,Caetano Veloso 17,Chico Buarque 18,Chico Science & Nação Zumbi 19,Cidade Negra 20,Cláudio Zoli 21,Various Artists 22,Led Zeppelin 23,Frank Zappa & Captain Beefheart 24,Marcos Valle 25,Milton Nascimento & Bebeto 26,Azymuth 27,Gilberto Gil 28,João Gilberto 29,Bebel Gilberto 30,Jorge Vercilo 31,Baby Consuelo 32,Ney Matogrosso 33,Luiz Melodia 34,Nando Reis 35,Pedro Luís & A Parede 36,O Rappa 37,Ed Motta 38,Banda Black Rio 39,Fernanda Porto 40,Os Cariocas 41,Elis Regina 42,Milton Nascimento 43,A Cor Do Som 44,Kid Abelha 45,Sandra De Sá 46,Jorge Ben 47,Hermeto Pascoal 48,Barão Vermelho 49,"Edson, DJ Marky & DJ Patife Featuring Fernanda Porto" 50,Metallica 51,Queen 52,Kiss 53,Spyro Gyra 54,Green Day 55,David Coverdale 56,Gonzaguinha 57,Os Mutantes 58,Deep Purple 59,Santana 60,Santana Feat. Dave Matthews 61,Santana Feat. Everlast 62,Santana Feat. Rob Thomas 63,Santana Feat. Lauryn Hill & Cee-Lo 64,Santana Feat. The Project G&B 65,Santana Feat. Maná 66,Santana Feat. Eagle-Eye Cherry 67,Santana Feat. Eric Clapton 68,Miles Davis 69,Gene Krupa 70,Toquinho & Vinícius 71,Vinícius De Moraes & Baden Powell 72,Vinícius De Moraes 73,Vinícius E Qurteto Em Cy 74,Vinícius E Odette Lara 75,"Vinicius, Toquinho & Quarteto Em Cy" 76,Creedence Clearwater Revival 77,Cássia Eller 78,Def Leppard 79,Dennis Chambers 80,Djavan 81,Eric Clapton 82,Faith No More 83,Falamansa 84,Foo Fighters 85,Frank Sinatra 86,Funk Como Le Gusta 87,Godsmack 88,Guns N' Roses 89,Incognito 90,Iron Maiden 91,James Brown 92,Jamiroquai 93,JET 94,Jimi Hendrix 95,Joe Satriani 96,Jota Quest 97,João Suplicy 98,Judas Priest 99,Legião Urbana 100,Lenny Kravitz 101,Lulu Santos 102,Marillion 103,Marisa Monte 104,Marvin Gaye 105,Men At Work 106,Motörhead 107,Motörhead & Girlschool 108,Mônica Marianno 109,Mötley Crüe 110,Nirvana 111,O Terço 112,Olodum 113,Os Paralamas Do Sucesso 114,Ozzy Osbourne 115,Page & Plant 116,Passengers 117,Paul D'Ianno 118,Pearl Jam 119,Peter Tosh 120,Pink Floyd 121,Planet Hemp 122,R.E.M. Feat. Kate Pearson 123,R.E.M. Feat. KRS-One 124,R.E.M. 125,Raimundos 126,Raul Seixas 127,Red Hot Chili Peppers 128,Rush 129,Simply Red 130,Skank 131,Smashing Pumpkins 132,Soundgarden 133,Stevie Ray Vaughan & Double Trouble 134,Stone Temple Pilots 135,System Of A Down 136,"Terry Bozzio, Tony Levin & Steve Stevens" 137,The Black Crowes 138,The Clash 139,The Cult 140,The Doors 141,The Police 142,The Rolling Stones 143,The Tea Party 144,The Who 145,Tim Maia 146,Titãs 147,Battlestar Galactica 148,Heroes 149,Lost 150,U2 151,UB40 152,Van Halen 153,Velvet Revolver 154,Whitesnake 155,Zeca Pagodinho 156,The Office 157,Dread Zeppelin 158,Battlestar Galactica (Classic) 159,Aquaman 160,Christina Aguilera featuring BigElf 161,Aerosmith & Sierra Leone's Refugee Allstars 162,Los Lonely Boys 163,Corinne Bailey Rae 164,Dhani Harrison & Jakob Dylan 165,Jackson Browne 166,Avril Lavigne 167,Big & Rich 168,Youssou N'Dour 169,Black Eyed Peas 170,Jack Johnson 171,Ben Harper 172,Snow Patrol 173,Matisyahu 174,The Postal Service 175,Jaguares 176,The Flaming Lips 177,Jack's Mannequin & Mick Fleetwood 178,Regina Spektor 179,Scorpions 180,House Of Pain 181,Xis 182,Nega Gizza 183,Gustavo & Andres Veiga & Salazar 184,Rodox 185,Charlie Brown Jr. 186,Pedro Luís E A Parede 187,Los Hermanos 188,Mundo Livre S/A 189,Otto 190,Instituto 191,Nação Zumbi 192,DJ Dolores & Orchestra Santa Massa 193,Seu Jorge 194,Sabotage E Instituto 195,Stereo Maracana 196,Cake 197,Aisha Duo 198,Habib Koité and Bamada 199,Karsh Kale 200,The Posies 201,Luciana Souza/Romero Lubambo 202,Aaron Goldberg 203,Nicolaus Esterhazy Sinfonia 204,Temple of the Dog 205,Chris Cornell 206,Alberto Turco & Nova Schola Gregoriana 207,"Richard Marlow & The Choir of Trinity College, Cambridge" 208,English Concert & Trevor Pinnock 209,"Anne-Sophie Mutter, Herbert Von Karajan & Wiener Philharmoniker" 210,"Hilary Hahn, Jeffrey Kahane, Los Angeles Chamber Orchestra & Margaret Batjer" 211,Wilhelm Kempff 212,Yo-Yo Ma 213,Scholars Baroque Ensemble 214,Academy of St. Martin in the Fields & Sir Neville Marriner 215,Academy of St. Martin in the Fields Chamber Ensemble & Sir Neville Marriner 216,"Berliner Philharmoniker, Claudio Abbado & Sabine Meyer" 217,Royal Philharmonic Orchestra & Sir Thomas Beecham 218,Orchestre Révolutionnaire et Romantique & John Eliot Gardiner 219,"Britten Sinfonia, Ivor Bolton & Lesley Garrett" 220,"Chicago Symphony Chorus, Chicago Symphony Orchestra & Sir Georg Solti" 221,Sir Georg Solti & Wiener Philharmoniker 222,"Academy of St. Martin in the Fields, John Birch, Sir Neville Marriner & Sylvia McNair" 223,London Symphony Orchestra & Sir Charles Mackerras 224,Barry Wordsworth & BBC Concert Orchestra 225,"Herbert Von Karajan, Mirella Freni & Wiener Philharmoniker" 226,Eugene Ormandy 227,Luciano Pavarotti 228,Leonard Bernstein & New York Philharmonic 229,Boston Symphony Orchestra & Seiji Ozawa 230,Aaron Copland & London Symphony Orchestra 231,Ton Koopman 232,Sergei Prokofiev & Yuri Temirkanov 233,Chicago Symphony Orchestra & Fritz Reiner 234,Orchestra of The Age of Enlightenment 235,"Emanuel Ax, Eugene Ormandy & Philadelphia Orchestra" 236,James Levine 237,Berliner Philharmoniker & Hans Rosbaud 238,Maurizio Pollini 239,"Academy of St. Martin in the Fields, Sir Neville Marriner & William Bennett" 240,Gustav Mahler 241,"Felix Schmidt, London Symphony Orchestra & Rafael Frühbeck de Burgos" 242,Edo de Waart & San Francisco Symphony 243,Antal Doráti & London Symphony Orchestra 244,Choir Of Westminster Abbey & Simon Preston 245,Michael Tilson Thomas & San Francisco Symphony 246,"Chor der Wiener Staatsoper, Herbert Von Karajan & Wiener Philharmoniker" 247,The King's Singers 248,Berliner Philharmoniker & Herbert Von Karajan 249,"Sir Georg Solti, Sumi Jo & Wiener Philharmoniker" 250,Christopher O'Riley 251,Fretwork 252,Amy Winehouse 253,Calexico 254,Otto Klemperer & Philharmonia Orchestra 255,Yehudi Menuhin 256,Philharmonia Orchestra & Sir Neville Marriner 257,"Academy of St. Martin in the Fields, Sir Neville Marriner & Thurston Dart" 258,Les Arts Florissants & William Christie 259,The 12 Cellists of The Berlin Philharmonic 260,Adrian Leaper & Doreen de Feis 261,"Roger Norrington, London Classical Players" 262,Charles Dutoit & L'Orchestre Symphonique de Montréal 263,"Equale Brass Ensemble, John Eliot Gardiner & Munich Monteverdi Orchestra and Choir" 264,Kent Nagano and Orchestre de l'Opéra de Lyon 265,Julian Bream 266,Martin Roscoe 267,Göteborgs Symfoniker & Neeme Järvi 268,Itzhak Perlman 269,Michele Campanella 270,Gerald Moore 271,"Mela Tenenbaum, Pro Musica Prague & Richard Kapp" 272,Emerson String Quartet 273,"C. Monteverdi, Nigel Rogers - Chiaroscuro; London Baroque; London Cornett & Sackbu" 274,Nash Ensemble 275,Philip Glass Ensemble ================================================ FILE: prqlc/prqlc/tests/integration/data/chinook/customers.csv ================================================ customer_id,first_name,last_name,company,address,city,state,country,postal_code,phone,fax,email,support_rep_id 1,Luís,Gonçalves,Embraer - Empresa Brasileira de Aeronáutica S.A.,"Av. Brigadeiro Faria Lima, 2170",São José dos Campos,SP,Brazil,12227-000,+55 (12) 3923-5555,+55 (12) 3923-5566,luisg@embraer.com.br,3 2,Leonie,Köhler,,Theodor-Heuss-Straße 34,Stuttgart,,Germany,70174,+49 0711 2842222,,leonekohler@surfeu.de,5 3,François,Tremblay,,1498 rue Bélanger,Montréal,QC,Canada,H2G 1A7,+1 (514) 721-4711,,ftremblay@gmail.com,3 4,Bjørn,Hansen,,Ullevålsveien 14,Oslo,,Norway,0171,+47 22 44 22 22,,bjorn.hansen@yahoo.no,4 5,František,Wichterlová,JetBrains s.r.o.,Klanova 9/506,Prague,,Czech Republic,14700,+420 2 4172 5555,+420 2 4172 5555,frantisekw@jetbrains.com,4 6,Helena,Holý,,Rilská 3174/6,Prague,,Czech Republic,14300,+420 2 4177 0449,,hholy@gmail.com,5 7,Astrid,Gruber,,"Rotenturmstraße 4, 1010 Innere Stadt",Vienne,,Austria,1010,+43 01 5134505,,astrid.gruber@apple.at,5 8,Daan,Peeters,,Grétrystraat 63,Brussels,,Belgium,1000,+32 02 219 03 03,,daan_peeters@apple.be,4 9,Kara,Nielsen,,Sønder Boulevard 51,Copenhagen,,Denmark,1720,+453 3331 9991,,kara.nielsen@jubii.dk,4 10,Eduardo,Martins,Woodstock Discos,"Rua Dr. Falcão Filho, 155",São Paulo,SP,Brazil,01007-010,+55 (11) 3033-5446,+55 (11) 3033-4564,eduardo@woodstock.com.br,4 11,Alexandre,Rocha,Banco do Brasil S.A.,"Av. Paulista, 2022",São Paulo,SP,Brazil,01310-200,+55 (11) 3055-3278,+55 (11) 3055-8131,alero@uol.com.br,5 12,Roberto,Almeida,Riotur,"Praça Pio X, 119",Rio de Janeiro,RJ,Brazil,20040-020,+55 (21) 2271-7000,+55 (21) 2271-7070,roberto.almeida@riotur.gov.br,3 13,Fernanda,Ramos,,Qe 7 Bloco G,Brasília,DF,Brazil,71020-677,+55 (61) 3363-5547,+55 (61) 3363-7855,fernadaramos4@uol.com.br,4 14,Mark,Philips,Telus,8210 111 ST NW,Edmonton,AB,Canada,T6G 2C7,+1 (780) 434-4554,+1 (780) 434-5565,mphilips12@shaw.ca,5 15,Jennifer,Peterson,Rogers Canada,700 W Pender Street,Vancouver,BC,Canada,V6C 1G8,+1 (604) 688-2255,+1 (604) 688-8756,jenniferp@rogers.ca,3 16,Frank,Harris,Google Inc.,1600 Amphitheatre Parkway,Mountain View,CA,USA,94043-1351,+1 (650) 253-0000,+1 (650) 253-0000,fharris@google.com,4 17,Jack,Smith,Microsoft Corporation,1 Microsoft Way,Redmond,WA,USA,98052-8300,+1 (425) 882-8080,+1 (425) 882-8081,jacksmith@microsoft.com,5 18,Michelle,Brooks,,627 Broadway,New York,NY,USA,10012-2612,+1 (212) 221-3546,+1 (212) 221-4679,michelleb@aol.com,3 19,Tim,Goyer,Apple Inc.,1 Infinite Loop,Cupertino,CA,USA,95014,+1 (408) 996-1010,+1 (408) 996-1011,tgoyer@apple.com,3 20,Dan,Miller,,541 Del Medio Avenue,Mountain View,CA,USA,94040-111,+1 (650) 644-3358,,dmiller@comcast.com,4 21,Kathy,Chase,,801 W 4th Street,Reno,NV,USA,89503,+1 (775) 223-7665,,kachase@hotmail.com,5 22,Heather,Leacock,,120 S Orange Ave,Orlando,FL,USA,32801,+1 (407) 999-7788,,hleacock@gmail.com,4 23,John,Gordon,,69 Salem Street,Boston,MA,USA,2113,+1 (617) 522-1333,,johngordon22@yahoo.com,4 24,Frank,Ralston,,162 E Superior Street,Chicago,IL,USA,60611,+1 (312) 332-3232,,fralston@gmail.com,3 25,Victor,Stevens,,319 N. Frances Street,Madison,WI,USA,53703,+1 (608) 257-0597,,vstevens@yahoo.com,5 26,Richard,Cunningham,,2211 W Berry Street,Fort Worth,TX,USA,76110,+1 (817) 924-7272,,ricunningham@hotmail.com,4 27,Patrick,Gray,,1033 N Park Ave,Tucson,AZ,USA,85719,+1 (520) 622-4200,,patrick.gray@aol.com,4 28,Julia,Barnett,,302 S 700 E,Salt Lake City,UT,USA,84102,+1 (801) 531-7272,,jubarnett@gmail.com,5 29,Robert,Brown,,796 Dundas Street West,Toronto,ON,Canada,M6J 1V1,+1 (416) 363-8888,,robbrown@shaw.ca,3 30,Edward,Francis,,230 Elgin Street,Ottawa,ON,Canada,K2P 1L7,+1 (613) 234-3322,,edfrancis@yachoo.ca,3 31,Martha,Silk,,194A Chain Lake Drive,Halifax,NS,Canada,B3S 1C5,+1 (902) 450-0450,,marthasilk@gmail.com,5 32,Aaron,Mitchell,,696 Osborne Street,Winnipeg,MB,Canada,R3L 2B9,+1 (204) 452-6452,,aaronmitchell@yahoo.ca,4 33,Ellie,Sullivan,,5112 48 Street,Yellowknife,NT,Canada,X1A 1N6,+1 (867) 920-2233,,ellie.sullivan@shaw.ca,3 34,João,Fernandes,,Rua da Assunção 53,Lisbon,,Portugal,,+351 (213) 466-111,,jfernandes@yahoo.pt,4 35,Madalena,Sampaio,,"Rua dos Campeões Europeus de Viena, 4350",Porto,,Portugal,,+351 (225) 022-448,,masampaio@sapo.pt,4 36,Hannah,Schneider,,Tauentzienstraße 8,Berlin,,Germany,10789,+49 030 26550280,,hannah.schneider@yahoo.de,5 37,Fynn,Zimmermann,,Berger Straße 10,Frankfurt,,Germany,60316,+49 069 40598889,,fzimmermann@yahoo.de,3 38,Niklas,Schröder,,Barbarossastraße 19,Berlin,,Germany,10779,+49 030 2141444,,nschroder@surfeu.de,3 39,Camille,Bernard,,"4, Rue Milton",Paris,,France,75009,+33 01 49 70 65 65,,camille.bernard@yahoo.fr,4 40,Dominique,Lefebvre,,"8, Rue Hanovre",Paris,,France,75002,+33 01 47 42 71 71,,dominiquelefebvre@gmail.com,4 41,Marc,Dubois,,"11, Place Bellecour",Lyon,,France,69002,+33 04 78 30 30 30,,marc.dubois@hotmail.com,5 42,Wyatt,Girard,,"9, Place Louis Barthou",Bordeaux,,France,33000,+33 05 56 96 96 96,,wyatt.girard@yahoo.fr,3 43,Isabelle,Mercier,,"68, Rue Jouvence",Dijon,,France,21000,+33 03 80 73 66 99,,isabelle_mercier@apple.fr,3 44,Terhi,Hämäläinen,,Porthaninkatu 9,Helsinki,,Finland,00530,+358 09 870 2000,,terhi.hamalainen@apple.fi,3 45,Ladislav,Kovács,,Erzsébet krt. 58.,Budapest,,Hungary,H-1073,,,ladislav_kovacs@apple.hu,3 46,Hugh,O'Reilly,,3 Chatham Street,Dublin,Dublin,Ireland,,+353 01 6792424,,hughoreilly@apple.ie,3 47,Lucas,Mancini,,"Via Degli Scipioni, 43",Rome,RM,Italy,00192,+39 06 39733434,,lucas.mancini@yahoo.it,5 48,Johannes,Van der Berg,,Lijnbaansgracht 120bg,Amsterdam,VV,Netherlands,1016,+31 020 6223130,,johavanderberg@yahoo.nl,5 49,Stanisław,Wójcik,,Ordynacka 10,Warsaw,,Poland,00-358,+48 22 828 37 39,,stanisław.wójcik@wp.pl,4 50,Enrique,Muñoz,,C/ San Bernardo 85,Madrid,,Spain,28015,+34 914 454 454,,enrique_munoz@yahoo.es,5 51,Joakim,Johansson,,Celsiusg. 9,Stockholm,,Sweden,11230,+46 08-651 52 52,,joakim.johansson@yahoo.se,5 52,Emma,Jones,,202 Hoxton Street,London,,United Kingdom,N1 5LH,+44 020 7707 0707,,emma_jones@hotmail.com,3 53,Phil,Hughes,,113 Lupus St,London,,United Kingdom,SW1V 3EN,+44 020 7976 5722,,phil.hughes@gmail.com,3 54,Steve,Murray,,110 Raeburn Pl,Edinburgh ,,United Kingdom,EH4 1HH,+44 0131 315 3300,,steve.murray@yahoo.uk,5 55,Mark,Taylor,,421 Bourke Street,Sidney,NSW,Australia,2010,+61 (02) 9332 3633,,mark.taylor@yahoo.au,4 56,Diego,Gutiérrez,,307 Macacha Güemes,Buenos Aires,,Argentina,1106,+54 (0)11 4311 4333,,diego.gutierrez@yahoo.ar,4 57,Luis,Rojas,,"Calle Lira, 198",Santiago,,Chile,,+56 (0)2 635 4444,,luisrojas@yahoo.cl,5 58,Manoj,Pareek,,"12,Community Centre",Delhi,,India,110017,+91 0124 39883988,,manoj.pareek@rediff.com,3 59,Puja,Srivastava,,"3,Raj Bhavan Road",Bangalore,,India,560001,+91 080 22289999,,puja_srivastava@yahoo.in,3 ================================================ FILE: prqlc/prqlc/tests/integration/data/chinook/employees.csv ================================================ employee_id,last_name,first_name,title,reports_to,birth_date,hire_date,address,city,state,country,postal_code,phone,fax,email 1,Adams,Andrew,General Manager,6,1962-02-18T00:00:00.000000000,2002-08-14T00:00:00.000000000,11120 Jasper Ave NW,Edmonton,AB,Canada,T5K 2N1,+1 (780) 428-9482,+1 (780) 428-3457,andrew@chinookcorp.com 2,Edwards,Nancy,Sales Manager,1,1958-12-08T00:00:00.000000000,2002-05-01T00:00:00.000000000,825 8 Ave SW,Calgary,AB,Canada,T2P 2T3,+1 (403) 262-3443,+1 (403) 262-3322,nancy@chinookcorp.com 3,Peacock,Jane,Sales Support Agent,2,1973-08-29T00:00:00.000000000,2002-04-01T00:00:00.000000000,1111 6 Ave SW,Calgary,AB,Canada,T2P 5M5,+1 (403) 262-3443,+1 (403) 262-6712,jane@chinookcorp.com 4,Park,Margaret,Sales Support Agent,2,1947-09-19T00:00:00.000000000,2003-05-03T00:00:00.000000000,683 10 Street SW,Calgary,AB,Canada,T2P 5G3,+1 (403) 263-4423,+1 (403) 263-4289,margaret@chinookcorp.com 5,Johnson,Steve,Sales Support Agent,2,1965-03-03T00:00:00.000000000,2003-10-17T00:00:00.000000000,7727B 41 Ave,Calgary,AB,Canada,T3B 1Y7,1 (780) 836-9987,1 (780) 836-9543,steve@chinookcorp.com 6,Mitchell,Michael,IT Manager,1,1973-07-01T00:00:00.000000000,2003-10-17T00:00:00.000000000,5827 Bowness Road NW,Calgary,AB,Canada,T3B 0C5,+1 (403) 246-9887,+1 (403) 246-9899,michael@chinookcorp.com 7,King,Robert,IT Staff,6,1970-05-29T00:00:00.000000000,2004-01-02T00:00:00.000000000,590 Columbia Boulevard West,Lethbridge,AB,Canada,T1K 5N8,+1 (403) 456-9986,+1 (403) 456-8485,robert@chinookcorp.com 8,Callahan,Laura,IT Staff,6,1968-01-09T00:00:00.000000000,2004-03-04T00:00:00.000000000,923 7 ST NW,Lethbridge,AB,Canada,T1H 1Y8,+1 (403) 467-3351,+1 (403) 467-8772,laura@chinookcorp.com ================================================ FILE: prqlc/prqlc/tests/integration/data/chinook/genres.csv ================================================ genre_id,name 1,Rock 2,Jazz 3,Metal 4,Alternative & Punk 5,Rock And Roll 6,Blues 7,Latin 8,Reggae 9,Pop 10,Soundtrack 11,Bossa Nova 12,Easy Listening 13,Heavy Metal 14,R&B/Soul 15,Electronica/Dance 16,World 17,Hip Hop/Rap 18,Science Fiction 19,TV Shows 20,Sci Fi & Fantasy 21,Drama 22,Comedy 23,Alternative 24,Classical 25,Opera ================================================ FILE: prqlc/prqlc/tests/integration/data/chinook/invoice_items.csv ================================================ invoice_line_id,invoice_id,track_id,unit_price,quantity 1,1,2,0.99,1 2,1,4,0.99,1 3,2,6,0.99,1 4,2,8,0.99,1 5,2,10,0.99,1 6,2,12,0.99,1 7,3,16,0.99,1 8,3,20,0.99,1 9,3,24,0.99,1 10,3,28,0.99,1 11,3,32,0.99,1 12,3,36,0.99,1 13,4,42,0.99,1 14,4,48,0.99,1 15,4,54,0.99,1 16,4,60,0.99,1 17,4,66,0.99,1 18,4,72,0.99,1 19,4,78,0.99,1 20,4,84,0.99,1 21,4,90,0.99,1 22,5,99,0.99,1 23,5,108,0.99,1 24,5,117,0.99,1 25,5,126,0.99,1 26,5,135,0.99,1 27,5,144,0.99,1 28,5,153,0.99,1 29,5,162,0.99,1 30,5,171,0.99,1 31,5,180,0.99,1 32,5,189,0.99,1 33,5,198,0.99,1 34,5,207,0.99,1 35,5,216,0.99,1 36,6,230,0.99,1 37,7,231,0.99,1 38,7,232,0.99,1 39,8,234,0.99,1 40,8,236,0.99,1 41,9,238,0.99,1 42,9,240,0.99,1 43,9,242,0.99,1 44,9,244,0.99,1 45,10,248,0.99,1 46,10,252,0.99,1 47,10,256,0.99,1 48,10,260,0.99,1 49,10,264,0.99,1 50,10,268,0.99,1 51,11,274,0.99,1 52,11,280,0.99,1 53,11,286,0.99,1 54,11,292,0.99,1 55,11,298,0.99,1 56,11,304,0.99,1 57,11,310,0.99,1 58,11,316,0.99,1 59,11,322,0.99,1 60,12,331,0.99,1 61,12,340,0.99,1 62,12,349,0.99,1 63,12,358,0.99,1 64,12,367,0.99,1 65,12,376,0.99,1 66,12,385,0.99,1 67,12,394,0.99,1 68,12,403,0.99,1 69,12,412,0.99,1 70,12,421,0.99,1 71,12,430,0.99,1 72,12,439,0.99,1 73,12,448,0.99,1 74,13,462,0.99,1 75,14,463,0.99,1 76,14,464,0.99,1 77,15,466,0.99,1 78,15,468,0.99,1 79,16,470,0.99,1 80,16,472,0.99,1 81,16,474,0.99,1 82,16,476,0.99,1 83,17,480,0.99,1 84,17,484,0.99,1 85,17,488,0.99,1 86,17,492,0.99,1 87,17,496,0.99,1 88,17,500,0.99,1 89,18,506,0.99,1 90,18,512,0.99,1 91,18,518,0.99,1 92,18,524,0.99,1 93,18,530,0.99,1 94,18,536,0.99,1 95,18,542,0.99,1 96,18,548,0.99,1 97,18,554,0.99,1 98,19,563,0.99,1 99,19,572,0.99,1 100,19,581,0.99,1 101,19,590,0.99,1 102,19,599,0.99,1 103,19,608,0.99,1 104,19,617,0.99,1 105,19,626,0.99,1 106,19,635,0.99,1 107,19,644,0.99,1 108,19,653,0.99,1 109,19,662,0.99,1 110,19,671,0.99,1 111,19,680,0.99,1 112,20,694,0.99,1 113,21,695,0.99,1 114,21,696,0.99,1 115,22,698,0.99,1 116,22,700,0.99,1 117,23,702,0.99,1 118,23,704,0.99,1 119,23,706,0.99,1 120,23,708,0.99,1 121,24,712,0.99,1 122,24,716,0.99,1 123,24,720,0.99,1 124,24,724,0.99,1 125,24,728,0.99,1 126,24,732,0.99,1 127,25,738,0.99,1 128,25,744,0.99,1 129,25,750,0.99,1 130,25,756,0.99,1 131,25,762,0.99,1 132,25,768,0.99,1 133,25,774,0.99,1 134,25,780,0.99,1 135,25,786,0.99,1 136,26,795,0.99,1 137,26,804,0.99,1 138,26,813,0.99,1 139,26,822,0.99,1 140,26,831,0.99,1 141,26,840,0.99,1 142,26,849,0.99,1 143,26,858,0.99,1 144,26,867,0.99,1 145,26,876,0.99,1 146,26,885,0.99,1 147,26,894,0.99,1 148,26,903,0.99,1 149,26,912,0.99,1 150,27,926,0.99,1 151,28,927,0.99,1 152,28,928,0.99,1 153,29,930,0.99,1 154,29,932,0.99,1 155,30,934,0.99,1 156,30,936,0.99,1 157,30,938,0.99,1 158,30,940,0.99,1 159,31,944,0.99,1 160,31,948,0.99,1 161,31,952,0.99,1 162,31,956,0.99,1 163,31,960,0.99,1 164,31,964,0.99,1 165,32,970,0.99,1 166,32,976,0.99,1 167,32,982,0.99,1 168,32,988,0.99,1 169,32,994,0.99,1 170,32,1000,0.99,1 171,32,1006,0.99,1 172,32,1012,0.99,1 173,32,1018,0.99,1 174,33,1027,0.99,1 175,33,1036,0.99,1 176,33,1045,0.99,1 177,33,1054,0.99,1 178,33,1063,0.99,1 179,33,1072,0.99,1 180,33,1081,0.99,1 181,33,1090,0.99,1 182,33,1099,0.99,1 183,33,1108,0.99,1 184,33,1117,0.99,1 185,33,1126,0.99,1 186,33,1135,0.99,1 187,33,1144,0.99,1 188,34,1158,0.99,1 189,35,1159,0.99,1 190,35,1160,0.99,1 191,36,1162,0.99,1 192,36,1164,0.99,1 193,37,1166,0.99,1 194,37,1168,0.99,1 195,37,1170,0.99,1 196,37,1172,0.99,1 197,38,1176,0.99,1 198,38,1180,0.99,1 199,38,1184,0.99,1 200,38,1188,0.99,1 201,38,1192,0.99,1 202,38,1196,0.99,1 203,39,1202,0.99,1 204,39,1208,0.99,1 205,39,1214,0.99,1 206,39,1220,0.99,1 207,39,1226,0.99,1 208,39,1232,0.99,1 209,39,1238,0.99,1 210,39,1244,0.99,1 211,39,1250,0.99,1 212,40,1259,0.99,1 213,40,1268,0.99,1 214,40,1277,0.99,1 215,40,1286,0.99,1 216,40,1295,0.99,1 217,40,1304,0.99,1 218,40,1313,0.99,1 219,40,1322,0.99,1 220,40,1331,0.99,1 221,40,1340,0.99,1 222,40,1349,0.99,1 223,40,1358,0.99,1 224,40,1367,0.99,1 225,40,1376,0.99,1 226,41,1390,0.99,1 227,42,1391,0.99,1 228,42,1392,0.99,1 229,43,1394,0.99,1 230,43,1396,0.99,1 231,44,1398,0.99,1 232,44,1400,0.99,1 233,44,1402,0.99,1 234,44,1404,0.99,1 235,45,1408,0.99,1 236,45,1412,0.99,1 237,45,1416,0.99,1 238,45,1420,0.99,1 239,45,1424,0.99,1 240,45,1428,0.99,1 241,46,1434,0.99,1 242,46,1440,0.99,1 243,46,1446,0.99,1 244,46,1452,0.99,1 245,46,1458,0.99,1 246,46,1464,0.99,1 247,46,1470,0.99,1 248,46,1476,0.99,1 249,46,1482,0.99,1 250,47,1491,0.99,1 251,47,1500,0.99,1 252,47,1509,0.99,1 253,47,1518,0.99,1 254,47,1527,0.99,1 255,47,1536,0.99,1 256,47,1545,0.99,1 257,47,1554,0.99,1 258,47,1563,0.99,1 259,47,1572,0.99,1 260,47,1581,0.99,1 261,47,1590,0.99,1 262,47,1599,0.99,1 263,47,1608,0.99,1 264,48,1622,0.99,1 265,49,1623,0.99,1 266,49,1624,0.99,1 267,50,1626,0.99,1 268,50,1628,0.99,1 269,51,1630,0.99,1 270,51,1632,0.99,1 271,51,1634,0.99,1 272,51,1636,0.99,1 273,52,1640,0.99,1 274,52,1644,0.99,1 275,52,1648,0.99,1 276,52,1652,0.99,1 277,52,1656,0.99,1 278,52,1660,0.99,1 279,53,1666,0.99,1 280,53,1672,0.99,1 281,53,1678,0.99,1 282,53,1684,0.99,1 283,53,1690,0.99,1 284,53,1696,0.99,1 285,53,1702,0.99,1 286,53,1708,0.99,1 287,53,1714,0.99,1 288,54,1723,0.99,1 289,54,1732,0.99,1 290,54,1741,0.99,1 291,54,1750,0.99,1 292,54,1759,0.99,1 293,54,1768,0.99,1 294,54,1777,0.99,1 295,54,1786,0.99,1 296,54,1795,0.99,1 297,54,1804,0.99,1 298,54,1813,0.99,1 299,54,1822,0.99,1 300,54,1831,0.99,1 301,54,1840,0.99,1 302,55,1854,0.99,1 303,56,1855,0.99,1 304,56,1856,0.99,1 305,57,1858,0.99,1 306,57,1860,0.99,1 307,58,1862,0.99,1 308,58,1864,0.99,1 309,58,1866,0.99,1 310,58,1868,0.99,1 311,59,1872,0.99,1 312,59,1876,0.99,1 313,59,1880,0.99,1 314,59,1884,0.99,1 315,59,1888,0.99,1 316,59,1892,0.99,1 317,60,1898,0.99,1 318,60,1904,0.99,1 319,60,1910,0.99,1 320,60,1916,0.99,1 321,60,1922,0.99,1 322,60,1928,0.99,1 323,60,1934,0.99,1 324,60,1940,0.99,1 325,60,1946,0.99,1 326,61,1955,0.99,1 327,61,1964,0.99,1 328,61,1973,0.99,1 329,61,1982,0.99,1 330,61,1991,0.99,1 331,61,2000,0.99,1 332,61,2009,0.99,1 333,61,2018,0.99,1 334,61,2027,0.99,1 335,61,2036,0.99,1 336,61,2045,0.99,1 337,61,2054,0.99,1 338,61,2063,0.99,1 339,61,2072,0.99,1 340,62,2086,0.99,1 341,63,2087,0.99,1 342,63,2088,0.99,1 343,64,2090,0.99,1 344,64,2092,0.99,1 345,65,2094,0.99,1 346,65,2096,0.99,1 347,65,2098,0.99,1 348,65,2100,0.99,1 349,66,2104,0.99,1 350,66,2108,0.99,1 351,66,2112,0.99,1 352,66,2116,0.99,1 353,66,2120,0.99,1 354,66,2124,0.99,1 355,67,2130,0.99,1 356,67,2136,0.99,1 357,67,2142,0.99,1 358,67,2148,0.99,1 359,67,2154,0.99,1 360,67,2160,0.99,1 361,67,2166,0.99,1 362,67,2172,0.99,1 363,67,2178,0.99,1 364,68,2187,0.99,1 365,68,2196,0.99,1 366,68,2205,0.99,1 367,68,2214,0.99,1 368,68,2223,0.99,1 369,68,2232,0.99,1 370,68,2241,0.99,1 371,68,2250,0.99,1 372,68,2259,0.99,1 373,68,2268,0.99,1 374,68,2277,0.99,1 375,68,2286,0.99,1 376,68,2295,0.99,1 377,68,2304,0.99,1 378,69,2318,0.99,1 379,70,2319,0.99,1 380,70,2320,0.99,1 381,71,2322,0.99,1 382,71,2324,0.99,1 383,72,2326,0.99,1 384,72,2328,0.99,1 385,72,2330,0.99,1 386,72,2332,0.99,1 387,73,2336,0.99,1 388,73,2340,0.99,1 389,73,2344,0.99,1 390,73,2348,0.99,1 391,73,2352,0.99,1 392,73,2356,0.99,1 393,74,2362,0.99,1 394,74,2368,0.99,1 395,74,2374,0.99,1 396,74,2380,0.99,1 397,74,2386,0.99,1 398,74,2392,0.99,1 399,74,2398,0.99,1 400,74,2404,0.99,1 401,74,2410,0.99,1 402,75,2419,0.99,1 403,75,2428,0.99,1 404,75,2437,0.99,1 405,75,2446,0.99,1 406,75,2455,0.99,1 407,75,2464,0.99,1 408,75,2473,0.99,1 409,75,2482,0.99,1 410,75,2491,0.99,1 411,75,2500,0.99,1 412,75,2509,0.99,1 413,75,2518,0.99,1 414,75,2527,0.99,1 415,75,2536,0.99,1 416,76,2550,0.99,1 417,77,2551,0.99,1 418,77,2552,0.99,1 419,78,2554,0.99,1 420,78,2556,0.99,1 421,79,2558,0.99,1 422,79,2560,0.99,1 423,79,2562,0.99,1 424,79,2564,0.99,1 425,80,2568,0.99,1 426,80,2572,0.99,1 427,80,2576,0.99,1 428,80,2580,0.99,1 429,80,2584,0.99,1 430,80,2588,0.99,1 431,81,2594,0.99,1 432,81,2600,0.99,1 433,81,2606,0.99,1 434,81,2612,0.99,1 435,81,2618,0.99,1 436,81,2624,0.99,1 437,81,2630,0.99,1 438,81,2636,0.99,1 439,81,2642,0.99,1 440,82,2651,0.99,1 441,82,2660,0.99,1 442,82,2669,0.99,1 443,82,2678,0.99,1 444,82,2687,0.99,1 445,82,2696,0.99,1 446,82,2705,0.99,1 447,82,2714,0.99,1 448,82,2723,0.99,1 449,82,2732,0.99,1 450,82,2741,0.99,1 451,82,2750,0.99,1 452,82,2759,0.99,1 453,82,2768,0.99,1 454,83,2782,0.99,1 455,84,2783,0.99,1 456,84,2784,0.99,1 457,85,2786,0.99,1 458,85,2788,0.99,1 459,86,2790,0.99,1 460,86,2792,0.99,1 461,86,2794,0.99,1 462,86,2796,0.99,1 463,87,2800,0.99,1 464,87,2804,0.99,1 465,87,2808,0.99,1 466,87,2812,0.99,1 467,87,2816,0.99,1 468,87,2820,1.99,1 469,88,2826,1.99,1 470,88,2832,1.99,1 471,88,2838,1.99,1 472,88,2844,1.99,1 473,88,2850,1.99,1 474,88,2856,1.99,1 475,88,2862,1.99,1 476,88,2868,1.99,1 477,88,2874,1.99,1 478,89,2883,1.99,1 479,89,2892,1.99,1 480,89,2901,1.99,1 481,89,2910,1.99,1 482,89,2919,1.99,1 483,89,2928,0.99,1 484,89,2937,0.99,1 485,89,2946,0.99,1 486,89,2955,0.99,1 487,89,2964,0.99,1 488,89,2973,0.99,1 489,89,2982,0.99,1 490,89,2991,0.99,1 491,89,3000,0.99,1 492,90,3014,0.99,1 493,91,3015,0.99,1 494,91,3016,0.99,1 495,92,3018,0.99,1 496,92,3020,0.99,1 497,93,3022,0.99,1 498,93,3024,0.99,1 499,93,3026,0.99,1 500,93,3028,0.99,1 501,94,3032,0.99,1 502,94,3036,0.99,1 503,94,3040,0.99,1 504,94,3044,0.99,1 505,94,3048,0.99,1 506,94,3052,0.99,1 507,95,3058,0.99,1 508,95,3064,0.99,1 509,95,3070,0.99,1 510,95,3076,0.99,1 511,95,3082,0.99,1 512,95,3088,0.99,1 513,95,3094,0.99,1 514,95,3100,0.99,1 515,95,3106,0.99,1 516,96,3115,0.99,1 517,96,3124,0.99,1 518,96,3133,0.99,1 519,96,3142,0.99,1 520,96,3151,0.99,1 521,96,3160,0.99,1 522,96,3169,1.99,1 523,96,3178,1.99,1 524,96,3187,1.99,1 525,96,3196,1.99,1 526,96,3205,1.99,1 527,96,3214,1.99,1 528,96,3223,1.99,1 529,96,3232,1.99,1 530,97,3246,1.99,1 531,98,3247,1.99,1 532,98,3248,1.99,1 533,99,3250,1.99,1 534,99,3252,1.99,1 535,100,3254,0.99,1 536,100,3256,0.99,1 537,100,3258,0.99,1 538,100,3260,0.99,1 539,101,3264,0.99,1 540,101,3268,0.99,1 541,101,3272,0.99,1 542,101,3276,0.99,1 543,101,3280,0.99,1 544,101,3284,0.99,1 545,102,3290,0.99,1 546,102,3296,0.99,1 547,102,3302,0.99,1 548,102,3308,0.99,1 549,102,3314,0.99,1 550,102,3320,0.99,1 551,102,3326,0.99,1 552,102,3332,0.99,1 553,102,3338,1.99,1 554,103,3347,1.99,1 555,103,3356,0.99,1 556,103,3365,0.99,1 557,103,3374,0.99,1 558,103,3383,0.99,1 559,103,3392,0.99,1 560,103,3401,0.99,1 561,103,3410,0.99,1 562,103,3419,0.99,1 563,103,3428,1.99,1 564,103,3437,0.99,1 565,103,3446,0.99,1 566,103,3455,0.99,1 567,103,3464,0.99,1 568,104,3478,0.99,1 569,105,3479,0.99,1 570,105,3480,0.99,1 571,106,3482,0.99,1 572,106,3484,0.99,1 573,107,3486,0.99,1 574,107,3488,0.99,1 575,107,3490,0.99,1 576,107,3492,0.99,1 577,108,3496,0.99,1 578,108,3500,0.99,1 579,108,1,0.99,1 580,108,5,0.99,1 581,108,9,0.99,1 582,108,13,0.99,1 583,109,19,0.99,1 584,109,25,0.99,1 585,109,31,0.99,1 586,109,37,0.99,1 587,109,43,0.99,1 588,109,49,0.99,1 589,109,55,0.99,1 590,109,61,0.99,1 591,109,67,0.99,1 592,110,76,0.99,1 593,110,85,0.99,1 594,110,94,0.99,1 595,110,103,0.99,1 596,110,112,0.99,1 597,110,121,0.99,1 598,110,130,0.99,1 599,110,139,0.99,1 600,110,148,0.99,1 601,110,157,0.99,1 602,110,166,0.99,1 603,110,175,0.99,1 604,110,184,0.99,1 605,110,193,0.99,1 606,111,207,0.99,1 607,112,208,0.99,1 608,112,209,0.99,1 609,113,211,0.99,1 610,113,213,0.99,1 611,114,215,0.99,1 612,114,217,0.99,1 613,114,219,0.99,1 614,114,221,0.99,1 615,115,225,0.99,1 616,115,229,0.99,1 617,115,233,0.99,1 618,115,237,0.99,1 619,115,241,0.99,1 620,115,245,0.99,1 621,116,251,0.99,1 622,116,257,0.99,1 623,116,263,0.99,1 624,116,269,0.99,1 625,116,275,0.99,1 626,116,281,0.99,1 627,116,287,0.99,1 628,116,293,0.99,1 629,116,299,0.99,1 630,117,308,0.99,1 631,117,317,0.99,1 632,117,326,0.99,1 633,117,335,0.99,1 634,117,344,0.99,1 635,117,353,0.99,1 636,117,362,0.99,1 637,117,371,0.99,1 638,117,380,0.99,1 639,117,389,0.99,1 640,117,398,0.99,1 641,117,407,0.99,1 642,117,416,0.99,1 643,117,425,0.99,1 644,118,439,0.99,1 645,119,440,0.99,1 646,119,441,0.99,1 647,120,443,0.99,1 648,120,445,0.99,1 649,121,447,0.99,1 650,121,449,0.99,1 651,121,451,0.99,1 652,121,453,0.99,1 653,122,457,0.99,1 654,122,461,0.99,1 655,122,465,0.99,1 656,122,469,0.99,1 657,122,473,0.99,1 658,122,477,0.99,1 659,123,483,0.99,1 660,123,489,0.99,1 661,123,495,0.99,1 662,123,501,0.99,1 663,123,507,0.99,1 664,123,513,0.99,1 665,123,519,0.99,1 666,123,525,0.99,1 667,123,531,0.99,1 668,124,540,0.99,1 669,124,549,0.99,1 670,124,558,0.99,1 671,124,567,0.99,1 672,124,576,0.99,1 673,124,585,0.99,1 674,124,594,0.99,1 675,124,603,0.99,1 676,124,612,0.99,1 677,124,621,0.99,1 678,124,630,0.99,1 679,124,639,0.99,1 680,124,648,0.99,1 681,124,657,0.99,1 682,125,671,0.99,1 683,126,672,0.99,1 684,126,673,0.99,1 685,127,675,0.99,1 686,127,677,0.99,1 687,128,679,0.99,1 688,128,681,0.99,1 689,128,683,0.99,1 690,128,685,0.99,1 691,129,689,0.99,1 692,129,693,0.99,1 693,129,697,0.99,1 694,129,701,0.99,1 695,129,705,0.99,1 696,129,709,0.99,1 697,130,715,0.99,1 698,130,721,0.99,1 699,130,727,0.99,1 700,130,733,0.99,1 701,130,739,0.99,1 702,130,745,0.99,1 703,130,751,0.99,1 704,130,757,0.99,1 705,130,763,0.99,1 706,131,772,0.99,1 707,131,781,0.99,1 708,131,790,0.99,1 709,131,799,0.99,1 710,131,808,0.99,1 711,131,817,0.99,1 712,131,826,0.99,1 713,131,835,0.99,1 714,131,844,0.99,1 715,131,853,0.99,1 716,131,862,0.99,1 717,131,871,0.99,1 718,131,880,0.99,1 719,131,889,0.99,1 720,132,903,0.99,1 721,133,904,0.99,1 722,133,905,0.99,1 723,134,907,0.99,1 724,134,909,0.99,1 725,135,911,0.99,1 726,135,913,0.99,1 727,135,915,0.99,1 728,135,917,0.99,1 729,136,921,0.99,1 730,136,925,0.99,1 731,136,929,0.99,1 732,136,933,0.99,1 733,136,937,0.99,1 734,136,941,0.99,1 735,137,947,0.99,1 736,137,953,0.99,1 737,137,959,0.99,1 738,137,965,0.99,1 739,137,971,0.99,1 740,137,977,0.99,1 741,137,983,0.99,1 742,137,989,0.99,1 743,137,995,0.99,1 744,138,1004,0.99,1 745,138,1013,0.99,1 746,138,1022,0.99,1 747,138,1031,0.99,1 748,138,1040,0.99,1 749,138,1049,0.99,1 750,138,1058,0.99,1 751,138,1067,0.99,1 752,138,1076,0.99,1 753,138,1085,0.99,1 754,138,1094,0.99,1 755,138,1103,0.99,1 756,138,1112,0.99,1 757,138,1121,0.99,1 758,139,1135,0.99,1 759,140,1136,0.99,1 760,140,1137,0.99,1 761,141,1139,0.99,1 762,141,1141,0.99,1 763,142,1143,0.99,1 764,142,1145,0.99,1 765,142,1147,0.99,1 766,142,1149,0.99,1 767,143,1153,0.99,1 768,143,1157,0.99,1 769,143,1161,0.99,1 770,143,1165,0.99,1 771,143,1169,0.99,1 772,143,1173,0.99,1 773,144,1179,0.99,1 774,144,1185,0.99,1 775,144,1191,0.99,1 776,144,1197,0.99,1 777,144,1203,0.99,1 778,144,1209,0.99,1 779,144,1215,0.99,1 780,144,1221,0.99,1 781,144,1227,0.99,1 782,145,1236,0.99,1 783,145,1245,0.99,1 784,145,1254,0.99,1 785,145,1263,0.99,1 786,145,1272,0.99,1 787,145,1281,0.99,1 788,145,1290,0.99,1 789,145,1299,0.99,1 790,145,1308,0.99,1 791,145,1317,0.99,1 792,145,1326,0.99,1 793,145,1335,0.99,1 794,145,1344,0.99,1 795,145,1353,0.99,1 796,146,1367,0.99,1 797,147,1368,0.99,1 798,147,1369,0.99,1 799,148,1371,0.99,1 800,148,1373,0.99,1 801,149,1375,0.99,1 802,149,1377,0.99,1 803,149,1379,0.99,1 804,149,1381,0.99,1 805,150,1385,0.99,1 806,150,1389,0.99,1 807,150,1393,0.99,1 808,150,1397,0.99,1 809,150,1401,0.99,1 810,150,1405,0.99,1 811,151,1411,0.99,1 812,151,1417,0.99,1 813,151,1423,0.99,1 814,151,1429,0.99,1 815,151,1435,0.99,1 816,151,1441,0.99,1 817,151,1447,0.99,1 818,151,1453,0.99,1 819,151,1459,0.99,1 820,152,1468,0.99,1 821,152,1477,0.99,1 822,152,1486,0.99,1 823,152,1495,0.99,1 824,152,1504,0.99,1 825,152,1513,0.99,1 826,152,1522,0.99,1 827,152,1531,0.99,1 828,152,1540,0.99,1 829,152,1549,0.99,1 830,152,1558,0.99,1 831,152,1567,0.99,1 832,152,1576,0.99,1 833,152,1585,0.99,1 834,153,1599,0.99,1 835,154,1600,0.99,1 836,154,1601,0.99,1 837,155,1603,0.99,1 838,155,1605,0.99,1 839,156,1607,0.99,1 840,156,1609,0.99,1 841,156,1611,0.99,1 842,156,1613,0.99,1 843,157,1617,0.99,1 844,157,1621,0.99,1 845,157,1625,0.99,1 846,157,1629,0.99,1 847,157,1633,0.99,1 848,157,1637,0.99,1 849,158,1643,0.99,1 850,158,1649,0.99,1 851,158,1655,0.99,1 852,158,1661,0.99,1 853,158,1667,0.99,1 854,158,1673,0.99,1 855,158,1679,0.99,1 856,158,1685,0.99,1 857,158,1691,0.99,1 858,159,1700,0.99,1 859,159,1709,0.99,1 860,159,1718,0.99,1 861,159,1727,0.99,1 862,159,1736,0.99,1 863,159,1745,0.99,1 864,159,1754,0.99,1 865,159,1763,0.99,1 866,159,1772,0.99,1 867,159,1781,0.99,1 868,159,1790,0.99,1 869,159,1799,0.99,1 870,159,1808,0.99,1 871,159,1817,0.99,1 872,160,1831,0.99,1 873,161,1832,0.99,1 874,161,1833,0.99,1 875,162,1835,0.99,1 876,162,1837,0.99,1 877,163,1839,0.99,1 878,163,1841,0.99,1 879,163,1843,0.99,1 880,163,1845,0.99,1 881,164,1849,0.99,1 882,164,1853,0.99,1 883,164,1857,0.99,1 884,164,1861,0.99,1 885,164,1865,0.99,1 886,164,1869,0.99,1 887,165,1875,0.99,1 888,165,1881,0.99,1 889,165,1887,0.99,1 890,165,1893,0.99,1 891,165,1899,0.99,1 892,165,1905,0.99,1 893,165,1911,0.99,1 894,165,1917,0.99,1 895,165,1923,0.99,1 896,166,1932,0.99,1 897,166,1941,0.99,1 898,166,1950,0.99,1 899,166,1959,0.99,1 900,166,1968,0.99,1 901,166,1977,0.99,1 902,166,1986,0.99,1 903,166,1995,0.99,1 904,166,2004,0.99,1 905,166,2013,0.99,1 906,166,2022,0.99,1 907,166,2031,0.99,1 908,166,2040,0.99,1 909,166,2049,0.99,1 910,167,2063,0.99,1 911,168,2064,0.99,1 912,168,2065,0.99,1 913,169,2067,0.99,1 914,169,2069,0.99,1 915,170,2071,0.99,1 916,170,2073,0.99,1 917,170,2075,0.99,1 918,170,2077,0.99,1 919,171,2081,0.99,1 920,171,2085,0.99,1 921,171,2089,0.99,1 922,171,2093,0.99,1 923,171,2097,0.99,1 924,171,2101,0.99,1 925,172,2107,0.99,1 926,172,2113,0.99,1 927,172,2119,0.99,1 928,172,2125,0.99,1 929,172,2131,0.99,1 930,172,2137,0.99,1 931,172,2143,0.99,1 932,172,2149,0.99,1 933,172,2155,0.99,1 934,173,2164,0.99,1 935,173,2173,0.99,1 936,173,2182,0.99,1 937,173,2191,0.99,1 938,173,2200,0.99,1 939,173,2209,0.99,1 940,173,2218,0.99,1 941,173,2227,0.99,1 942,173,2236,0.99,1 943,173,2245,0.99,1 944,173,2254,0.99,1 945,173,2263,0.99,1 946,173,2272,0.99,1 947,173,2281,0.99,1 948,174,2295,0.99,1 949,175,2296,0.99,1 950,175,2297,0.99,1 951,176,2299,0.99,1 952,176,2301,0.99,1 953,177,2303,0.99,1 954,177,2305,0.99,1 955,177,2307,0.99,1 956,177,2309,0.99,1 957,178,2313,0.99,1 958,178,2317,0.99,1 959,178,2321,0.99,1 960,178,2325,0.99,1 961,178,2329,0.99,1 962,178,2333,0.99,1 963,179,2339,0.99,1 964,179,2345,0.99,1 965,179,2351,0.99,1 966,179,2357,0.99,1 967,179,2363,0.99,1 968,179,2369,0.99,1 969,179,2375,0.99,1 970,179,2381,0.99,1 971,179,2387,0.99,1 972,180,2396,0.99,1 973,180,2405,0.99,1 974,180,2414,0.99,1 975,180,2423,0.99,1 976,180,2432,0.99,1 977,180,2441,0.99,1 978,180,2450,0.99,1 979,180,2459,0.99,1 980,180,2468,0.99,1 981,180,2477,0.99,1 982,180,2486,0.99,1 983,180,2495,0.99,1 984,180,2504,0.99,1 985,180,2513,0.99,1 986,181,2527,0.99,1 987,182,2528,0.99,1 988,182,2529,0.99,1 989,183,2531,0.99,1 990,183,2533,0.99,1 991,184,2535,0.99,1 992,184,2537,0.99,1 993,184,2539,0.99,1 994,184,2541,0.99,1 995,185,2545,0.99,1 996,185,2549,0.99,1 997,185,2553,0.99,1 998,185,2557,0.99,1 999,185,2561,0.99,1 1000,185,2565,0.99,1 1001,186,2571,0.99,1 1002,186,2577,0.99,1 1003,186,2583,0.99,1 1004,186,2589,0.99,1 1005,186,2595,0.99,1 1006,186,2601,0.99,1 1007,186,2607,0.99,1 1008,186,2613,0.99,1 1009,186,2619,0.99,1 1010,187,2628,0.99,1 1011,187,2637,0.99,1 1012,187,2646,0.99,1 1013,187,2655,0.99,1 1014,187,2664,0.99,1 1015,187,2673,0.99,1 1016,187,2682,0.99,1 1017,187,2691,0.99,1 1018,187,2700,0.99,1 1019,187,2709,0.99,1 1020,187,2718,0.99,1 1021,187,2727,0.99,1 1022,187,2736,0.99,1 1023,187,2745,0.99,1 1024,188,2759,0.99,1 1025,189,2760,0.99,1 1026,189,2761,0.99,1 1027,190,2763,0.99,1 1028,190,2765,0.99,1 1029,191,2767,0.99,1 1030,191,2769,0.99,1 1031,191,2771,0.99,1 1032,191,2773,0.99,1 1033,192,2777,0.99,1 1034,192,2781,0.99,1 1035,192,2785,0.99,1 1036,192,2789,0.99,1 1037,192,2793,0.99,1 1038,192,2797,0.99,1 1039,193,2803,0.99,1 1040,193,2809,0.99,1 1041,193,2815,0.99,1 1042,193,2821,1.99,1 1043,193,2827,1.99,1 1044,193,2833,1.99,1 1045,193,2839,1.99,1 1046,193,2845,1.99,1 1047,193,2851,1.99,1 1048,194,2860,1.99,1 1049,194,2869,1.99,1 1050,194,2878,1.99,1 1051,194,2887,1.99,1 1052,194,2896,1.99,1 1053,194,2905,1.99,1 1054,194,2914,1.99,1 1055,194,2923,1.99,1 1056,194,2932,0.99,1 1057,194,2941,0.99,1 1058,194,2950,0.99,1 1059,194,2959,0.99,1 1060,194,2968,0.99,1 1061,194,2977,0.99,1 1062,195,2991,0.99,1 1063,196,2992,0.99,1 1064,196,2993,0.99,1 1065,197,2995,0.99,1 1066,197,2997,0.99,1 1067,198,2999,0.99,1 1068,198,3001,0.99,1 1069,198,3003,0.99,1 1070,198,3005,0.99,1 1071,199,3009,0.99,1 1072,199,3013,0.99,1 1073,199,3017,0.99,1 1074,199,3021,0.99,1 1075,199,3025,0.99,1 1076,199,3029,0.99,1 1077,200,3035,0.99,1 1078,200,3041,0.99,1 1079,200,3047,0.99,1 1080,200,3053,0.99,1 1081,200,3059,0.99,1 1082,200,3065,0.99,1 1083,200,3071,0.99,1 1084,200,3077,0.99,1 1085,200,3083,0.99,1 1086,201,3092,0.99,1 1087,201,3101,0.99,1 1088,201,3110,0.99,1 1089,201,3119,0.99,1 1090,201,3128,0.99,1 1091,201,3137,0.99,1 1092,201,3146,0.99,1 1093,201,3155,0.99,1 1094,201,3164,0.99,1 1095,201,3173,1.99,1 1096,201,3182,1.99,1 1097,201,3191,1.99,1 1098,201,3200,1.99,1 1099,201,3209,1.99,1 1100,202,3223,1.99,1 1101,203,3224,1.99,1 1102,203,3225,0.99,1 1103,204,3227,1.99,1 1104,204,3229,1.99,1 1105,205,3231,1.99,1 1106,205,3233,1.99,1 1107,205,3235,1.99,1 1108,205,3237,1.99,1 1109,206,3241,1.99,1 1110,206,3245,1.99,1 1111,206,3249,1.99,1 1112,206,3253,0.99,1 1113,206,3257,0.99,1 1114,206,3261,0.99,1 1115,207,3267,0.99,1 1116,207,3273,0.99,1 1117,207,3279,0.99,1 1118,207,3285,0.99,1 1119,207,3291,0.99,1 1120,207,3297,0.99,1 1121,207,3303,0.99,1 1122,207,3309,0.99,1 1123,207,3315,0.99,1 1124,208,3324,0.99,1 1125,208,3333,0.99,1 1126,208,3342,1.99,1 1127,208,3351,0.99,1 1128,208,3360,1.99,1 1129,208,3369,0.99,1 1130,208,3378,0.99,1 1131,208,3387,0.99,1 1132,208,3396,0.99,1 1133,208,3405,0.99,1 1134,208,3414,0.99,1 1135,208,3423,0.99,1 1136,208,3432,0.99,1 1137,208,3441,0.99,1 1138,209,3455,0.99,1 1139,210,3456,0.99,1 1140,210,3457,0.99,1 1141,211,3459,0.99,1 1142,211,3461,0.99,1 1143,212,3463,0.99,1 1144,212,3465,0.99,1 1145,212,3467,0.99,1 1146,212,3469,0.99,1 1147,213,3473,0.99,1 1148,213,3477,0.99,1 1149,213,3481,0.99,1 1150,213,3485,0.99,1 1151,213,3489,0.99,1 1152,213,3493,0.99,1 1153,214,3499,0.99,1 1154,214,2,0.99,1 1155,214,8,0.99,1 1156,214,14,0.99,1 1157,214,20,0.99,1 1158,214,26,0.99,1 1159,214,32,0.99,1 1160,214,38,0.99,1 1161,214,44,0.99,1 1162,215,53,0.99,1 1163,215,62,0.99,1 1164,215,71,0.99,1 1165,215,80,0.99,1 1166,215,89,0.99,1 1167,215,98,0.99,1 1168,215,107,0.99,1 1169,215,116,0.99,1 1170,215,125,0.99,1 1171,215,134,0.99,1 1172,215,143,0.99,1 1173,215,152,0.99,1 1174,215,161,0.99,1 1175,215,170,0.99,1 1176,216,184,0.99,1 1177,217,185,0.99,1 1178,217,186,0.99,1 1179,218,188,0.99,1 1180,218,190,0.99,1 1181,219,192,0.99,1 1182,219,194,0.99,1 1183,219,196,0.99,1 1184,219,198,0.99,1 1185,220,202,0.99,1 1186,220,206,0.99,1 1187,220,210,0.99,1 1188,220,214,0.99,1 1189,220,218,0.99,1 1190,220,222,0.99,1 1191,221,228,0.99,1 1192,221,234,0.99,1 1193,221,240,0.99,1 1194,221,246,0.99,1 1195,221,252,0.99,1 1196,221,258,0.99,1 1197,221,264,0.99,1 1198,221,270,0.99,1 1199,221,276,0.99,1 1200,222,285,0.99,1 1201,222,294,0.99,1 1202,222,303,0.99,1 1203,222,312,0.99,1 1204,222,321,0.99,1 1205,222,330,0.99,1 1206,222,339,0.99,1 1207,222,348,0.99,1 1208,222,357,0.99,1 1209,222,366,0.99,1 1210,222,375,0.99,1 1211,222,384,0.99,1 1212,222,393,0.99,1 1213,222,402,0.99,1 1214,223,416,0.99,1 1215,224,417,0.99,1 1216,224,418,0.99,1 1217,225,420,0.99,1 1218,225,422,0.99,1 1219,226,424,0.99,1 1220,226,426,0.99,1 1221,226,428,0.99,1 1222,226,430,0.99,1 1223,227,434,0.99,1 1224,227,438,0.99,1 1225,227,442,0.99,1 1226,227,446,0.99,1 1227,227,450,0.99,1 1228,227,454,0.99,1 1229,228,460,0.99,1 1230,228,466,0.99,1 1231,228,472,0.99,1 1232,228,478,0.99,1 1233,228,484,0.99,1 1234,228,490,0.99,1 1235,228,496,0.99,1 1236,228,502,0.99,1 1237,228,508,0.99,1 1238,229,517,0.99,1 1239,229,526,0.99,1 1240,229,535,0.99,1 1241,229,544,0.99,1 1242,229,553,0.99,1 1243,229,562,0.99,1 1244,229,571,0.99,1 1245,229,580,0.99,1 1246,229,589,0.99,1 1247,229,598,0.99,1 1248,229,607,0.99,1 1249,229,616,0.99,1 1250,229,625,0.99,1 1251,229,634,0.99,1 1252,230,648,0.99,1 1253,231,649,0.99,1 1254,231,650,0.99,1 1255,232,652,0.99,1 1256,232,654,0.99,1 1257,233,656,0.99,1 1258,233,658,0.99,1 1259,233,660,0.99,1 1260,233,662,0.99,1 1261,234,666,0.99,1 1262,234,670,0.99,1 1263,234,674,0.99,1 1264,234,678,0.99,1 1265,234,682,0.99,1 1266,234,686,0.99,1 1267,235,692,0.99,1 1268,235,698,0.99,1 1269,235,704,0.99,1 1270,235,710,0.99,1 1271,235,716,0.99,1 1272,235,722,0.99,1 1273,235,728,0.99,1 1274,235,734,0.99,1 1275,235,740,0.99,1 1276,236,749,0.99,1 1277,236,758,0.99,1 1278,236,767,0.99,1 1279,236,776,0.99,1 1280,236,785,0.99,1 1281,236,794,0.99,1 1282,236,803,0.99,1 1283,236,812,0.99,1 1284,236,821,0.99,1 1285,236,830,0.99,1 1286,236,839,0.99,1 1287,236,848,0.99,1 1288,236,857,0.99,1 1289,236,866,0.99,1 1290,237,880,0.99,1 1291,238,881,0.99,1 1292,238,882,0.99,1 1293,239,884,0.99,1 1294,239,886,0.99,1 1295,240,888,0.99,1 1296,240,890,0.99,1 1297,240,892,0.99,1 1298,240,894,0.99,1 1299,241,898,0.99,1 1300,241,902,0.99,1 1301,241,906,0.99,1 1302,241,910,0.99,1 1303,241,914,0.99,1 1304,241,918,0.99,1 1305,242,924,0.99,1 1306,242,930,0.99,1 1307,242,936,0.99,1 1308,242,942,0.99,1 1309,242,948,0.99,1 1310,242,954,0.99,1 1311,242,960,0.99,1 1312,242,966,0.99,1 1313,242,972,0.99,1 1314,243,981,0.99,1 1315,243,990,0.99,1 1316,243,999,0.99,1 1317,243,1008,0.99,1 1318,243,1017,0.99,1 1319,243,1026,0.99,1 1320,243,1035,0.99,1 1321,243,1044,0.99,1 1322,243,1053,0.99,1 1323,243,1062,0.99,1 1324,243,1071,0.99,1 1325,243,1080,0.99,1 1326,243,1089,0.99,1 1327,243,1098,0.99,1 1328,244,1112,0.99,1 1329,245,1113,0.99,1 1330,245,1114,0.99,1 1331,246,1116,0.99,1 1332,246,1118,0.99,1 1333,247,1120,0.99,1 1334,247,1122,0.99,1 1335,247,1124,0.99,1 1336,247,1126,0.99,1 1337,248,1130,0.99,1 1338,248,1134,0.99,1 1339,248,1138,0.99,1 1340,248,1142,0.99,1 1341,248,1146,0.99,1 1342,248,1150,0.99,1 1343,249,1156,0.99,1 1344,249,1162,0.99,1 1345,249,1168,0.99,1 1346,249,1174,0.99,1 1347,249,1180,0.99,1 1348,249,1186,0.99,1 1349,249,1192,0.99,1 1350,249,1198,0.99,1 1351,249,1204,0.99,1 1352,250,1213,0.99,1 1353,250,1222,0.99,1 1354,250,1231,0.99,1 1355,250,1240,0.99,1 1356,250,1249,0.99,1 1357,250,1258,0.99,1 1358,250,1267,0.99,1 1359,250,1276,0.99,1 1360,250,1285,0.99,1 1361,250,1294,0.99,1 1362,250,1303,0.99,1 1363,250,1312,0.99,1 1364,250,1321,0.99,1 1365,250,1330,0.99,1 1366,251,1344,0.99,1 1367,252,1345,0.99,1 1368,252,1346,0.99,1 1369,253,1348,0.99,1 1370,253,1350,0.99,1 1371,254,1352,0.99,1 1372,254,1354,0.99,1 1373,254,1356,0.99,1 1374,254,1358,0.99,1 1375,255,1362,0.99,1 1376,255,1366,0.99,1 1377,255,1370,0.99,1 1378,255,1374,0.99,1 1379,255,1378,0.99,1 1380,255,1382,0.99,1 1381,256,1388,0.99,1 1382,256,1394,0.99,1 1383,256,1400,0.99,1 1384,256,1406,0.99,1 1385,256,1412,0.99,1 1386,256,1418,0.99,1 1387,256,1424,0.99,1 1388,256,1430,0.99,1 1389,256,1436,0.99,1 1390,257,1445,0.99,1 1391,257,1454,0.99,1 1392,257,1463,0.99,1 1393,257,1472,0.99,1 1394,257,1481,0.99,1 1395,257,1490,0.99,1 1396,257,1499,0.99,1 1397,257,1508,0.99,1 1398,257,1517,0.99,1 1399,257,1526,0.99,1 1400,257,1535,0.99,1 1401,257,1544,0.99,1 1402,257,1553,0.99,1 1403,257,1562,0.99,1 1404,258,1576,0.99,1 1405,259,1577,0.99,1 1406,259,1578,0.99,1 1407,260,1580,0.99,1 1408,260,1582,0.99,1 1409,261,1584,0.99,1 1410,261,1586,0.99,1 1411,261,1588,0.99,1 1412,261,1590,0.99,1 1413,262,1594,0.99,1 1414,262,1598,0.99,1 1415,262,1602,0.99,1 1416,262,1606,0.99,1 1417,262,1610,0.99,1 1418,262,1614,0.99,1 1419,263,1620,0.99,1 1420,263,1626,0.99,1 1421,263,1632,0.99,1 1422,263,1638,0.99,1 1423,263,1644,0.99,1 1424,263,1650,0.99,1 1425,263,1656,0.99,1 1426,263,1662,0.99,1 1427,263,1668,0.99,1 1428,264,1677,0.99,1 1429,264,1686,0.99,1 1430,264,1695,0.99,1 1431,264,1704,0.99,1 1432,264,1713,0.99,1 1433,264,1722,0.99,1 1434,264,1731,0.99,1 1435,264,1740,0.99,1 1436,264,1749,0.99,1 1437,264,1758,0.99,1 1438,264,1767,0.99,1 1439,264,1776,0.99,1 1440,264,1785,0.99,1 1441,264,1794,0.99,1 1442,265,1808,0.99,1 1443,266,1809,0.99,1 1444,266,1810,0.99,1 1445,267,1812,0.99,1 1446,267,1814,0.99,1 1447,268,1816,0.99,1 1448,268,1818,0.99,1 1449,268,1820,0.99,1 1450,268,1822,0.99,1 1451,269,1826,0.99,1 1452,269,1830,0.99,1 1453,269,1834,0.99,1 1454,269,1838,0.99,1 1455,269,1842,0.99,1 1456,269,1846,0.99,1 1457,270,1852,0.99,1 1458,270,1858,0.99,1 1459,270,1864,0.99,1 1460,270,1870,0.99,1 1461,270,1876,0.99,1 1462,270,1882,0.99,1 1463,270,1888,0.99,1 1464,270,1894,0.99,1 1465,270,1900,0.99,1 1466,271,1909,0.99,1 1467,271,1918,0.99,1 1468,271,1927,0.99,1 1469,271,1936,0.99,1 1470,271,1945,0.99,1 1471,271,1954,0.99,1 1472,271,1963,0.99,1 1473,271,1972,0.99,1 1474,271,1981,0.99,1 1475,271,1990,0.99,1 1476,271,1999,0.99,1 1477,271,2008,0.99,1 1478,271,2017,0.99,1 1479,271,2026,0.99,1 1480,272,2040,0.99,1 1481,273,2041,0.99,1 1482,273,2042,0.99,1 1483,274,2044,0.99,1 1484,274,2046,0.99,1 1485,275,2048,0.99,1 1486,275,2050,0.99,1 1487,275,2052,0.99,1 1488,275,2054,0.99,1 1489,276,2058,0.99,1 1490,276,2062,0.99,1 1491,276,2066,0.99,1 1492,276,2070,0.99,1 1493,276,2074,0.99,1 1494,276,2078,0.99,1 1495,277,2084,0.99,1 1496,277,2090,0.99,1 1497,277,2096,0.99,1 1498,277,2102,0.99,1 1499,277,2108,0.99,1 1500,277,2114,0.99,1 1501,277,2120,0.99,1 1502,277,2126,0.99,1 1503,277,2132,0.99,1 1504,278,2141,0.99,1 1505,278,2150,0.99,1 1506,278,2159,0.99,1 1507,278,2168,0.99,1 1508,278,2177,0.99,1 1509,278,2186,0.99,1 1510,278,2195,0.99,1 1511,278,2204,0.99,1 1512,278,2213,0.99,1 1513,278,2222,0.99,1 1514,278,2231,0.99,1 1515,278,2240,0.99,1 1516,278,2249,0.99,1 1517,278,2258,0.99,1 1518,279,2272,0.99,1 1519,280,2273,0.99,1 1520,280,2274,0.99,1 1521,281,2276,0.99,1 1522,281,2278,0.99,1 1523,282,2280,0.99,1 1524,282,2282,0.99,1 1525,282,2284,0.99,1 1526,282,2286,0.99,1 1527,283,2290,0.99,1 1528,283,2294,0.99,1 1529,283,2298,0.99,1 1530,283,2302,0.99,1 1531,283,2306,0.99,1 1532,283,2310,0.99,1 1533,284,2316,0.99,1 1534,284,2322,0.99,1 1535,284,2328,0.99,1 1536,284,2334,0.99,1 1537,284,2340,0.99,1 1538,284,2346,0.99,1 1539,284,2352,0.99,1 1540,284,2358,0.99,1 1541,284,2364,0.99,1 1542,285,2373,0.99,1 1543,285,2382,0.99,1 1544,285,2391,0.99,1 1545,285,2400,0.99,1 1546,285,2409,0.99,1 1547,285,2418,0.99,1 1548,285,2427,0.99,1 1549,285,2436,0.99,1 1550,285,2445,0.99,1 1551,285,2454,0.99,1 1552,285,2463,0.99,1 1553,285,2472,0.99,1 1554,285,2481,0.99,1 1555,285,2490,0.99,1 1556,286,2504,0.99,1 1557,287,2505,0.99,1 1558,287,2506,0.99,1 1559,288,2508,0.99,1 1560,288,2510,0.99,1 1561,289,2512,0.99,1 1562,289,2514,0.99,1 1563,289,2516,0.99,1 1564,289,2518,0.99,1 1565,290,2522,0.99,1 1566,290,2526,0.99,1 1567,290,2530,0.99,1 1568,290,2534,0.99,1 1569,290,2538,0.99,1 1570,290,2542,0.99,1 1571,291,2548,0.99,1 1572,291,2554,0.99,1 1573,291,2560,0.99,1 1574,291,2566,0.99,1 1575,291,2572,0.99,1 1576,291,2578,0.99,1 1577,291,2584,0.99,1 1578,291,2590,0.99,1 1579,291,2596,0.99,1 1580,292,2605,0.99,1 1581,292,2614,0.99,1 1582,292,2623,0.99,1 1583,292,2632,0.99,1 1584,292,2641,0.99,1 1585,292,2650,0.99,1 1586,292,2659,0.99,1 1587,292,2668,0.99,1 1588,292,2677,0.99,1 1589,292,2686,0.99,1 1590,292,2695,0.99,1 1591,292,2704,0.99,1 1592,292,2713,0.99,1 1593,292,2722,0.99,1 1594,293,2736,0.99,1 1595,294,2737,0.99,1 1596,294,2738,0.99,1 1597,295,2740,0.99,1 1598,295,2742,0.99,1 1599,296,2744,0.99,1 1600,296,2746,0.99,1 1601,296,2748,0.99,1 1602,296,2750,0.99,1 1603,297,2754,0.99,1 1604,297,2758,0.99,1 1605,297,2762,0.99,1 1606,297,2766,0.99,1 1607,297,2770,0.99,1 1608,297,2774,0.99,1 1609,298,2780,0.99,1 1610,298,2786,0.99,1 1611,298,2792,0.99,1 1612,298,2798,0.99,1 1613,298,2804,0.99,1 1614,298,2810,0.99,1 1615,298,2816,0.99,1 1616,298,2822,1.99,1 1617,298,2828,1.99,1 1618,299,2837,1.99,1 1619,299,2846,1.99,1 1620,299,2855,1.99,1 1621,299,2864,1.99,1 1622,299,2873,1.99,1 1623,299,2882,1.99,1 1624,299,2891,1.99,1 1625,299,2900,1.99,1 1626,299,2909,1.99,1 1627,299,2918,1.99,1 1628,299,2927,0.99,1 1629,299,2936,0.99,1 1630,299,2945,0.99,1 1631,299,2954,0.99,1 1632,300,2968,0.99,1 1633,301,2969,0.99,1 1634,301,2970,0.99,1 1635,302,2972,0.99,1 1636,302,2974,0.99,1 1637,303,2976,0.99,1 1638,303,2978,0.99,1 1639,303,2980,0.99,1 1640,303,2982,0.99,1 1641,304,2986,0.99,1 1642,304,2990,0.99,1 1643,304,2994,0.99,1 1644,304,2998,0.99,1 1645,304,3002,0.99,1 1646,304,3006,0.99,1 1647,305,3012,0.99,1 1648,305,3018,0.99,1 1649,305,3024,0.99,1 1650,305,3030,0.99,1 1651,305,3036,0.99,1 1652,305,3042,0.99,1 1653,305,3048,0.99,1 1654,305,3054,0.99,1 1655,305,3060,0.99,1 1656,306,3069,0.99,1 1657,306,3078,0.99,1 1658,306,3087,0.99,1 1659,306,3096,0.99,1 1660,306,3105,0.99,1 1661,306,3114,0.99,1 1662,306,3123,0.99,1 1663,306,3132,0.99,1 1664,306,3141,0.99,1 1665,306,3150,0.99,1 1666,306,3159,0.99,1 1667,306,3168,1.99,1 1668,306,3177,1.99,1 1669,306,3186,1.99,1 1670,307,3200,1.99,1 1671,308,3201,1.99,1 1672,308,3202,1.99,1 1673,309,3204,1.99,1 1674,309,3206,1.99,1 1675,310,3208,1.99,1 1676,310,3210,1.99,1 1677,310,3212,1.99,1 1678,310,3214,1.99,1 1679,311,3218,1.99,1 1680,311,3222,1.99,1 1681,311,3226,1.99,1 1682,311,3230,1.99,1 1683,311,3234,1.99,1 1684,311,3238,1.99,1 1685,312,3244,1.99,1 1686,312,3250,1.99,1 1687,312,3256,0.99,1 1688,312,3262,0.99,1 1689,312,3268,0.99,1 1690,312,3274,0.99,1 1691,312,3280,0.99,1 1692,312,3286,0.99,1 1693,312,3292,0.99,1 1694,313,3301,0.99,1 1695,313,3310,0.99,1 1696,313,3319,0.99,1 1697,313,3328,0.99,1 1698,313,3337,1.99,1 1699,313,3346,1.99,1 1700,313,3355,0.99,1 1701,313,3364,1.99,1 1702,313,3373,0.99,1 1703,313,3382,0.99,1 1704,313,3391,0.99,1 1705,313,3400,0.99,1 1706,313,3409,0.99,1 1707,313,3418,0.99,1 1708,314,3432,0.99,1 1709,315,3433,0.99,1 1710,315,3434,0.99,1 1711,316,3436,0.99,1 1712,316,3438,0.99,1 1713,317,3440,0.99,1 1714,317,3442,0.99,1 1715,317,3444,0.99,1 1716,317,3446,0.99,1 1717,318,3450,0.99,1 1718,318,3454,0.99,1 1719,318,3458,0.99,1 1720,318,3462,0.99,1 1721,318,3466,0.99,1 1722,318,3470,0.99,1 1723,319,3476,0.99,1 1724,319,3482,0.99,1 1725,319,3488,0.99,1 1726,319,3494,0.99,1 1727,319,3500,0.99,1 1728,319,3,0.99,1 1729,319,9,0.99,1 1730,319,15,0.99,1 1731,319,21,0.99,1 1732,320,30,0.99,1 1733,320,39,0.99,1 1734,320,48,0.99,1 1735,320,57,0.99,1 1736,320,66,0.99,1 1737,320,75,0.99,1 1738,320,84,0.99,1 1739,320,93,0.99,1 1740,320,102,0.99,1 1741,320,111,0.99,1 1742,320,120,0.99,1 1743,320,129,0.99,1 1744,320,138,0.99,1 1745,320,147,0.99,1 1746,321,161,0.99,1 1747,322,162,0.99,1 1748,322,163,0.99,1 1749,323,165,0.99,1 1750,323,167,0.99,1 1751,324,169,0.99,1 1752,324,171,0.99,1 1753,324,173,0.99,1 1754,324,175,0.99,1 1755,325,179,0.99,1 1756,325,183,0.99,1 1757,325,187,0.99,1 1758,325,191,0.99,1 1759,325,195,0.99,1 1760,325,199,0.99,1 1761,326,205,0.99,1 1762,326,211,0.99,1 1763,326,217,0.99,1 1764,326,223,0.99,1 1765,326,229,0.99,1 1766,326,235,0.99,1 1767,326,241,0.99,1 1768,326,247,0.99,1 1769,326,253,0.99,1 1770,327,262,0.99,1 1771,327,271,0.99,1 1772,327,280,0.99,1 1773,327,289,0.99,1 1774,327,298,0.99,1 1775,327,307,0.99,1 1776,327,316,0.99,1 1777,327,325,0.99,1 1778,327,334,0.99,1 1779,327,343,0.99,1 1780,327,352,0.99,1 1781,327,361,0.99,1 1782,327,370,0.99,1 1783,327,379,0.99,1 1784,328,393,0.99,1 1785,329,394,0.99,1 1786,329,395,0.99,1 1787,330,397,0.99,1 1788,330,399,0.99,1 1789,331,401,0.99,1 1790,331,403,0.99,1 1791,331,405,0.99,1 1792,331,407,0.99,1 1793,332,411,0.99,1 1794,332,415,0.99,1 1795,332,419,0.99,1 1796,332,423,0.99,1 1797,332,427,0.99,1 1798,332,431,0.99,1 1799,333,437,0.99,1 1800,333,443,0.99,1 1801,333,449,0.99,1 1802,333,455,0.99,1 1803,333,461,0.99,1 1804,333,467,0.99,1 1805,333,473,0.99,1 1806,333,479,0.99,1 1807,333,485,0.99,1 1808,334,494,0.99,1 1809,334,503,0.99,1 1810,334,512,0.99,1 1811,334,521,0.99,1 1812,334,530,0.99,1 1813,334,539,0.99,1 1814,334,548,0.99,1 1815,334,557,0.99,1 1816,334,566,0.99,1 1817,334,575,0.99,1 1818,334,584,0.99,1 1819,334,593,0.99,1 1820,334,602,0.99,1 1821,334,611,0.99,1 1822,335,625,0.99,1 1823,336,626,0.99,1 1824,336,627,0.99,1 1825,337,629,0.99,1 1826,337,631,0.99,1 1827,338,633,0.99,1 1828,338,635,0.99,1 1829,338,637,0.99,1 1830,338,639,0.99,1 1831,339,643,0.99,1 1832,339,647,0.99,1 1833,339,651,0.99,1 1834,339,655,0.99,1 1835,339,659,0.99,1 1836,339,663,0.99,1 1837,340,669,0.99,1 1838,340,675,0.99,1 1839,340,681,0.99,1 1840,340,687,0.99,1 1841,340,693,0.99,1 1842,340,699,0.99,1 1843,340,705,0.99,1 1844,340,711,0.99,1 1845,340,717,0.99,1 1846,341,726,0.99,1 1847,341,735,0.99,1 1848,341,744,0.99,1 1849,341,753,0.99,1 1850,341,762,0.99,1 1851,341,771,0.99,1 1852,341,780,0.99,1 1853,341,789,0.99,1 1854,341,798,0.99,1 1855,341,807,0.99,1 1856,341,816,0.99,1 1857,341,825,0.99,1 1858,341,834,0.99,1 1859,341,843,0.99,1 1860,342,857,0.99,1 1861,343,858,0.99,1 1862,343,859,0.99,1 1863,344,861,0.99,1 1864,344,863,0.99,1 1865,345,865,0.99,1 1866,345,867,0.99,1 1867,345,869,0.99,1 1868,345,871,0.99,1 1869,346,875,0.99,1 1870,346,879,0.99,1 1871,346,883,0.99,1 1872,346,887,0.99,1 1873,346,891,0.99,1 1874,346,895,0.99,1 1875,347,901,0.99,1 1876,347,907,0.99,1 1877,347,913,0.99,1 1878,347,919,0.99,1 1879,347,925,0.99,1 1880,347,931,0.99,1 1881,347,937,0.99,1 1882,347,943,0.99,1 1883,347,949,0.99,1 1884,348,958,0.99,1 1885,348,967,0.99,1 1886,348,976,0.99,1 1887,348,985,0.99,1 1888,348,994,0.99,1 1889,348,1003,0.99,1 1890,348,1012,0.99,1 1891,348,1021,0.99,1 1892,348,1030,0.99,1 1893,348,1039,0.99,1 1894,348,1048,0.99,1 1895,348,1057,0.99,1 1896,348,1066,0.99,1 1897,348,1075,0.99,1 1898,349,1089,0.99,1 1899,350,1090,0.99,1 1900,350,1091,0.99,1 1901,351,1093,0.99,1 1902,351,1095,0.99,1 1903,352,1097,0.99,1 1904,352,1099,0.99,1 1905,352,1101,0.99,1 1906,352,1103,0.99,1 1907,353,1107,0.99,1 1908,353,1111,0.99,1 1909,353,1115,0.99,1 1910,353,1119,0.99,1 1911,353,1123,0.99,1 1912,353,1127,0.99,1 1913,354,1133,0.99,1 1914,354,1139,0.99,1 1915,354,1145,0.99,1 1916,354,1151,0.99,1 1917,354,1157,0.99,1 1918,354,1163,0.99,1 1919,354,1169,0.99,1 1920,354,1175,0.99,1 1921,354,1181,0.99,1 1922,355,1190,0.99,1 1923,355,1199,0.99,1 1924,355,1208,0.99,1 1925,355,1217,0.99,1 1926,355,1226,0.99,1 1927,355,1235,0.99,1 1928,355,1244,0.99,1 1929,355,1253,0.99,1 1930,355,1262,0.99,1 1931,355,1271,0.99,1 1932,355,1280,0.99,1 1933,355,1289,0.99,1 1934,355,1298,0.99,1 1935,355,1307,0.99,1 1936,356,1321,0.99,1 1937,357,1322,0.99,1 1938,357,1323,0.99,1 1939,358,1325,0.99,1 1940,358,1327,0.99,1 1941,359,1329,0.99,1 1942,359,1331,0.99,1 1943,359,1333,0.99,1 1944,359,1335,0.99,1 1945,360,1339,0.99,1 1946,360,1343,0.99,1 1947,360,1347,0.99,1 1948,360,1351,0.99,1 1949,360,1355,0.99,1 1950,360,1359,0.99,1 1951,361,1365,0.99,1 1952,361,1371,0.99,1 1953,361,1377,0.99,1 1954,361,1383,0.99,1 1955,361,1389,0.99,1 1956,361,1395,0.99,1 1957,361,1401,0.99,1 1958,361,1407,0.99,1 1959,361,1413,0.99,1 1960,362,1422,0.99,1 1961,362,1431,0.99,1 1962,362,1440,0.99,1 1963,362,1449,0.99,1 1964,362,1458,0.99,1 1965,362,1467,0.99,1 1966,362,1476,0.99,1 1967,362,1485,0.99,1 1968,362,1494,0.99,1 1969,362,1503,0.99,1 1970,362,1512,0.99,1 1971,362,1521,0.99,1 1972,362,1530,0.99,1 1973,362,1539,0.99,1 1974,363,1553,0.99,1 1975,364,1554,0.99,1 1976,364,1555,0.99,1 1977,365,1557,0.99,1 1978,365,1559,0.99,1 1979,366,1561,0.99,1 1980,366,1563,0.99,1 1981,366,1565,0.99,1 1982,366,1567,0.99,1 1983,367,1571,0.99,1 1984,367,1575,0.99,1 1985,367,1579,0.99,1 1986,367,1583,0.99,1 1987,367,1587,0.99,1 1988,367,1591,0.99,1 1989,368,1597,0.99,1 1990,368,1603,0.99,1 1991,368,1609,0.99,1 1992,368,1615,0.99,1 1993,368,1621,0.99,1 1994,368,1627,0.99,1 1995,368,1633,0.99,1 1996,368,1639,0.99,1 1997,368,1645,0.99,1 1998,369,1654,0.99,1 1999,369,1663,0.99,1 2000,369,1672,0.99,1 2001,369,1681,0.99,1 2002,369,1690,0.99,1 2003,369,1699,0.99,1 2004,369,1708,0.99,1 2005,369,1717,0.99,1 2006,369,1726,0.99,1 2007,369,1735,0.99,1 2008,369,1744,0.99,1 2009,369,1753,0.99,1 2010,369,1762,0.99,1 2011,369,1771,0.99,1 2012,370,1785,0.99,1 2013,371,1786,0.99,1 2014,371,1787,0.99,1 2015,372,1789,0.99,1 2016,372,1791,0.99,1 2017,373,1793,0.99,1 2018,373,1795,0.99,1 2019,373,1797,0.99,1 2020,373,1799,0.99,1 2021,374,1803,0.99,1 2022,374,1807,0.99,1 2023,374,1811,0.99,1 2024,374,1815,0.99,1 2025,374,1819,0.99,1 2026,374,1823,0.99,1 2027,375,1829,0.99,1 2028,375,1835,0.99,1 2029,375,1841,0.99,1 2030,375,1847,0.99,1 2031,375,1853,0.99,1 2032,375,1859,0.99,1 2033,375,1865,0.99,1 2034,375,1871,0.99,1 2035,375,1877,0.99,1 2036,376,1886,0.99,1 2037,376,1895,0.99,1 2038,376,1904,0.99,1 2039,376,1913,0.99,1 2040,376,1922,0.99,1 2041,376,1931,0.99,1 2042,376,1940,0.99,1 2043,376,1949,0.99,1 2044,376,1958,0.99,1 2045,376,1967,0.99,1 2046,376,1976,0.99,1 2047,376,1985,0.99,1 2048,376,1994,0.99,1 2049,376,2003,0.99,1 2050,377,2017,0.99,1 2051,378,2018,0.99,1 2052,378,2019,0.99,1 2053,379,2021,0.99,1 2054,379,2023,0.99,1 2055,380,2025,0.99,1 2056,380,2027,0.99,1 2057,380,2029,0.99,1 2058,380,2031,0.99,1 2059,381,2035,0.99,1 2060,381,2039,0.99,1 2061,381,2043,0.99,1 2062,381,2047,0.99,1 2063,381,2051,0.99,1 2064,381,2055,0.99,1 2065,382,2061,0.99,1 2066,382,2067,0.99,1 2067,382,2073,0.99,1 2068,382,2079,0.99,1 2069,382,2085,0.99,1 2070,382,2091,0.99,1 2071,382,2097,0.99,1 2072,382,2103,0.99,1 2073,382,2109,0.99,1 2074,383,2118,0.99,1 2075,383,2127,0.99,1 2076,383,2136,0.99,1 2077,383,2145,0.99,1 2078,383,2154,0.99,1 2079,383,2163,0.99,1 2080,383,2172,0.99,1 2081,383,2181,0.99,1 2082,383,2190,0.99,1 2083,383,2199,0.99,1 2084,383,2208,0.99,1 2085,383,2217,0.99,1 2086,383,2226,0.99,1 2087,383,2235,0.99,1 2088,384,2249,0.99,1 2089,385,2250,0.99,1 2090,385,2251,0.99,1 2091,386,2253,0.99,1 2092,386,2255,0.99,1 2093,387,2257,0.99,1 2094,387,2259,0.99,1 2095,387,2261,0.99,1 2096,387,2263,0.99,1 2097,388,2267,0.99,1 2098,388,2271,0.99,1 2099,388,2275,0.99,1 2100,388,2279,0.99,1 2101,388,2283,0.99,1 2102,388,2287,0.99,1 2103,389,2293,0.99,1 2104,389,2299,0.99,1 2105,389,2305,0.99,1 2106,389,2311,0.99,1 2107,389,2317,0.99,1 2108,389,2323,0.99,1 2109,389,2329,0.99,1 2110,389,2335,0.99,1 2111,389,2341,0.99,1 2112,390,2350,0.99,1 2113,390,2359,0.99,1 2114,390,2368,0.99,1 2115,390,2377,0.99,1 2116,390,2386,0.99,1 2117,390,2395,0.99,1 2118,390,2404,0.99,1 2119,390,2413,0.99,1 2120,390,2422,0.99,1 2121,390,2431,0.99,1 2122,390,2440,0.99,1 2123,390,2449,0.99,1 2124,390,2458,0.99,1 2125,390,2467,0.99,1 2126,391,2481,0.99,1 2127,392,2482,0.99,1 2128,392,2483,0.99,1 2129,393,2485,0.99,1 2130,393,2487,0.99,1 2131,394,2489,0.99,1 2132,394,2491,0.99,1 2133,394,2493,0.99,1 2134,394,2495,0.99,1 2135,395,2499,0.99,1 2136,395,2503,0.99,1 2137,395,2507,0.99,1 2138,395,2511,0.99,1 2139,395,2515,0.99,1 2140,395,2519,0.99,1 2141,396,2525,0.99,1 2142,396,2531,0.99,1 2143,396,2537,0.99,1 2144,396,2543,0.99,1 2145,396,2549,0.99,1 2146,396,2555,0.99,1 2147,396,2561,0.99,1 2148,396,2567,0.99,1 2149,396,2573,0.99,1 2150,397,2582,0.99,1 2151,397,2591,0.99,1 2152,397,2600,0.99,1 2153,397,2609,0.99,1 2154,397,2618,0.99,1 2155,397,2627,0.99,1 2156,397,2636,0.99,1 2157,397,2645,0.99,1 2158,397,2654,0.99,1 2159,397,2663,0.99,1 2160,397,2672,0.99,1 2161,397,2681,0.99,1 2162,397,2690,0.99,1 2163,397,2699,0.99,1 2164,398,2713,0.99,1 2165,399,2714,0.99,1 2166,399,2715,0.99,1 2167,400,2717,0.99,1 2168,400,2719,0.99,1 2169,401,2721,0.99,1 2170,401,2723,0.99,1 2171,401,2725,0.99,1 2172,401,2727,0.99,1 2173,402,2731,0.99,1 2174,402,2735,0.99,1 2175,402,2739,0.99,1 2176,402,2743,0.99,1 2177,402,2747,0.99,1 2178,402,2751,0.99,1 2179,403,2757,0.99,1 2180,403,2763,0.99,1 2181,403,2769,0.99,1 2182,403,2775,0.99,1 2183,403,2781,0.99,1 2184,403,2787,0.99,1 2185,403,2793,0.99,1 2186,403,2799,0.99,1 2187,403,2805,0.99,1 2188,404,2814,0.99,1 2189,404,2823,1.99,1 2190,404,2832,1.99,1 2191,404,2841,1.99,1 2192,404,2850,1.99,1 2193,404,2859,1.99,1 2194,404,2868,1.99,1 2195,404,2877,1.99,1 2196,404,2886,1.99,1 2197,404,2895,1.99,1 2198,404,2904,1.99,1 2199,404,2913,1.99,1 2200,404,2922,1.99,1 2201,404,2931,0.99,1 2202,405,2945,0.99,1 2203,406,2946,0.99,1 2204,406,2947,0.99,1 2205,407,2949,0.99,1 2206,407,2951,0.99,1 2207,408,2953,0.99,1 2208,408,2955,0.99,1 2209,408,2957,0.99,1 2210,408,2959,0.99,1 2211,409,2963,0.99,1 2212,409,2967,0.99,1 2213,409,2971,0.99,1 2214,409,2975,0.99,1 2215,409,2979,0.99,1 2216,409,2983,0.99,1 2217,410,2989,0.99,1 2218,410,2995,0.99,1 2219,410,3001,0.99,1 2220,410,3007,0.99,1 2221,410,3013,0.99,1 2222,410,3019,0.99,1 2223,410,3025,0.99,1 2224,410,3031,0.99,1 2225,410,3037,0.99,1 2226,411,3046,0.99,1 2227,411,3055,0.99,1 2228,411,3064,0.99,1 2229,411,3073,0.99,1 2230,411,3082,0.99,1 2231,411,3091,0.99,1 2232,411,3100,0.99,1 2233,411,3109,0.99,1 2234,411,3118,0.99,1 2235,411,3127,0.99,1 2236,411,3136,0.99,1 2237,411,3145,0.99,1 2238,411,3154,0.99,1 2239,411,3163,0.99,1 2240,412,3177,1.99,1 ================================================ FILE: prqlc/prqlc/tests/integration/data/chinook/invoices.csv ================================================ invoice_id,customer_id,invoice_date,billing_address,billing_city,billing_state,billing_country,billing_postal_code,total 1,2,2009-01-01T00:00:00.000000000,Theodor-Heuss-Straße 34,Stuttgart,,Germany,70174,1.98 2,4,2009-01-02T00:00:00.000000000,Ullevålsveien 14,Oslo,,Norway,0171,3.96 3,8,2009-01-03T00:00:00.000000000,Grétrystraat 63,Brussels,,Belgium,1000,5.94 4,14,2009-01-06T00:00:00.000000000,8210 111 ST NW,Edmonton,AB,Canada,T6G 2C7,8.91 5,23,2009-01-11T00:00:00.000000000,69 Salem Street,Boston,MA,USA,2113,13.86 6,37,2009-01-19T00:00:00.000000000,Berger Straße 10,Frankfurt,,Germany,60316,0.99 7,38,2009-02-01T00:00:00.000000000,Barbarossastraße 19,Berlin,,Germany,10779,1.98 8,40,2009-02-01T00:00:00.000000000,"8, Rue Hanovre",Paris,,France,75002,1.98 9,42,2009-02-02T00:00:00.000000000,"9, Place Louis Barthou",Bordeaux,,France,33000,3.96 10,46,2009-02-03T00:00:00.000000000,3 Chatham Street,Dublin,Dublin,Ireland,,5.94 11,52,2009-02-06T00:00:00.000000000,202 Hoxton Street,London,,United Kingdom,N1 5LH,8.91 12,2,2009-02-11T00:00:00.000000000,Theodor-Heuss-Straße 34,Stuttgart,,Germany,70174,13.86 13,16,2009-02-19T00:00:00.000000000,1600 Amphitheatre Parkway,Mountain View,CA,USA,94043-1351,0.99 14,17,2009-03-04T00:00:00.000000000,1 Microsoft Way,Redmond,WA,USA,98052-8300,1.98 15,19,2009-03-04T00:00:00.000000000,1 Infinite Loop,Cupertino,CA,USA,95014,1.98 16,21,2009-03-05T00:00:00.000000000,801 W 4th Street,Reno,NV,USA,89503,3.96 17,25,2009-03-06T00:00:00.000000000,319 N. Frances Street,Madison,WI,USA,53703,5.94 18,31,2009-03-09T00:00:00.000000000,194A Chain Lake Drive,Halifax,NS,Canada,B3S 1C5,8.91 19,40,2009-03-14T00:00:00.000000000,"8, Rue Hanovre",Paris,,France,75002,13.86 20,54,2009-03-22T00:00:00.000000000,110 Raeburn Pl,Edinburgh ,,United Kingdom,EH4 1HH,0.99 21,55,2009-04-04T00:00:00.000000000,421 Bourke Street,Sidney,NSW,Australia,2010,1.98 22,57,2009-04-04T00:00:00.000000000,"Calle Lira, 198",Santiago,,Chile,,1.98 23,59,2009-04-05T00:00:00.000000000,"3,Raj Bhavan Road",Bangalore,,India,560001,3.96 24,4,2009-04-06T00:00:00.000000000,Ullevålsveien 14,Oslo,,Norway,0171,5.94 25,10,2009-04-09T00:00:00.000000000,"Rua Dr. Falcão Filho, 155",São Paulo,SP,Brazil,01007-010,8.91 26,19,2009-04-14T00:00:00.000000000,1 Infinite Loop,Cupertino,CA,USA,95014,13.86 27,33,2009-04-22T00:00:00.000000000,5112 48 Street,Yellowknife,NT,Canada,X1A 1N6,0.99 28,34,2009-05-05T00:00:00.000000000,Rua da Assunção 53,Lisbon,,Portugal,,1.98 29,36,2009-05-05T00:00:00.000000000,Tauentzienstraße 8,Berlin,,Germany,10789,1.98 30,38,2009-05-06T00:00:00.000000000,Barbarossastraße 19,Berlin,,Germany,10779,3.96 31,42,2009-05-07T00:00:00.000000000,"9, Place Louis Barthou",Bordeaux,,France,33000,5.94 32,48,2009-05-10T00:00:00.000000000,Lijnbaansgracht 120bg,Amsterdam,VV,Netherlands,1016,8.91 33,57,2009-05-15T00:00:00.000000000,"Calle Lira, 198",Santiago,,Chile,,13.86 34,12,2009-05-23T00:00:00.000000000,"Praça Pio X, 119",Rio de Janeiro,RJ,Brazil,20040-020,0.99 35,13,2009-06-05T00:00:00.000000000,Qe 7 Bloco G,Brasília,DF,Brazil,71020-677,1.98 36,15,2009-06-05T00:00:00.000000000,700 W Pender Street,Vancouver,BC,Canada,V6C 1G8,1.98 37,17,2009-06-06T00:00:00.000000000,1 Microsoft Way,Redmond,WA,USA,98052-8300,3.96 38,21,2009-06-07T00:00:00.000000000,801 W 4th Street,Reno,NV,USA,89503,5.94 39,27,2009-06-10T00:00:00.000000000,1033 N Park Ave,Tucson,AZ,USA,85719,8.91 40,36,2009-06-15T00:00:00.000000000,Tauentzienstraße 8,Berlin,,Germany,10789,13.86 41,50,2009-06-23T00:00:00.000000000,C/ San Bernardo 85,Madrid,,Spain,28015,0.99 42,51,2009-07-06T00:00:00.000000000,Celsiusg. 9,Stockholm,,Sweden,11230,1.98 43,53,2009-07-06T00:00:00.000000000,113 Lupus St,London,,United Kingdom,SW1V 3EN,1.98 44,55,2009-07-07T00:00:00.000000000,421 Bourke Street,Sidney,NSW,Australia,2010,3.96 45,59,2009-07-08T00:00:00.000000000,"3,Raj Bhavan Road",Bangalore,,India,560001,5.94 46,6,2009-07-11T00:00:00.000000000,Rilská 3174/6,Prague,,Czech Republic,14300,8.91 47,15,2009-07-16T00:00:00.000000000,700 W Pender Street,Vancouver,BC,Canada,V6C 1G8,13.86 48,29,2009-07-24T00:00:00.000000000,796 Dundas Street West,Toronto,ON,Canada,M6J 1V1,0.99 49,30,2009-08-06T00:00:00.000000000,230 Elgin Street,Ottawa,ON,Canada,K2P 1L7,1.98 50,32,2009-08-06T00:00:00.000000000,696 Osborne Street,Winnipeg,MB,Canada,R3L 2B9,1.98 51,34,2009-08-07T00:00:00.000000000,Rua da Assunção 53,Lisbon,,Portugal,,3.96 52,38,2009-08-08T00:00:00.000000000,Barbarossastraße 19,Berlin,,Germany,10779,5.94 53,44,2009-08-11T00:00:00.000000000,Porthaninkatu 9,Helsinki,,Finland,00530,8.91 54,53,2009-08-16T00:00:00.000000000,113 Lupus St,London,,United Kingdom,SW1V 3EN,13.86 55,8,2009-08-24T00:00:00.000000000,Grétrystraat 63,Brussels,,Belgium,1000,0.99 56,9,2009-09-06T00:00:00.000000000,Sønder Boulevard 51,Copenhagen,,Denmark,1720,1.98 57,11,2009-09-06T00:00:00.000000000,"Av. Paulista, 2022",São Paulo,SP,Brazil,01310-200,1.98 58,13,2009-09-07T00:00:00.000000000,Qe 7 Bloco G,Brasília,DF,Brazil,71020-677,3.96 59,17,2009-09-08T00:00:00.000000000,1 Microsoft Way,Redmond,WA,USA,98052-8300,5.94 60,23,2009-09-11T00:00:00.000000000,69 Salem Street,Boston,MA,USA,2113,8.91 61,32,2009-09-16T00:00:00.000000000,696 Osborne Street,Winnipeg,MB,Canada,R3L 2B9,13.86 62,46,2009-09-24T00:00:00.000000000,3 Chatham Street,Dublin,Dublin,Ireland,,0.99 63,47,2009-10-07T00:00:00.000000000,"Via Degli Scipioni, 43",Rome,RM,Italy,00192,1.98 64,49,2009-10-07T00:00:00.000000000,Ordynacka 10,Warsaw,,Poland,00-358,1.98 65,51,2009-10-08T00:00:00.000000000,Celsiusg. 9,Stockholm,,Sweden,11230,3.96 66,55,2009-10-09T00:00:00.000000000,421 Bourke Street,Sidney,NSW,Australia,2010,5.94 67,2,2009-10-12T00:00:00.000000000,Theodor-Heuss-Straße 34,Stuttgart,,Germany,70174,8.91 68,11,2009-10-17T00:00:00.000000000,"Av. Paulista, 2022",São Paulo,SP,Brazil,01310-200,13.86 69,25,2009-10-25T00:00:00.000000000,319 N. Frances Street,Madison,WI,USA,53703,0.99 70,26,2009-11-07T00:00:00.000000000,2211 W Berry Street,Fort Worth,TX,USA,76110,1.98 71,28,2009-11-07T00:00:00.000000000,302 S 700 E,Salt Lake City,UT,USA,84102,1.98 72,30,2009-11-08T00:00:00.000000000,230 Elgin Street,Ottawa,ON,Canada,K2P 1L7,3.96 73,34,2009-11-09T00:00:00.000000000,Rua da Assunção 53,Lisbon,,Portugal,,5.94 74,40,2009-11-12T00:00:00.000000000,"8, Rue Hanovre",Paris,,France,75002,8.91 75,49,2009-11-17T00:00:00.000000000,Ordynacka 10,Warsaw,,Poland,00-358,13.86 76,4,2009-11-25T00:00:00.000000000,Ullevålsveien 14,Oslo,,Norway,0171,0.99 77,5,2009-12-08T00:00:00.000000000,Klanova 9/506,Prague,,Czech Republic,14700,1.98 78,7,2009-12-08T00:00:00.000000000,"Rotenturmstraße 4, 1010 Innere Stadt",Vienne,,Austria,1010,1.98 79,9,2009-12-09T00:00:00.000000000,Sønder Boulevard 51,Copenhagen,,Denmark,1720,3.96 80,13,2009-12-10T00:00:00.000000000,Qe 7 Bloco G,Brasília,DF,Brazil,71020-677,5.94 81,19,2009-12-13T00:00:00.000000000,1 Infinite Loop,Cupertino,CA,USA,95014,8.91 82,28,2009-12-18T00:00:00.000000000,302 S 700 E,Salt Lake City,UT,USA,84102,13.86 83,42,2009-12-26T00:00:00.000000000,"9, Place Louis Barthou",Bordeaux,,France,33000,0.99 84,43,2010-01-08T00:00:00.000000000,"68, Rue Jouvence",Dijon,,France,21000,1.98 85,45,2010-01-08T00:00:00.000000000,Erzsébet krt. 58.,Budapest,,Hungary,H-1073,1.98 86,47,2010-01-09T00:00:00.000000000,"Via Degli Scipioni, 43",Rome,RM,Italy,00192,3.96 87,51,2010-01-10T00:00:00.000000000,Celsiusg. 9,Stockholm,,Sweden,11230,6.94 88,57,2010-01-13T00:00:00.000000000,"Calle Lira, 198",Santiago,,Chile,,17.91 89,7,2010-01-18T00:00:00.000000000,"Rotenturmstraße 4, 1010 Innere Stadt",Vienne,,Austria,1010,18.86 90,21,2010-01-26T00:00:00.000000000,801 W 4th Street,Reno,NV,USA,89503,0.99 91,22,2010-02-08T00:00:00.000000000,120 S Orange Ave,Orlando,FL,USA,32801,1.98 92,24,2010-02-08T00:00:00.000000000,162 E Superior Street,Chicago,IL,USA,60611,1.98 93,26,2010-02-09T00:00:00.000000000,2211 W Berry Street,Fort Worth,TX,USA,76110,3.96 94,30,2010-02-10T00:00:00.000000000,230 Elgin Street,Ottawa,ON,Canada,K2P 1L7,5.94 95,36,2010-02-13T00:00:00.000000000,Tauentzienstraße 8,Berlin,,Germany,10789,8.91 96,45,2010-02-18T00:00:00.000000000,Erzsébet krt. 58.,Budapest,,Hungary,H-1073,21.86 97,59,2010-02-26T00:00:00.000000000,"3,Raj Bhavan Road",Bangalore,,India,560001,1.99 98,1,2010-03-11T00:00:00.000000000,"Av. Brigadeiro Faria Lima, 2170",São José dos Campos,SP,Brazil,12227-000,3.98 99,3,2010-03-11T00:00:00.000000000,1498 rue Bélanger,Montréal,QC,Canada,H2G 1A7,3.98 100,5,2010-03-12T00:00:00.000000000,Klanova 9/506,Prague,,Czech Republic,14700,3.96 101,9,2010-03-13T00:00:00.000000000,Sønder Boulevard 51,Copenhagen,,Denmark,1720,5.94 102,15,2010-03-16T00:00:00.000000000,700 W Pender Street,Vancouver,BC,Canada,V6C 1G8,9.91 103,24,2010-03-21T00:00:00.000000000,162 E Superior Street,Chicago,IL,USA,60611,15.86 104,38,2010-03-29T00:00:00.000000000,Barbarossastraße 19,Berlin,,Germany,10779,0.99 105,39,2010-04-11T00:00:00.000000000,"4, Rue Milton",Paris,,France,75009,1.98 106,41,2010-04-11T00:00:00.000000000,"11, Place Bellecour",Lyon,,France,69002,1.98 107,43,2010-04-12T00:00:00.000000000,"68, Rue Jouvence",Dijon,,France,21000,3.96 108,47,2010-04-13T00:00:00.000000000,"Via Degli Scipioni, 43",Rome,RM,Italy,00192,5.94 109,53,2010-04-16T00:00:00.000000000,113 Lupus St,London,,United Kingdom,SW1V 3EN,8.91 110,3,2010-04-21T00:00:00.000000000,1498 rue Bélanger,Montréal,QC,Canada,H2G 1A7,13.86 111,17,2010-04-29T00:00:00.000000000,1 Microsoft Way,Redmond,WA,USA,98052-8300,0.99 112,18,2010-05-12T00:00:00.000000000,627 Broadway,New York,NY,USA,10012-2612,1.98 113,20,2010-05-12T00:00:00.000000000,541 Del Medio Avenue,Mountain View,CA,USA,94040-111,1.98 114,22,2010-05-13T00:00:00.000000000,120 S Orange Ave,Orlando,FL,USA,32801,3.96 115,26,2010-05-14T00:00:00.000000000,2211 W Berry Street,Fort Worth,TX,USA,76110,5.94 116,32,2010-05-17T00:00:00.000000000,696 Osborne Street,Winnipeg,MB,Canada,R3L 2B9,8.91 117,41,2010-05-22T00:00:00.000000000,"11, Place Bellecour",Lyon,,France,69002,13.86 118,55,2010-05-30T00:00:00.000000000,421 Bourke Street,Sidney,NSW,Australia,2010,0.99 119,56,2010-06-12T00:00:00.000000000,307 Macacha Güemes,Buenos Aires,,Argentina,1106,1.98 120,58,2010-06-12T00:00:00.000000000,"12,Community Centre",Delhi,,India,110017,1.98 121,1,2010-06-13T00:00:00.000000000,"Av. Brigadeiro Faria Lima, 2170",São José dos Campos,SP,Brazil,12227-000,3.96 122,5,2010-06-14T00:00:00.000000000,Klanova 9/506,Prague,,Czech Republic,14700,5.94 123,11,2010-06-17T00:00:00.000000000,"Av. Paulista, 2022",São Paulo,SP,Brazil,01310-200,8.91 124,20,2010-06-22T00:00:00.000000000,541 Del Medio Avenue,Mountain View,CA,USA,94040-111,13.86 125,34,2010-06-30T00:00:00.000000000,Rua da Assunção 53,Lisbon,,Portugal,,0.99 126,35,2010-07-13T00:00:00.000000000,"Rua dos Campeões Europeus de Viena, 4350",Porto,,Portugal,,1.98 127,37,2010-07-13T00:00:00.000000000,Berger Straße 10,Frankfurt,,Germany,60316,1.98 128,39,2010-07-14T00:00:00.000000000,"4, Rue Milton",Paris,,France,75009,3.96 129,43,2010-07-15T00:00:00.000000000,"68, Rue Jouvence",Dijon,,France,21000,5.94 130,49,2010-07-18T00:00:00.000000000,Ordynacka 10,Warsaw,,Poland,00-358,8.91 131,58,2010-07-23T00:00:00.000000000,"12,Community Centre",Delhi,,India,110017,13.86 132,13,2010-07-31T00:00:00.000000000,Qe 7 Bloco G,Brasília,DF,Brazil,71020-677,0.99 133,14,2010-08-13T00:00:00.000000000,8210 111 ST NW,Edmonton,AB,Canada,T6G 2C7,1.98 134,16,2010-08-13T00:00:00.000000000,1600 Amphitheatre Parkway,Mountain View,CA,USA,94043-1351,1.98 135,18,2010-08-14T00:00:00.000000000,627 Broadway,New York,NY,USA,10012-2612,3.96 136,22,2010-08-15T00:00:00.000000000,120 S Orange Ave,Orlando,FL,USA,32801,5.94 137,28,2010-08-18T00:00:00.000000000,302 S 700 E,Salt Lake City,UT,USA,84102,8.91 138,37,2010-08-23T00:00:00.000000000,Berger Straße 10,Frankfurt,,Germany,60316,13.86 139,51,2010-08-31T00:00:00.000000000,Celsiusg. 9,Stockholm,,Sweden,11230,0.99 140,52,2010-09-13T00:00:00.000000000,202 Hoxton Street,London,,United Kingdom,N1 5LH,1.98 141,54,2010-09-13T00:00:00.000000000,110 Raeburn Pl,Edinburgh ,,United Kingdom,EH4 1HH,1.98 142,56,2010-09-14T00:00:00.000000000,307 Macacha Güemes,Buenos Aires,,Argentina,1106,3.96 143,1,2010-09-15T00:00:00.000000000,"Av. Brigadeiro Faria Lima, 2170",São José dos Campos,SP,Brazil,12227-000,5.94 144,7,2010-09-18T00:00:00.000000000,"Rotenturmstraße 4, 1010 Innere Stadt",Vienne,,Austria,1010,8.91 145,16,2010-09-23T00:00:00.000000000,1600 Amphitheatre Parkway,Mountain View,CA,USA,94043-1351,13.86 146,30,2010-10-01T00:00:00.000000000,230 Elgin Street,Ottawa,ON,Canada,K2P 1L7,0.99 147,31,2010-10-14T00:00:00.000000000,194A Chain Lake Drive,Halifax,NS,Canada,B3S 1C5,1.98 148,33,2010-10-14T00:00:00.000000000,5112 48 Street,Yellowknife,NT,Canada,X1A 1N6,1.98 149,35,2010-10-15T00:00:00.000000000,"Rua dos Campeões Europeus de Viena, 4350",Porto,,Portugal,,3.96 150,39,2010-10-16T00:00:00.000000000,"4, Rue Milton",Paris,,France,75009,5.94 151,45,2010-10-19T00:00:00.000000000,Erzsébet krt. 58.,Budapest,,Hungary,H-1073,8.91 152,54,2010-10-24T00:00:00.000000000,110 Raeburn Pl,Edinburgh ,,United Kingdom,EH4 1HH,13.86 153,9,2010-11-01T00:00:00.000000000,Sønder Boulevard 51,Copenhagen,,Denmark,1720,0.99 154,10,2010-11-14T00:00:00.000000000,"Rua Dr. Falcão Filho, 155",São Paulo,SP,Brazil,01007-010,1.98 155,12,2010-11-14T00:00:00.000000000,"Praça Pio X, 119",Rio de Janeiro,RJ,Brazil,20040-020,1.98 156,14,2010-11-15T00:00:00.000000000,8210 111 ST NW,Edmonton,AB,Canada,T6G 2C7,3.96 157,18,2010-11-16T00:00:00.000000000,627 Broadway,New York,NY,USA,10012-2612,5.94 158,24,2010-11-19T00:00:00.000000000,162 E Superior Street,Chicago,IL,USA,60611,8.91 159,33,2010-11-24T00:00:00.000000000,5112 48 Street,Yellowknife,NT,Canada,X1A 1N6,13.86 160,47,2010-12-02T00:00:00.000000000,"Via Degli Scipioni, 43",Rome,RM,Italy,00192,0.99 161,48,2010-12-15T00:00:00.000000000,Lijnbaansgracht 120bg,Amsterdam,VV,Netherlands,1016,1.98 162,50,2010-12-15T00:00:00.000000000,C/ San Bernardo 85,Madrid,,Spain,28015,1.98 163,52,2010-12-16T00:00:00.000000000,202 Hoxton Street,London,,United Kingdom,N1 5LH,3.96 164,56,2010-12-17T00:00:00.000000000,307 Macacha Güemes,Buenos Aires,,Argentina,1106,5.94 165,3,2010-12-20T00:00:00.000000000,1498 rue Bélanger,Montréal,QC,Canada,H2G 1A7,8.91 166,12,2010-12-25T00:00:00.000000000,"Praça Pio X, 119",Rio de Janeiro,RJ,Brazil,20040-020,13.86 167,26,2011-01-02T00:00:00.000000000,2211 W Berry Street,Fort Worth,TX,USA,76110,0.99 168,27,2011-01-15T00:00:00.000000000,1033 N Park Ave,Tucson,AZ,USA,85719,1.98 169,29,2011-01-15T00:00:00.000000000,796 Dundas Street West,Toronto,ON,Canada,M6J 1V1,1.98 170,31,2011-01-16T00:00:00.000000000,194A Chain Lake Drive,Halifax,NS,Canada,B3S 1C5,3.96 171,35,2011-01-17T00:00:00.000000000,"Rua dos Campeões Europeus de Viena, 4350",Porto,,Portugal,,5.94 172,41,2011-01-20T00:00:00.000000000,"11, Place Bellecour",Lyon,,France,69002,8.91 173,50,2011-01-25T00:00:00.000000000,C/ San Bernardo 85,Madrid,,Spain,28015,13.86 174,5,2011-02-02T00:00:00.000000000,Klanova 9/506,Prague,,Czech Republic,14700,0.99 175,6,2011-02-15T00:00:00.000000000,Rilská 3174/6,Prague,,Czech Republic,14300,1.98 176,8,2011-02-15T00:00:00.000000000,Grétrystraat 63,Brussels,,Belgium,1000,1.98 177,10,2011-02-16T00:00:00.000000000,"Rua Dr. Falcão Filho, 155",São Paulo,SP,Brazil,01007-010,3.96 178,14,2011-02-17T00:00:00.000000000,8210 111 ST NW,Edmonton,AB,Canada,T6G 2C7,5.94 179,20,2011-02-20T00:00:00.000000000,541 Del Medio Avenue,Mountain View,CA,USA,94040-111,8.91 180,29,2011-02-25T00:00:00.000000000,796 Dundas Street West,Toronto,ON,Canada,M6J 1V1,13.86 181,43,2011-03-05T00:00:00.000000000,"68, Rue Jouvence",Dijon,,France,21000,0.99 182,44,2011-03-18T00:00:00.000000000,Porthaninkatu 9,Helsinki,,Finland,00530,1.98 183,46,2011-03-18T00:00:00.000000000,3 Chatham Street,Dublin,Dublin,Ireland,,1.98 184,48,2011-03-19T00:00:00.000000000,Lijnbaansgracht 120bg,Amsterdam,VV,Netherlands,1016,3.96 185,52,2011-03-20T00:00:00.000000000,202 Hoxton Street,London,,United Kingdom,N1 5LH,5.94 186,58,2011-03-23T00:00:00.000000000,"12,Community Centre",Delhi,,India,110017,8.91 187,8,2011-03-28T00:00:00.000000000,Grétrystraat 63,Brussels,,Belgium,1000,13.86 188,22,2011-04-05T00:00:00.000000000,120 S Orange Ave,Orlando,FL,USA,32801,0.99 189,23,2011-04-18T00:00:00.000000000,69 Salem Street,Boston,MA,USA,2113,1.98 190,25,2011-04-18T00:00:00.000000000,319 N. Frances Street,Madison,WI,USA,53703,1.98 191,27,2011-04-19T00:00:00.000000000,1033 N Park Ave,Tucson,AZ,USA,85719,3.96 192,31,2011-04-20T00:00:00.000000000,194A Chain Lake Drive,Halifax,NS,Canada,B3S 1C5,5.94 193,37,2011-04-23T00:00:00.000000000,Berger Straße 10,Frankfurt,,Germany,60316,14.91 194,46,2011-04-28T00:00:00.000000000,3 Chatham Street,Dublin,Dublin,Ireland,,21.86 195,1,2011-05-06T00:00:00.000000000,"Av. Brigadeiro Faria Lima, 2170",São José dos Campos,SP,Brazil,12227-000,0.99 196,2,2011-05-19T00:00:00.000000000,Theodor-Heuss-Straße 34,Stuttgart,,Germany,70174,1.98 197,4,2011-05-19T00:00:00.000000000,Ullevålsveien 14,Oslo,,Norway,0171,1.98 198,6,2011-05-20T00:00:00.000000000,Rilská 3174/6,Prague,,Czech Republic,14300,3.96 199,10,2011-05-21T00:00:00.000000000,"Rua Dr. Falcão Filho, 155",São Paulo,SP,Brazil,01007-010,5.94 200,16,2011-05-24T00:00:00.000000000,1600 Amphitheatre Parkway,Mountain View,CA,USA,94043-1351,8.91 201,25,2011-05-29T00:00:00.000000000,319 N. Frances Street,Madison,WI,USA,53703,18.86 202,39,2011-06-06T00:00:00.000000000,"4, Rue Milton",Paris,,France,75009,1.99 203,40,2011-06-19T00:00:00.000000000,"8, Rue Hanovre",Paris,,France,75002,2.98 204,42,2011-06-19T00:00:00.000000000,"9, Place Louis Barthou",Bordeaux,,France,33000,3.98 205,44,2011-06-20T00:00:00.000000000,Porthaninkatu 9,Helsinki,,Finland,00530,7.96 206,48,2011-06-21T00:00:00.000000000,Lijnbaansgracht 120bg,Amsterdam,VV,Netherlands,1016,8.94 207,54,2011-06-24T00:00:00.000000000,110 Raeburn Pl,Edinburgh ,,United Kingdom,EH4 1HH,8.91 208,4,2011-06-29T00:00:00.000000000,Ullevålsveien 14,Oslo,,Norway,0171,15.86 209,18,2011-07-07T00:00:00.000000000,627 Broadway,New York,NY,USA,10012-2612,0.99 210,19,2011-07-20T00:00:00.000000000,1 Infinite Loop,Cupertino,CA,USA,95014,1.98 211,21,2011-07-20T00:00:00.000000000,801 W 4th Street,Reno,NV,USA,89503,1.98 212,23,2011-07-21T00:00:00.000000000,69 Salem Street,Boston,MA,USA,2113,3.96 213,27,2011-07-22T00:00:00.000000000,1033 N Park Ave,Tucson,AZ,USA,85719,5.94 214,33,2011-07-25T00:00:00.000000000,5112 48 Street,Yellowknife,NT,Canada,X1A 1N6,8.91 215,42,2011-07-30T00:00:00.000000000,"9, Place Louis Barthou",Bordeaux,,France,33000,13.86 216,56,2011-08-07T00:00:00.000000000,307 Macacha Güemes,Buenos Aires,,Argentina,1106,0.99 217,57,2011-08-20T00:00:00.000000000,"Calle Lira, 198",Santiago,,Chile,,1.98 218,59,2011-08-20T00:00:00.000000000,"3,Raj Bhavan Road",Bangalore,,India,560001,1.98 219,2,2011-08-21T00:00:00.000000000,Theodor-Heuss-Straße 34,Stuttgart,,Germany,70174,3.96 220,6,2011-08-22T00:00:00.000000000,Rilská 3174/6,Prague,,Czech Republic,14300,5.94 221,12,2011-08-25T00:00:00.000000000,"Praça Pio X, 119",Rio de Janeiro,RJ,Brazil,20040-020,8.91 222,21,2011-08-30T00:00:00.000000000,801 W 4th Street,Reno,NV,USA,89503,13.86 223,35,2011-09-07T00:00:00.000000000,"Rua dos Campeões Europeus de Viena, 4350",Porto,,Portugal,,0.99 224,36,2011-09-20T00:00:00.000000000,Tauentzienstraße 8,Berlin,,Germany,10789,1.98 225,38,2011-09-20T00:00:00.000000000,Barbarossastraße 19,Berlin,,Germany,10779,1.98 226,40,2011-09-21T00:00:00.000000000,"8, Rue Hanovre",Paris,,France,75002,3.96 227,44,2011-09-22T00:00:00.000000000,Porthaninkatu 9,Helsinki,,Finland,00530,5.94 228,50,2011-09-25T00:00:00.000000000,C/ San Bernardo 85,Madrid,,Spain,28015,8.91 229,59,2011-09-30T00:00:00.000000000,"3,Raj Bhavan Road",Bangalore,,India,560001,13.86 230,14,2011-10-08T00:00:00.000000000,8210 111 ST NW,Edmonton,AB,Canada,T6G 2C7,0.99 231,15,2011-10-21T00:00:00.000000000,700 W Pender Street,Vancouver,BC,Canada,V6C 1G8,1.98 232,17,2011-10-21T00:00:00.000000000,1 Microsoft Way,Redmond,WA,USA,98052-8300,1.98 233,19,2011-10-22T00:00:00.000000000,1 Infinite Loop,Cupertino,CA,USA,95014,3.96 234,23,2011-10-23T00:00:00.000000000,69 Salem Street,Boston,MA,USA,2113,5.94 235,29,2011-10-26T00:00:00.000000000,796 Dundas Street West,Toronto,ON,Canada,M6J 1V1,8.91 236,38,2011-10-31T00:00:00.000000000,Barbarossastraße 19,Berlin,,Germany,10779,13.86 237,52,2011-11-08T00:00:00.000000000,202 Hoxton Street,London,,United Kingdom,N1 5LH,0.99 238,53,2011-11-21T00:00:00.000000000,113 Lupus St,London,,United Kingdom,SW1V 3EN,1.98 239,55,2011-11-21T00:00:00.000000000,421 Bourke Street,Sidney,NSW,Australia,2010,1.98 240,57,2011-11-22T00:00:00.000000000,"Calle Lira, 198",Santiago,,Chile,,3.96 241,2,2011-11-23T00:00:00.000000000,Theodor-Heuss-Straße 34,Stuttgart,,Germany,70174,5.94 242,8,2011-11-26T00:00:00.000000000,Grétrystraat 63,Brussels,,Belgium,1000,8.91 243,17,2011-12-01T00:00:00.000000000,1 Microsoft Way,Redmond,WA,USA,98052-8300,13.86 244,31,2011-12-09T00:00:00.000000000,194A Chain Lake Drive,Halifax,NS,Canada,B3S 1C5,0.99 245,32,2011-12-22T00:00:00.000000000,696 Osborne Street,Winnipeg,MB,Canada,R3L 2B9,1.98 246,34,2011-12-22T00:00:00.000000000,Rua da Assunção 53,Lisbon,,Portugal,,1.98 247,36,2011-12-23T00:00:00.000000000,Tauentzienstraße 8,Berlin,,Germany,10789,3.96 248,40,2011-12-24T00:00:00.000000000,"8, Rue Hanovre",Paris,,France,75002,5.94 249,46,2011-12-27T00:00:00.000000000,3 Chatham Street,Dublin,Dublin,Ireland,,8.91 250,55,2012-01-01T00:00:00.000000000,421 Bourke Street,Sidney,NSW,Australia,2010,13.86 251,10,2012-01-09T00:00:00.000000000,"Rua Dr. Falcão Filho, 155",São Paulo,SP,Brazil,01007-010,0.99 252,11,2012-01-22T00:00:00.000000000,"Av. Paulista, 2022",São Paulo,SP,Brazil,01310-200,1.98 253,13,2012-01-22T00:00:00.000000000,Qe 7 Bloco G,Brasília,DF,Brazil,71020-677,1.98 254,15,2012-01-23T00:00:00.000000000,700 W Pender Street,Vancouver,BC,Canada,V6C 1G8,3.96 255,19,2012-01-24T00:00:00.000000000,1 Infinite Loop,Cupertino,CA,USA,95014,5.94 256,25,2012-01-27T00:00:00.000000000,319 N. Frances Street,Madison,WI,USA,53703,8.91 257,34,2012-02-01T00:00:00.000000000,Rua da Assunção 53,Lisbon,,Portugal,,13.86 258,48,2012-02-09T00:00:00.000000000,Lijnbaansgracht 120bg,Amsterdam,VV,Netherlands,1016,0.99 259,49,2012-02-22T00:00:00.000000000,Ordynacka 10,Warsaw,,Poland,00-358,1.98 260,51,2012-02-22T00:00:00.000000000,Celsiusg. 9,Stockholm,,Sweden,11230,1.98 261,53,2012-02-23T00:00:00.000000000,113 Lupus St,London,,United Kingdom,SW1V 3EN,3.96 262,57,2012-02-24T00:00:00.000000000,"Calle Lira, 198",Santiago,,Chile,,5.94 263,4,2012-02-27T00:00:00.000000000,Ullevålsveien 14,Oslo,,Norway,0171,8.91 264,13,2012-03-03T00:00:00.000000000,Qe 7 Bloco G,Brasília,DF,Brazil,71020-677,13.86 265,27,2012-03-11T00:00:00.000000000,1033 N Park Ave,Tucson,AZ,USA,85719,0.99 266,28,2012-03-24T00:00:00.000000000,302 S 700 E,Salt Lake City,UT,USA,84102,1.98 267,30,2012-03-24T00:00:00.000000000,230 Elgin Street,Ottawa,ON,Canada,K2P 1L7,1.98 268,32,2012-03-25T00:00:00.000000000,696 Osborne Street,Winnipeg,MB,Canada,R3L 2B9,3.96 269,36,2012-03-26T00:00:00.000000000,Tauentzienstraße 8,Berlin,,Germany,10789,5.94 270,42,2012-03-29T00:00:00.000000000,"9, Place Louis Barthou",Bordeaux,,France,33000,8.91 271,51,2012-04-03T00:00:00.000000000,Celsiusg. 9,Stockholm,,Sweden,11230,13.86 272,6,2012-04-11T00:00:00.000000000,Rilská 3174/6,Prague,,Czech Republic,14300,0.99 273,7,2012-04-24T00:00:00.000000000,"Rotenturmstraße 4, 1010 Innere Stadt",Vienne,,Austria,1010,1.98 274,9,2012-04-24T00:00:00.000000000,Sønder Boulevard 51,Copenhagen,,Denmark,1720,1.98 275,11,2012-04-25T00:00:00.000000000,"Av. Paulista, 2022",São Paulo,SP,Brazil,01310-200,3.96 276,15,2012-04-26T00:00:00.000000000,700 W Pender Street,Vancouver,BC,Canada,V6C 1G8,5.94 277,21,2012-04-29T00:00:00.000000000,801 W 4th Street,Reno,NV,USA,89503,8.91 278,30,2012-05-04T00:00:00.000000000,230 Elgin Street,Ottawa,ON,Canada,K2P 1L7,13.86 279,44,2012-05-12T00:00:00.000000000,Porthaninkatu 9,Helsinki,,Finland,00530,0.99 280,45,2012-05-25T00:00:00.000000000,Erzsébet krt. 58.,Budapest,,Hungary,H-1073,1.98 281,47,2012-05-25T00:00:00.000000000,"Via Degli Scipioni, 43",Rome,RM,Italy,00192,1.98 282,49,2012-05-26T00:00:00.000000000,Ordynacka 10,Warsaw,,Poland,00-358,3.96 283,53,2012-05-27T00:00:00.000000000,113 Lupus St,London,,United Kingdom,SW1V 3EN,5.94 284,59,2012-05-30T00:00:00.000000000,"3,Raj Bhavan Road",Bangalore,,India,560001,8.91 285,9,2012-06-04T00:00:00.000000000,Sønder Boulevard 51,Copenhagen,,Denmark,1720,13.86 286,23,2012-06-12T00:00:00.000000000,69 Salem Street,Boston,MA,USA,2113,0.99 287,24,2012-06-25T00:00:00.000000000,162 E Superior Street,Chicago,IL,USA,60611,1.98 288,26,2012-06-25T00:00:00.000000000,2211 W Berry Street,Fort Worth,TX,USA,76110,1.98 289,28,2012-06-26T00:00:00.000000000,302 S 700 E,Salt Lake City,UT,USA,84102,3.96 290,32,2012-06-27T00:00:00.000000000,696 Osborne Street,Winnipeg,MB,Canada,R3L 2B9,5.94 291,38,2012-06-30T00:00:00.000000000,Barbarossastraße 19,Berlin,,Germany,10779,8.91 292,47,2012-07-05T00:00:00.000000000,"Via Degli Scipioni, 43",Rome,RM,Italy,00192,13.86 293,2,2012-07-13T00:00:00.000000000,Theodor-Heuss-Straße 34,Stuttgart,,Germany,70174,0.99 294,3,2012-07-26T00:00:00.000000000,1498 rue Bélanger,Montréal,QC,Canada,H2G 1A7,1.98 295,5,2012-07-26T00:00:00.000000000,Klanova 9/506,Prague,,Czech Republic,14700,1.98 296,7,2012-07-27T00:00:00.000000000,"Rotenturmstraße 4, 1010 Innere Stadt",Vienne,,Austria,1010,3.96 297,11,2012-07-28T00:00:00.000000000,"Av. Paulista, 2022",São Paulo,SP,Brazil,01310-200,5.94 298,17,2012-07-31T00:00:00.000000000,1 Microsoft Way,Redmond,WA,USA,98052-8300,10.91 299,26,2012-08-05T00:00:00.000000000,2211 W Berry Street,Fort Worth,TX,USA,76110,23.86 300,40,2012-08-13T00:00:00.000000000,"8, Rue Hanovre",Paris,,France,75002,0.99 301,41,2012-08-26T00:00:00.000000000,"11, Place Bellecour",Lyon,,France,69002,1.98 302,43,2012-08-26T00:00:00.000000000,"68, Rue Jouvence",Dijon,,France,21000,1.98 303,45,2012-08-27T00:00:00.000000000,Erzsébet krt. 58.,Budapest,,Hungary,H-1073,3.96 304,49,2012-08-28T00:00:00.000000000,Ordynacka 10,Warsaw,,Poland,00-358,5.94 305,55,2012-08-31T00:00:00.000000000,421 Bourke Street,Sidney,NSW,Australia,2010,8.91 306,5,2012-09-05T00:00:00.000000000,Klanova 9/506,Prague,,Czech Republic,14700,16.86 307,19,2012-09-13T00:00:00.000000000,1 Infinite Loop,Cupertino,CA,USA,95014,1.99 308,20,2012-09-26T00:00:00.000000000,541 Del Medio Avenue,Mountain View,CA,USA,94040-111,3.98 309,22,2012-09-26T00:00:00.000000000,120 S Orange Ave,Orlando,FL,USA,32801,3.98 310,24,2012-09-27T00:00:00.000000000,162 E Superior Street,Chicago,IL,USA,60611,7.96 311,28,2012-09-28T00:00:00.000000000,302 S 700 E,Salt Lake City,UT,USA,84102,11.94 312,34,2012-10-01T00:00:00.000000000,Rua da Assunção 53,Lisbon,,Portugal,,10.91 313,43,2012-10-06T00:00:00.000000000,"68, Rue Jouvence",Dijon,,France,21000,16.86 314,57,2012-10-14T00:00:00.000000000,"Calle Lira, 198",Santiago,,Chile,,0.99 315,58,2012-10-27T00:00:00.000000000,"12,Community Centre",Delhi,,India,110017,1.98 316,1,2012-10-27T00:00:00.000000000,"Av. Brigadeiro Faria Lima, 2170",São José dos Campos,SP,Brazil,12227-000,1.98 317,3,2012-10-28T00:00:00.000000000,1498 rue Bélanger,Montréal,QC,Canada,H2G 1A7,3.96 318,7,2012-10-29T00:00:00.000000000,"Rotenturmstraße 4, 1010 Innere Stadt",Vienne,,Austria,1010,5.94 319,13,2012-11-01T00:00:00.000000000,Qe 7 Bloco G,Brasília,DF,Brazil,71020-677,8.91 320,22,2012-11-06T00:00:00.000000000,120 S Orange Ave,Orlando,FL,USA,32801,13.86 321,36,2012-11-14T00:00:00.000000000,Tauentzienstraße 8,Berlin,,Germany,10789,0.99 322,37,2012-11-27T00:00:00.000000000,Berger Straße 10,Frankfurt,,Germany,60316,1.98 323,39,2012-11-27T00:00:00.000000000,"4, Rue Milton",Paris,,France,75009,1.98 324,41,2012-11-28T00:00:00.000000000,"11, Place Bellecour",Lyon,,France,69002,3.96 325,45,2012-11-29T00:00:00.000000000,Erzsébet krt. 58.,Budapest,,Hungary,H-1073,5.94 326,51,2012-12-02T00:00:00.000000000,Celsiusg. 9,Stockholm,,Sweden,11230,8.91 327,1,2012-12-07T00:00:00.000000000,"Av. Brigadeiro Faria Lima, 2170",São José dos Campos,SP,Brazil,12227-000,13.86 328,15,2012-12-15T00:00:00.000000000,700 W Pender Street,Vancouver,BC,Canada,V6C 1G8,0.99 329,16,2012-12-28T00:00:00.000000000,1600 Amphitheatre Parkway,Mountain View,CA,USA,94043-1351,1.98 330,18,2012-12-28T00:00:00.000000000,627 Broadway,New York,NY,USA,10012-2612,1.98 331,20,2012-12-29T00:00:00.000000000,541 Del Medio Avenue,Mountain View,CA,USA,94040-111,3.96 332,24,2012-12-30T00:00:00.000000000,162 E Superior Street,Chicago,IL,USA,60611,5.94 333,30,2013-01-02T00:00:00.000000000,230 Elgin Street,Ottawa,ON,Canada,K2P 1L7,8.91 334,39,2013-01-07T00:00:00.000000000,"4, Rue Milton",Paris,,France,75009,13.86 335,53,2013-01-15T00:00:00.000000000,113 Lupus St,London,,United Kingdom,SW1V 3EN,0.99 336,54,2013-01-28T00:00:00.000000000,110 Raeburn Pl,Edinburgh ,,United Kingdom,EH4 1HH,1.98 337,56,2013-01-28T00:00:00.000000000,307 Macacha Güemes,Buenos Aires,,Argentina,1106,1.98 338,58,2013-01-29T00:00:00.000000000,"12,Community Centre",Delhi,,India,110017,3.96 339,3,2013-01-30T00:00:00.000000000,1498 rue Bélanger,Montréal,QC,Canada,H2G 1A7,5.94 340,9,2013-02-02T00:00:00.000000000,Sønder Boulevard 51,Copenhagen,,Denmark,1720,8.91 341,18,2013-02-07T00:00:00.000000000,627 Broadway,New York,NY,USA,10012-2612,13.86 342,32,2013-02-15T00:00:00.000000000,696 Osborne Street,Winnipeg,MB,Canada,R3L 2B9,0.99 343,33,2013-02-28T00:00:00.000000000,5112 48 Street,Yellowknife,NT,Canada,X1A 1N6,1.98 344,35,2013-02-28T00:00:00.000000000,"Rua dos Campeões Europeus de Viena, 4350",Porto,,Portugal,,1.98 345,37,2013-03-01T00:00:00.000000000,Berger Straße 10,Frankfurt,,Germany,60316,3.96 346,41,2013-03-02T00:00:00.000000000,"11, Place Bellecour",Lyon,,France,69002,5.94 347,47,2013-03-05T00:00:00.000000000,"Via Degli Scipioni, 43",Rome,RM,Italy,00192,8.91 348,56,2013-03-10T00:00:00.000000000,307 Macacha Güemes,Buenos Aires,,Argentina,1106,13.86 349,11,2013-03-18T00:00:00.000000000,"Av. Paulista, 2022",São Paulo,SP,Brazil,01310-200,0.99 350,12,2013-03-31T00:00:00.000000000,"Praça Pio X, 119",Rio de Janeiro,RJ,Brazil,20040-020,1.98 351,14,2013-03-31T00:00:00.000000000,8210 111 ST NW,Edmonton,AB,Canada,T6G 2C7,1.98 352,16,2013-04-01T00:00:00.000000000,1600 Amphitheatre Parkway,Mountain View,CA,USA,94043-1351,3.96 353,20,2013-04-02T00:00:00.000000000,541 Del Medio Avenue,Mountain View,CA,USA,94040-111,5.94 354,26,2013-04-05T00:00:00.000000000,2211 W Berry Street,Fort Worth,TX,USA,76110,8.91 355,35,2013-04-10T00:00:00.000000000,"Rua dos Campeões Europeus de Viena, 4350",Porto,,Portugal,,13.86 356,49,2013-04-18T00:00:00.000000000,Ordynacka 10,Warsaw,,Poland,00-358,0.99 357,50,2013-05-01T00:00:00.000000000,C/ San Bernardo 85,Madrid,,Spain,28015,1.98 358,52,2013-05-01T00:00:00.000000000,202 Hoxton Street,London,,United Kingdom,N1 5LH,1.98 359,54,2013-05-02T00:00:00.000000000,110 Raeburn Pl,Edinburgh ,,United Kingdom,EH4 1HH,3.96 360,58,2013-05-03T00:00:00.000000000,"12,Community Centre",Delhi,,India,110017,5.94 361,5,2013-05-06T00:00:00.000000000,Klanova 9/506,Prague,,Czech Republic,14700,8.91 362,14,2013-05-11T00:00:00.000000000,8210 111 ST NW,Edmonton,AB,Canada,T6G 2C7,13.86 363,28,2013-05-19T00:00:00.000000000,302 S 700 E,Salt Lake City,UT,USA,84102,0.99 364,29,2013-06-01T00:00:00.000000000,796 Dundas Street West,Toronto,ON,Canada,M6J 1V1,1.98 365,31,2013-06-01T00:00:00.000000000,194A Chain Lake Drive,Halifax,NS,Canada,B3S 1C5,1.98 366,33,2013-06-02T00:00:00.000000000,5112 48 Street,Yellowknife,NT,Canada,X1A 1N6,3.96 367,37,2013-06-03T00:00:00.000000000,Berger Straße 10,Frankfurt,,Germany,60316,5.94 368,43,2013-06-06T00:00:00.000000000,"68, Rue Jouvence",Dijon,,France,21000,8.91 369,52,2013-06-11T00:00:00.000000000,202 Hoxton Street,London,,United Kingdom,N1 5LH,13.86 370,7,2013-06-19T00:00:00.000000000,"Rotenturmstraße 4, 1010 Innere Stadt",Vienne,,Austria,1010,0.99 371,8,2013-07-02T00:00:00.000000000,Grétrystraat 63,Brussels,,Belgium,1000,1.98 372,10,2013-07-02T00:00:00.000000000,"Rua Dr. Falcão Filho, 155",São Paulo,SP,Brazil,01007-010,1.98 373,12,2013-07-03T00:00:00.000000000,"Praça Pio X, 119",Rio de Janeiro,RJ,Brazil,20040-020,3.96 374,16,2013-07-04T00:00:00.000000000,1600 Amphitheatre Parkway,Mountain View,CA,USA,94043-1351,5.94 375,22,2013-07-07T00:00:00.000000000,120 S Orange Ave,Orlando,FL,USA,32801,8.91 376,31,2013-07-12T00:00:00.000000000,194A Chain Lake Drive,Halifax,NS,Canada,B3S 1C5,13.86 377,45,2013-07-20T00:00:00.000000000,Erzsébet krt. 58.,Budapest,,Hungary,H-1073,0.99 378,46,2013-08-02T00:00:00.000000000,3 Chatham Street,Dublin,Dublin,Ireland,,1.98 379,48,2013-08-02T00:00:00.000000000,Lijnbaansgracht 120bg,Amsterdam,VV,Netherlands,1016,1.98 380,50,2013-08-03T00:00:00.000000000,C/ San Bernardo 85,Madrid,,Spain,28015,3.96 381,54,2013-08-04T00:00:00.000000000,110 Raeburn Pl,Edinburgh ,,United Kingdom,EH4 1HH,5.94 382,1,2013-08-07T00:00:00.000000000,"Av. Brigadeiro Faria Lima, 2170",São José dos Campos,SP,Brazil,12227-000,8.91 383,10,2013-08-12T00:00:00.000000000,"Rua Dr. Falcão Filho, 155",São Paulo,SP,Brazil,01007-010,13.86 384,24,2013-08-20T00:00:00.000000000,162 E Superior Street,Chicago,IL,USA,60611,0.99 385,25,2013-09-02T00:00:00.000000000,319 N. Frances Street,Madison,WI,USA,53703,1.98 386,27,2013-09-02T00:00:00.000000000,1033 N Park Ave,Tucson,AZ,USA,85719,1.98 387,29,2013-09-03T00:00:00.000000000,796 Dundas Street West,Toronto,ON,Canada,M6J 1V1,3.96 388,33,2013-09-04T00:00:00.000000000,5112 48 Street,Yellowknife,NT,Canada,X1A 1N6,5.94 389,39,2013-09-07T00:00:00.000000000,"4, Rue Milton",Paris,,France,75009,8.91 390,48,2013-09-12T00:00:00.000000000,Lijnbaansgracht 120bg,Amsterdam,VV,Netherlands,1016,13.86 391,3,2013-09-20T00:00:00.000000000,1498 rue Bélanger,Montréal,QC,Canada,H2G 1A7,0.99 392,4,2013-10-03T00:00:00.000000000,Ullevålsveien 14,Oslo,,Norway,0171,1.98 393,6,2013-10-03T00:00:00.000000000,Rilská 3174/6,Prague,,Czech Republic,14300,1.98 394,8,2013-10-04T00:00:00.000000000,Grétrystraat 63,Brussels,,Belgium,1000,3.96 395,12,2013-10-05T00:00:00.000000000,"Praça Pio X, 119",Rio de Janeiro,RJ,Brazil,20040-020,5.94 396,18,2013-10-08T00:00:00.000000000,627 Broadway,New York,NY,USA,10012-2612,8.91 397,27,2013-10-13T00:00:00.000000000,1033 N Park Ave,Tucson,AZ,USA,85719,13.86 398,41,2013-10-21T00:00:00.000000000,"11, Place Bellecour",Lyon,,France,69002,0.99 399,42,2013-11-03T00:00:00.000000000,"9, Place Louis Barthou",Bordeaux,,France,33000,1.98 400,44,2013-11-03T00:00:00.000000000,Porthaninkatu 9,Helsinki,,Finland,00530,1.98 401,46,2013-11-04T00:00:00.000000000,3 Chatham Street,Dublin,Dublin,Ireland,,3.96 402,50,2013-11-05T00:00:00.000000000,C/ San Bernardo 85,Madrid,,Spain,28015,5.94 403,56,2013-11-08T00:00:00.000000000,307 Macacha Güemes,Buenos Aires,,Argentina,1106,8.91 404,6,2013-11-13T00:00:00.000000000,Rilská 3174/6,Prague,,Czech Republic,14300,25.86 405,20,2013-11-21T00:00:00.000000000,541 Del Medio Avenue,Mountain View,CA,USA,94040-111,0.99 406,21,2013-12-04T00:00:00.000000000,801 W 4th Street,Reno,NV,USA,89503,1.98 407,23,2013-12-04T00:00:00.000000000,69 Salem Street,Boston,MA,USA,2113,1.98 408,25,2013-12-05T00:00:00.000000000,319 N. Frances Street,Madison,WI,USA,53703,3.96 409,29,2013-12-06T00:00:00.000000000,796 Dundas Street West,Toronto,ON,Canada,M6J 1V1,5.94 410,35,2013-12-09T00:00:00.000000000,"Rua dos Campeões Europeus de Viena, 4350",Porto,,Portugal,,8.91 411,44,2013-12-14T00:00:00.000000000,Porthaninkatu 9,Helsinki,,Finland,00530,13.86 412,58,2013-12-22T00:00:00.000000000,"12,Community Centre",Delhi,,India,110017,1.99 ================================================ FILE: prqlc/prqlc/tests/integration/data/chinook/media_types.csv ================================================ media_type_id,name 1,MPEG audio file 2,Protected AAC audio file 3,Protected MPEG-4 video file 4,Purchased AAC audio file 5,AAC audio file ================================================ FILE: prqlc/prqlc/tests/integration/data/chinook/media_types.json ================================================ [ { "media_type_id": 1, "name": "MPEG audio file" }, { "media_type_id": 2, "name": "Protected AAC audio file" }, { "media_type_id": 3, "name": "Protected MPEG-4 video file" }, { "media_type_id": 4, "name": "Purchased AAC audio file" }, { "media_type_id": 5, "name": "AAC audio file" } ] ================================================ FILE: prqlc/prqlc/tests/integration/data/chinook/playlist_track.csv ================================================ playlist_id,track_id 1,3402 1,3389 1,3390 1,3391 1,3392 1,3393 1,3394 1,3395 1,3396 1,3397 1,3398 1,3399 1,3400 1,3401 1,3336 1,3478 1,3375 1,3376 1,3377 1,3378 1,3379 1,3380 1,3381 1,3382 1,3383 1,3384 1,3385 1,3386 1,3387 1,3388 1,3365 1,3366 1,3367 1,3368 1,3369 1,3370 1,3371 1,3372 1,3373 1,3374 1,99 1,100 1,101 1,102 1,103 1,104 1,105 1,106 1,107 1,108 1,109 1,110 1,166 1,167 1,168 1,169 1,170 1,171 1,172 1,173 1,174 1,175 1,176 1,177 1,178 1,179 1,180 1,181 1,182 1,2591 1,2592 1,2593 1,2594 1,2595 1,2596 1,2597 1,2598 1,2599 1,2600 1,2601 1,2602 1,2603 1,2604 1,2605 1,2606 1,2607 1,2608 1,923 1,924 1,925 1,926 1,927 1,928 1,929 1,930 1,931 1,932 1,933 1,934 1,935 1,936 1,937 1,938 1,939 1,940 1,941 1,942 1,943 1,944 1,945 1,946 1,947 1,948 1,964 1,965 1,966 1,967 1,968 1,969 1,970 1,971 1,972 1,973 1,974 1,1009 1,1010 1,1011 1,1012 1,1013 1,1014 1,1015 1,1016 1,1017 1,1018 1,1019 1,1133 1,1134 1,1135 1,1136 1,1137 1,1138 1,1139 1,1140 1,1141 1,1142 1,1143 1,1144 1,1145 1,468 1,469 1,470 1,471 1,472 1,473 1,474 1,475 1,476 1,477 1,478 1,479 1,480 1,481 1,482 1,483 1,484 1,485 1,486 1,487 1,488 1,1466 1,1467 1,1468 1,1469 1,1470 1,1471 1,1472 1,1473 1,1474 1,1475 1,1476 1,1477 1,1478 1,529 1,530 1,531 1,532 1,533 1,534 1,535 1,536 1,537 1,538 1,539 1,540 1,541 1,542 1,2165 1,2166 1,2167 1,2168 1,2169 1,2170 1,2171 1,2172 1,2173 1,2174 1,2175 1,2176 1,2177 1,2318 1,2319 1,2320 1,2321 1,2322 1,2323 1,2324 1,2325 1,2326 1,2327 1,2328 1,2329 1,2330 1,2331 1,2332 1,2333 1,2285 1,2286 1,2287 1,2288 1,2289 1,2290 1,2291 1,2292 1,2293 1,2294 1,2295 1,2310 1,2311 1,2312 1,2313 1,2314 1,2315 1,2316 1,2317 1,2282 1,2283 1,2284 1,2334 1,2335 1,2336 1,2337 1,2338 1,2339 1,2340 1,2341 1,2342 1,2343 1,2358 1,2359 1,2360 1,2361 1,2362 1,2363 1,2364 1,2365 1,2366 1,2367 1,2368 1,2369 1,2370 1,2371 1,2372 1,2373 1,2374 1,2472 1,2473 1,2474 1,2475 1,2476 1,2477 1,2478 1,2479 1,2480 1,2481 1,2482 1,2483 1,2484 1,2485 1,2486 1,2487 1,2488 1,2489 1,2490 1,2491 1,2492 1,2493 1,2494 1,2495 1,2496 1,2497 1,2498 1,2499 1,2500 1,2501 1,2502 1,2503 1,2504 1,2505 1,2705 1,2706 1,2707 1,2708 1,2709 1,2710 1,2711 1,2712 1,2713 1,2714 1,2715 1,2716 1,2717 1,2718 1,2719 1,2720 1,2721 1,2722 1,2723 1,2724 1,2725 1,2726 1,2727 1,2728 1,2729 1,2730 1,2781 1,2782 1,2783 1,2784 1,2785 1,2786 1,2787 1,2788 1,2789 1,2790 1,2791 1,2792 1,2793 1,2794 1,2795 1,2796 1,2797 1,2798 1,2799 1,2800 1,2801 1,2802 1,2803 1,2804 1,2805 1,2806 1,2807 1,2808 1,2809 1,2810 1,2811 1,2812 1,2813 1,2814 1,2815 1,2816 1,2817 1,2818 1,2572 1,2573 1,2574 1,2575 1,2576 1,2577 1,2578 1,2579 1,2580 1,2581 1,2582 1,2583 1,2584 1,2585 1,2586 1,2587 1,2588 1,2589 1,2590 1,194 1,195 1,196 1,197 1,198 1,199 1,200 1,201 1,202 1,203 1,204 1,891 1,892 1,893 1,894 1,895 1,896 1,897 1,898 1,899 1,900 1,901 1,902 1,903 1,904 1,905 1,906 1,907 1,908 1,909 1,910 1,911 1,912 1,913 1,914 1,915 1,916 1,917 1,918 1,919 1,920 1,921 1,922 1,1268 1,1269 1,1270 1,1271 1,1272 1,1273 1,1274 1,1275 1,1276 1,2532 1,2533 1,2534 1,2535 1,2536 1,2537 1,2538 1,2539 1,2540 1,2541 1,646 1,647 1,648 1,649 1,651 1,653 1,655 1,658 1,652 1,656 1,657 1,650 1,659 1,654 1,660 1,3427 1,3411 1,3412 1,3419 1,3482 1,3438 1,3485 1,3403 1,3406 1,3442 1,3421 1,3436 1,3450 1,3454 1,3491 1,3413 1,3426 1,3416 1,3501 1,3487 1,3417 1,3432 1,3443 1,3447 1,3452 1,3441 1,3434 1,3500 1,3449 1,3405 1,3488 1,3423 1,3499 1,3445 1,3440 1,3453 1,3497 1,3494 1,3439 1,3422 1,3407 1,3495 1,3435 1,3490 1,3489 1,3448 1,3492 1,3425 1,3483 1,3420 1,3424 1,3493 1,3437 1,3498 1,3446 1,3444 1,3496 1,3502 1,3359 1,3433 1,3415 1,3479 1,3481 1,3404 1,3486 1,3414 1,3410 1,3431 1,3418 1,3430 1,3408 1,3480 1,3409 1,3484 1,1033 1,1034 1,1035 1,1036 1,1037 1,1038 1,1039 1,1040 1,1041 1,1042 1,1043 1,1044 1,1045 1,1046 1,1047 1,1048 1,1049 1,1050 1,1051 1,1052 1,1053 1,1054 1,1055 1,1056 1,3324 1,3331 1,3332 1,3322 1,3329 1,1455 1,1456 1,1457 1,1458 1,1459 1,1460 1,1461 1,1462 1,1463 1,1464 1,1465 1,3352 1,3358 1,3326 1,3327 1,3330 1,3321 1,3319 1,3328 1,3325 1,3323 1,3334 1,3333 1,3335 1,3320 1,1245 1,1246 1,1247 1,1248 1,1249 1,1250 1,1251 1,1252 1,1253 1,1254 1,1255 1,1277 1,1278 1,1279 1,1280 1,1281 1,1282 1,1283 1,1284 1,1285 1,1286 1,1287 1,1288 1,1300 1,1301 1,1302 1,1303 1,1304 1,3301 1,3300 1,3302 1,3303 1,3304 1,3305 1,3306 1,3307 1,3308 1,3309 1,3310 1,3311 1,3312 1,3313 1,3314 1,3315 1,3316 1,3317 1,3318 1,2238 1,2239 1,2240 1,2241 1,2242 1,2243 1,2244 1,2245 1,2246 1,2247 1,2248 1,2249 1,2250 1,2251 1,2252 1,2253 1,3357 1,3350 1,3349 1,63 1,64 1,65 1,66 1,67 1,68 1,69 1,70 1,71 1,72 1,73 1,74 1,75 1,76 1,123 1,124 1,125 1,126 1,127 1,128 1,129 1,130 1,842 1,843 1,844 1,845 1,846 1,847 1,848 1,849 1,850 1,624 1,625 1,626 1,627 1,628 1,629 1,630 1,631 1,632 1,633 1,634 1,635 1,636 1,637 1,638 1,639 1,640 1,641 1,642 1,643 1,644 1,645 1,1102 1,1103 1,1104 1,1188 1,1189 1,1190 1,1191 1,1192 1,1193 1,1194 1,1195 1,1196 1,1197 1,1198 1,1199 1,1200 1,597 1,598 1,599 1,600 1,601 1,602 1,603 1,604 1,605 1,606 1,607 1,608 1,609 1,610 1,611 1,612 1,613 1,614 1,615 1,616 1,617 1,618 1,619 1,1902 1,1903 1,1904 1,1905 1,1906 1,1907 1,1908 1,1909 1,1910 1,1911 1,1912 1,1913 1,1914 1,1915 1,456 1,457 1,458 1,459 1,460 1,461 1,462 1,463 1,464 1,465 1,466 1,467 1,2523 1,2524 1,2525 1,2526 1,2527 1,2528 1,2529 1,2530 1,2531 1,379 1,391 1,376 1,397 1,382 1,389 1,404 1,406 1,380 1,394 1,515 1,516 1,517 1,518 1,519 1,520 1,521 1,522 1,523 1,524 1,525 1,526 1,527 1,528 1,205 1,206 1,207 1,208 1,209 1,210 1,211 1,212 1,213 1,214 1,215 1,216 1,217 1,218 1,219 1,220 1,221 1,222 1,223 1,224 1,225 1,715 1,716 1,717 1,718 1,719 1,720 1,721 1,722 1,723 1,724 1,725 1,726 1,727 1,728 1,729 1,730 1,731 1,732 1,733 1,734 1,735 1,736 1,737 1,738 1,739 1,740 1,741 1,742 1,743 1,744 1,226 1,227 1,228 1,229 1,230 1,231 1,232 1,233 1,234 1,235 1,236 1,237 1,238 1,239 1,240 1,241 1,242 1,243 1,244 1,245 1,246 1,247 1,248 1,249 1,250 1,251 1,252 1,253 1,254 1,255 1,256 1,257 1,258 1,259 1,260 1,261 1,262 1,263 1,264 1,265 1,266 1,267 1,268 1,269 1,270 1,271 1,272 1,273 1,274 1,275 1,276 1,277 1,278 1,279 1,280 1,281 1,313 1,314 1,315 1,316 1,317 1,318 1,319 1,320 1,321 1,322 1,399 1,851 1,852 1,853 1,854 1,855 1,856 1,857 1,858 1,859 1,860 1,861 1,862 1,863 1,864 1,865 1,866 1,867 1,868 1,869 1,870 1,871 1,872 1,873 1,874 1,875 1,876 1,583 1,584 1,585 1,586 1,587 1,588 1,589 1,590 1,591 1,592 1,593 1,594 1,595 1,596 1,388 1,402 1,407 1,396 1,877 1,878 1,879 1,880 1,881 1,882 1,883 1,884 1,885 1,886 1,887 1,888 1,889 1,890 1,975 1,976 1,977 1,978 1,979 1,980 1,981 1,982 1,983 1,984 1,985 1,986 1,987 1,988 1,390 1,1057 1,1058 1,1059 1,1060 1,1061 1,1062 1,1063 1,1064 1,1065 1,1066 1,1067 1,1068 1,1069 1,1070 1,1071 1,1072 1,377 1,395 1,1087 1,1088 1,1089 1,1090 1,1091 1,1092 1,1093 1,1094 1,1095 1,1096 1,1097 1,1098 1,1099 1,1100 1,1101 1,1105 1,1106 1,1107 1,1108 1,1109 1,1110 1,1111 1,1112 1,1113 1,1114 1,1115 1,1116 1,1117 1,1118 1,1119 1,1120 1,501 1,502 1,503 1,504 1,505 1,506 1,507 1,508 1,509 1,510 1,511 1,512 1,513 1,514 1,405 1,378 1,392 1,403 1,1506 1,1507 1,1508 1,1509 1,1510 1,1511 1,1512 1,1513 1,1514 1,1515 1,1516 1,1517 1,1518 1,1519 1,381 1,1520 1,1521 1,1522 1,1523 1,1524 1,1525 1,1526 1,1527 1,1528 1,1529 1,1530 1,1531 1,400 1,1686 1,1687 1,1688 1,1689 1,1690 1,1691 1,1692 1,1693 1,1694 1,1695 1,1696 1,1697 1,1698 1,1699 1,1700 1,1701 1,1671 1,1672 1,1673 1,1674 1,1675 1,1676 1,1677 1,1678 1,1679 1,1680 1,1681 1,1682 1,1683 1,1684 1,1685 1,3356 1,384 1,1717 1,1720 1,1722 1,1723 1,1726 1,1727 1,1730 1,1731 1,1733 1,1736 1,1737 1,1740 1,1742 1,1743 1,1718 1,1719 1,1721 1,1724 1,1725 1,1728 1,1729 1,1732 1,1734 1,1735 1,1738 1,1739 1,1741 1,1744 1,374 1,1755 1,1762 1,1763 1,1756 1,1764 1,1757 1,1758 1,1765 1,1766 1,1759 1,1760 1,1767 1,1761 1,1768 1,1769 1,1770 1,1771 1,1772 1,398 1,1916 1,1917 1,1918 1,1919 1,1920 1,1921 1,1922 1,1923 1,1924 1,1925 1,1926 1,1927 1,1928 1,1929 1,1930 1,1931 1,1932 1,1933 1,1934 1,1935 1,1936 1,1937 1,1938 1,1939 1,1940 1,1941 1,375 1,385 1,383 1,387 1,2030 1,2031 1,2032 1,2033 1,2034 1,2035 1,2036 1,2037 1,2038 1,2039 1,2040 1,2041 1,2042 1,2043 1,393 1,2044 1,2045 1,2046 1,2047 1,2048 1,2049 1,2050 1,2051 1,2052 1,2053 1,2054 1,2055 1,2056 1,2057 1,2058 1,2059 1,2060 1,2061 1,2062 1,2063 1,2064 1,2065 1,2066 1,2067 1,2068 1,2069 1,2070 1,2071 1,2072 1,2073 1,2074 1,2075 1,2076 1,2077 1,2078 1,2079 1,2080 1,2081 1,2082 1,2083 1,2084 1,2085 1,2086 1,2087 1,2088 1,2089 1,2090 1,2091 1,2092 1,386 1,401 1,2751 1,2752 1,2753 1,2754 1,2755 1,2756 1,2757 1,2758 1,2759 1,2760 1,2761 1,2762 1,2763 1,2764 1,2765 1,2766 1,2767 1,2768 1,2769 1,2770 1,2771 1,2772 1,2773 1,2774 1,2775 1,2776 1,2777 1,2778 1,2779 1,2780 1,556 1,557 1,558 1,559 1,560 1,561 1,562 1,563 1,564 1,565 1,566 1,567 1,568 1,569 1,661 1,662 1,663 1,664 1,665 1,666 1,667 1,668 1,669 1,670 1,671 1,672 1,673 1,674 1,3117 1,3118 1,3119 1,3120 1,3121 1,3122 1,3123 1,3124 1,3125 1,3126 1,3127 1,3128 1,3129 1,3130 1,3131 1,3146 1,3147 1,3148 1,3149 1,3150 1,3151 1,3152 1,3153 1,3154 1,3155 1,3156 1,3157 1,3158 1,3159 1,3160 1,3161 1,3162 1,3163 1,3164 1,77 1,78 1,79 1,80 1,81 1,82 1,83 1,84 1,131 1,132 1,133 1,134 1,135 1,136 1,137 1,138 1,139 1,140 1,141 1,142 1,143 1,144 1,145 1,146 1,147 1,148 1,149 1,150 1,151 1,152 1,153 1,154 1,155 1,156 1,157 1,158 1,159 1,160 1,161 1,162 1,163 1,164 1,165 1,183 1,184 1,185 1,186 1,187 1,188 1,189 1,190 1,191 1,192 1,193 1,1121 1,1122 1,1123 1,1124 1,1125 1,1126 1,1127 1,1128 1,1129 1,1130 1,1131 1,1132 1,1174 1,1175 1,1176 1,1177 1,1178 1,1179 1,1180 1,1181 1,1182 1,1183 1,1184 1,1185 1,1186 1,1187 1,1289 1,1290 1,1291 1,1292 1,1293 1,1294 1,1295 1,1296 1,1297 1,1298 1,1299 1,1325 1,1326 1,1327 1,1328 1,1329 1,1330 1,1331 1,1332 1,1333 1,1334 1,1391 1,1388 1,1394 1,1387 1,1392 1,1389 1,1390 1,1335 1,1336 1,1337 1,1338 1,1339 1,1340 1,1341 1,1342 1,1343 1,1344 1,1345 1,1346 1,1347 1,1348 1,1349 1,1350 1,1351 1,1212 1,1213 1,1214 1,1215 1,1216 1,1217 1,1218 1,1219 1,1220 1,1221 1,1222 1,1223 1,1224 1,1225 1,1226 1,1227 1,1228 1,1229 1,1230 1,1231 1,1232 1,1233 1,1234 1,1352 1,1353 1,1354 1,1355 1,1356 1,1357 1,1358 1,1359 1,1360 1,1361 1,1364 1,1371 1,1372 1,1373 1,1374 1,1375 1,1376 1,1377 1,1378 1,1379 1,1380 1,1381 1,1382 1,1386 1,1383 1,1385 1,1384 1,1546 1,1547 1,1548 1,1549 1,1550 1,1551 1,1552 1,1553 1,1554 1,1555 1,1556 1,1557 1,1558 1,1559 1,1560 1,1561 1,1893 1,1894 1,1895 1,1896 1,1897 1,1898 1,1899 1,1900 1,1901 1,1801 1,1802 1,1803 1,1804 1,1805 1,1806 1,1807 1,1808 1,1809 1,1810 1,1811 1,1812 1,408 1,409 1,410 1,411 1,412 1,413 1,414 1,415 1,416 1,417 1,418 1,1813 1,1814 1,1815 1,1816 1,1817 1,1818 1,1819 1,1820 1,1821 1,1822 1,1823 1,1824 1,1825 1,1826 1,1827 1,1828 1,1829 1,1830 1,1831 1,1832 1,1833 1,1834 1,1835 1,1836 1,1837 1,1838 1,1839 1,1840 1,1841 1,1842 1,1843 1,1844 1,1845 1,1846 1,1847 1,1848 1,1849 1,1850 1,1851 1,1852 1,1853 1,1854 1,1855 1,1856 1,1857 1,1858 1,1859 1,1860 1,1861 1,1862 1,1863 1,1864 1,1865 1,1866 1,1867 1,1868 1,1869 1,1870 1,1871 1,1872 1,1873 1,1874 1,1875 1,1876 1,1877 1,1878 1,1879 1,1880 1,1881 1,1882 1,1883 1,1884 1,1885 1,1886 1,1887 1,1888 1,1889 1,1890 1,1891 1,1892 1,1969 1,1970 1,1971 1,1972 1,1973 1,1974 1,1975 1,1976 1,1977 1,1978 1,1979 1,1980 1,1981 1,1982 1,1983 1,1984 1,1985 1,1942 1,1943 1,1944 1,1945 1,1946 1,1947 1,1948 1,1949 1,1950 1,1951 1,1952 1,1953 1,1954 1,1955 1,1956 1,2099 1,2100 1,2101 1,2102 1,2103 1,2104 1,2105 1,2106 1,2107 1,2108 1,2109 1,2110 1,2111 1,2112 1,2554 1,2555 1,2556 1,2557 1,2558 1,2559 1,2560 1,2561 1,2562 1,2563 1,2564 1,3132 1,3133 1,3134 1,3135 1,3136 1,3137 1,3138 1,3139 1,3140 1,3141 1,3142 1,3143 1,3144 1,3145 1,3451 1,3256 1,3467 1,3468 1,3469 1,3470 1,3471 1,3472 1,3473 1,3474 1,3475 1,3476 1,3477 1,3262 1,3268 1,3263 1,3266 1,3255 1,3259 1,3260 1,3273 1,3265 1,3274 1,3267 1,3261 1,3272 1,3257 1,3258 1,3270 1,3271 1,3254 1,3275 1,3269 1,3253 1,323 1,324 1,325 1,326 1,327 1,328 1,329 1,330 1,331 1,332 1,333 1,334 1,335 1,336 1,3264 1,3455 1,3456 1,3457 1,3458 1,3459 1,3460 1,3461 1,3462 1,3463 1,3464 1,3465 1,3466 1,1414 1,1415 1,1416 1,1417 1,1418 1,1419 1,1420 1,1421 1,1422 1,1423 1,1424 1,1425 1,1426 1,1427 1,1428 1,1429 1,1430 1,1431 1,1432 1,1433 1,1444 1,1445 1,1446 1,1447 1,1448 1,1449 1,1450 1,1451 1,1452 1,1453 1,1454 1,1773 1,1774 1,1775 1,1776 1,1777 1,1778 1,1779 1,1780 1,1781 1,1782 1,1783 1,1784 1,1785 1,1786 1,1787 1,1788 1,1789 1,1790 1,282 1,283 1,284 1,285 1,286 1,287 1,288 1,289 1,290 1,291 1,292 1,293 1,294 1,295 1,296 1,297 1,298 1,299 1,300 1,301 1,302 1,303 1,304 1,305 1,306 1,307 1,308 1,309 1,310 1,311 1,312 1,2216 1,2217 1,2218 1,2219 1,2220 1,2221 1,2222 1,2223 1,2224 1,2225 1,2226 1,2227 1,2228 1,3038 1,3039 1,3040 1,3041 1,3042 1,3043 1,3044 1,3045 1,3046 1,3047 1,3048 1,3049 1,3050 1,3051 1,1 1,6 1,7 1,8 1,9 1,10 1,11 1,12 1,13 1,14 1,15 1,16 1,17 1,18 1,19 1,20 1,21 1,22 1,2 1,3 1,4 1,5 1,23 1,24 1,25 1,26 1,27 1,28 1,29 1,30 1,31 1,32 1,33 1,34 1,35 1,36 1,37 1,38 1,39 1,40 1,41 1,42 1,43 1,44 1,45 1,46 1,47 1,48 1,49 1,50 1,51 1,52 1,53 1,54 1,55 1,56 1,57 1,58 1,59 1,60 1,61 1,62 1,85 1,86 1,87 1,88 1,89 1,90 1,91 1,92 1,93 1,94 1,95 1,96 1,97 1,98 1,675 1,676 1,677 1,678 1,679 1,680 1,681 1,682 1,683 1,684 1,685 1,686 1,687 1,688 1,689 1,690 1,691 1,692 1,693 1,694 1,695 1,696 1,697 1,698 1,699 1,700 1,701 1,702 1,703 1,704 1,705 1,706 1,707 1,708 1,709 1,710 1,711 1,712 1,713 1,714 1,2609 1,2610 1,2611 1,2612 1,2613 1,2614 1,2615 1,2616 1,2617 1,2618 1,2619 1,2620 1,2621 1,2622 1,2623 1,2624 1,2625 1,2626 1,2627 1,2628 1,2629 1,2630 1,2631 1,2632 1,2633 1,2634 1,2635 1,2636 1,2637 1,2638 1,489 1,490 1,491 1,492 1,493 1,494 1,495 1,496 1,497 1,498 1,499 1,500 1,816 1,817 1,818 1,819 1,820 1,821 1,822 1,823 1,824 1,825 1,745 1,746 1,747 1,748 1,749 1,750 1,751 1,752 1,753 1,754 1,755 1,756 1,757 1,758 1,759 1,760 1,620 1,621 1,622 1,623 1,761 1,762 1,763 1,764 1,765 1,766 1,767 1,768 1,769 1,770 1,771 1,772 1,773 1,774 1,775 1,776 1,777 1,778 1,779 1,780 1,781 1,782 1,783 1,784 1,785 1,543 1,544 1,545 1,546 1,547 1,548 1,549 1,786 1,787 1,788 1,789 1,790 1,791 1,792 1,793 1,794 1,795 1,796 1,797 1,798 1,799 1,800 1,801 1,802 1,803 1,804 1,805 1,806 1,807 1,808 1,809 1,810 1,811 1,812 1,813 1,814 1,815 1,826 1,827 1,828 1,829 1,830 1,831 1,832 1,833 1,834 1,835 1,836 1,837 1,838 1,839 1,840 1,841 1,2639 1,2640 1,2641 1,2642 1,2643 1,2644 1,2645 1,2646 1,2647 1,2648 1,2649 1,3225 1,949 1,950 1,951 1,952 1,953 1,954 1,955 1,956 1,957 1,958 1,959 1,960 1,961 1,962 1,963 1,1020 1,1021 1,1022 1,1023 1,1024 1,1025 1,1026 1,1027 1,1028 1,1029 1,1030 1,1031 1,1032 1,989 1,990 1,991 1,992 1,993 1,994 1,995 1,996 1,997 1,998 1,999 1,1000 1,1001 1,1002 1,1003 1,1004 1,1005 1,1006 1,1007 1,1008 1,351 1,352 1,353 1,354 1,355 1,356 1,357 1,358 1,359 1,1146 1,1147 1,1148 1,1149 1,1150 1,1151 1,1152 1,1153 1,1154 1,1155 1,1156 1,1157 1,1158 1,1159 1,1160 1,1161 1,1162 1,1163 1,1164 1,1165 1,1166 1,1167 1,1168 1,1169 1,1170 1,1171 1,1172 1,1173 1,1235 1,1236 1,1237 1,1238 1,1239 1,1240 1,1241 1,1242 1,1243 1,1244 1,1256 1,1257 1,1258 1,1259 1,1260 1,1261 1,1262 1,1263 1,1264 1,1265 1,1266 1,1267 1,1305 1,1306 1,1307 1,1308 1,1309 1,1310 1,1311 1,1312 1,1313 1,1314 1,1315 1,1316 1,1317 1,1318 1,1319 1,1320 1,1321 1,1322 1,1323 1,1324 1,1201 1,1202 1,1203 1,1204 1,1205 1,1206 1,1207 1,1208 1,1209 1,1210 1,1211 1,1393 1,1362 1,1363 1,1365 1,1366 1,1367 1,1368 1,1369 1,1370 1,1406 1,1407 1,1408 1,1409 1,1410 1,1411 1,1412 1,1413 1,1395 1,1396 1,1397 1,1398 1,1399 1,1400 1,1401 1,1402 1,1403 1,1404 1,1405 1,1434 1,1435 1,1436 1,1437 1,1438 1,1439 1,1440 1,1441 1,1442 1,1443 1,1479 1,1480 1,1481 1,1482 1,1483 1,1484 1,1485 1,1486 1,1487 1,1488 1,1489 1,1490 1,1491 1,1492 1,1493 1,1494 1,1495 1,1496 1,1497 1,1498 1,1499 1,1500 1,1501 1,1502 1,1503 1,1504 1,1505 1,436 1,437 1,438 1,439 1,440 1,441 1,442 1,443 1,444 1,445 1,446 1,447 1,448 1,449 1,450 1,451 1,452 1,453 1,454 1,455 1,1562 1,1563 1,1564 1,1565 1,1566 1,1567 1,1568 1,1569 1,1570 1,1571 1,1572 1,1573 1,1574 1,1575 1,1576 1,337 1,338 1,339 1,340 1,341 1,342 1,343 1,344 1,345 1,346 1,347 1,348 1,349 1,350 1,1577 1,1578 1,1579 1,1580 1,1581 1,1582 1,1583 1,1584 1,1585 1,1586 1,1587 1,1588 1,1589 1,1590 1,1591 1,1592 1,1593 1,1594 1,1595 1,1596 1,1597 1,1598 1,1599 1,1600 1,1601 1,1602 1,1603 1,1604 1,1605 1,1606 1,1607 1,1608 1,1609 1,1610 1,1611 1,1612 1,1613 1,1614 1,1615 1,1616 1,1617 1,1618 1,1619 1,1620 1,1621 1,1622 1,1623 1,1624 1,1625 1,1626 1,1627 1,1628 1,1629 1,1630 1,1631 1,1632 1,1633 1,1634 1,1635 1,1636 1,1637 1,1638 1,1639 1,1640 1,1641 1,1642 1,1643 1,1644 1,1645 1,550 1,551 1,552 1,553 1,554 1,555 1,1646 1,1647 1,1648 1,1649 1,1650 1,1651 1,1652 1,1653 1,1654 1,1655 1,1656 1,1657 1,1658 1,1659 1,1660 1,1661 1,1662 1,1663 1,1664 1,1665 1,1666 1,1667 1,1668 1,1669 1,1670 1,1702 1,1703 1,1704 1,1705 1,1706 1,1707 1,1708 1,1709 1,1710 1,1711 1,1712 1,1713 1,1714 1,1715 1,1716 1,1745 1,1746 1,1747 1,1748 1,1749 1,1750 1,1751 1,1752 1,1753 1,1754 1,1791 1,1792 1,1793 1,1794 1,1795 1,1796 1,1797 1,1798 1,1799 1,1800 1,1986 1,1987 1,1988 1,1989 1,1990 1,1991 1,1992 1,1993 1,1994 1,1995 1,1996 1,1997 1,1998 1,1999 1,2000 1,2001 1,2002 1,2003 1,2004 1,2005 1,2006 1,2007 1,2008 1,2009 1,2010 1,2011 1,2012 1,2013 1,2014 1,2015 1,2016 1,2017 1,2018 1,2019 1,2020 1,2021 1,2022 1,2023 1,2024 1,2025 1,2026 1,2027 1,2028 1,2029 1,2093 1,2094 1,2095 1,2096 1,2097 1,2098 1,3276 1,3277 1,3278 1,3279 1,3280 1,3281 1,3282 1,3283 1,3284 1,3285 1,3286 1,3287 1,2113 1,2114 1,2115 1,2116 1,2117 1,2118 1,2119 1,2120 1,2121 1,2122 1,2123 1,2124 1,2139 1,2140 1,2141 1,2142 1,2143 1,2144 1,2145 1,2146 1,2147 1,2148 1,2149 1,2150 1,2151 1,2152 1,2153 1,2154 1,2155 1,2156 1,2157 1,2158 1,2159 1,2160 1,2161 1,2162 1,2163 1,2164 1,2178 1,2179 1,2180 1,2181 1,2182 1,2183 1,2184 1,2185 1,2186 1,2187 1,2188 1,2189 1,2190 1,2191 1,2192 1,2193 1,2194 1,2195 1,2196 1,2197 1,2198 1,2199 1,2200 1,2201 1,2202 1,2203 1,2204 1,2205 1,2206 1,2207 1,2208 1,2209 1,2210 1,2211 1,2212 1,2213 1,2214 1,2215 1,2229 1,2230 1,2231 1,2232 1,2233 1,2234 1,2235 1,2236 1,2237 1,2650 1,2651 1,2652 1,2653 1,2654 1,2655 1,2656 1,2657 1,2658 1,2659 1,2660 1,2661 1,2662 1,2663 1,3353 1,3355 1,2254 1,2255 1,2256 1,2257 1,2258 1,2259 1,2260 1,2261 1,2262 1,2263 1,2264 1,2265 1,2266 1,2267 1,2268 1,2269 1,2270 1,419 1,420 1,421 1,422 1,423 1,424 1,425 1,426 1,427 1,428 1,429 1,430 1,431 1,432 1,433 1,434 1,435 1,2271 1,2272 1,2273 1,2274 1,2275 1,2276 1,2277 1,2278 1,2279 1,2280 1,2281 1,2296 1,2297 1,2298 1,2299 1,2300 1,2301 1,2302 1,2303 1,2304 1,2305 1,2306 1,2307 1,2308 1,2309 1,2344 1,2345 1,2346 1,2347 1,2348 1,2349 1,2350 1,2351 1,2352 1,2353 1,2354 1,2355 1,2356 1,2357 1,2375 1,2376 1,2377 1,2378 1,2379 1,2380 1,2381 1,2382 1,2383 1,2384 1,2385 1,2386 1,2387 1,2388 1,2389 1,2390 1,2391 1,2392 1,2393 1,2394 1,2395 1,2396 1,2397 1,2398 1,2399 1,2400 1,2401 1,2402 1,2403 1,2404 1,2405 1,2664 1,2665 1,2666 1,2667 1,2668 1,2669 1,2670 1,2671 1,2672 1,2673 1,2674 1,2675 1,2676 1,2677 1,2678 1,2679 1,2680 1,2681 1,2682 1,2683 1,2684 1,2685 1,2686 1,2687 1,2688 1,2689 1,2690 1,2691 1,2692 1,2693 1,2694 1,2695 1,2696 1,2697 1,2698 1,2699 1,2700 1,2701 1,2702 1,2703 1,2704 1,2406 1,2407 1,2408 1,2409 1,2410 1,2411 1,2412 1,2413 1,2414 1,2415 1,2416 1,2417 1,2418 1,2419 1,2420 1,2421 1,2422 1,2423 1,2424 1,2425 1,2426 1,2427 1,2428 1,2429 1,2430 1,2431 1,2432 1,2433 1,570 1,573 1,577 1,580 1,581 1,571 1,579 1,582 1,572 1,575 1,578 1,574 1,576 1,3288 1,3289 1,3290 1,3291 1,3292 1,3293 1,3294 1,3295 1,3296 1,3297 1,3298 1,3299 1,2434 1,2435 1,2436 1,2437 1,2438 1,2439 1,2440 1,2441 1,2442 1,2443 1,2444 1,2445 1,2446 1,2447 1,2448 1,2449 1,2450 1,2451 1,2452 1,2453 1,2454 1,2455 1,2456 1,2457 1,2458 1,2459 1,2460 1,2461 1,2462 1,2463 1,2464 1,2465 1,2466 1,2467 1,2468 1,2469 1,2470 1,2471 1,2506 1,2507 1,2508 1,2509 1,2510 1,2511 1,2512 1,2513 1,2514 1,2515 1,2516 1,2517 1,2518 1,2519 1,2520 1,2521 1,2522 1,2542 1,2543 1,2544 1,2545 1,2546 1,2547 1,2548 1,2549 1,2550 1,2551 1,2552 1,2553 1,2565 1,2566 1,2567 1,2568 1,2569 1,2570 1,2571 1,2926 1,2927 1,2928 1,2929 1,2930 1,2931 1,2932 1,2933 1,2934 1,2935 1,2936 1,2937 1,2938 1,2939 1,2940 1,2941 1,2942 1,2943 1,2944 1,2945 1,2946 1,2947 1,2948 1,2949 1,2950 1,2951 1,2952 1,2953 1,2954 1,2955 1,2956 1,2957 1,2958 1,2959 1,2960 1,2961 1,2962 1,2963 1,3004 1,3005 1,3006 1,3007 1,3008 1,3009 1,3010 1,3011 1,3012 1,3013 1,3014 1,3015 1,3016 1,3017 1,2964 1,2965 1,2966 1,2967 1,2968 1,2969 1,2970 1,2971 1,2972 1,2973 1,2974 1,2975 1,2976 1,2977 1,2978 1,2979 1,2980 1,2981 1,2982 1,2983 1,2984 1,2985 1,2986 1,2987 1,2988 1,2989 1,2990 1,2991 1,2992 1,2993 1,2994 1,2995 1,2996 1,2997 1,2998 1,2999 1,3000 1,3001 1,3002 1,3003 1,3018 1,3019 1,3020 1,3021 1,3022 1,3023 1,3024 1,3025 1,3026 1,3027 1,3028 1,3029 1,3030 1,3031 1,3032 1,3033 1,3034 1,3035 1,3036 1,3037 1,3064 1,3065 1,3066 1,3067 1,3068 1,3069 1,3070 1,3071 1,3072 1,3073 1,3074 1,3075 1,3076 1,3077 1,3078 1,3079 1,3080 1,3052 1,3053 1,3054 1,3055 1,3056 1,3057 1,3058 1,3059 1,3060 1,3061 1,3062 1,3063 1,3081 1,3082 1,3083 1,3084 1,3085 1,3086 1,3087 1,3088 1,3089 1,3090 1,3091 1,3092 1,3093 1,3094 1,3095 1,3096 1,3097 1,3098 1,3099 1,3100 1,3101 1,3102 1,3103 1,3104 1,3105 1,3106 1,3107 1,3108 1,3109 1,3110 1,3111 1,3112 1,3113 1,3114 1,3115 1,3116 1,2731 1,2732 1,2733 1,2734 1,2735 1,2736 1,2737 1,2738 1,2739 1,2740 1,2741 1,2742 1,2743 1,2744 1,2745 1,2746 1,2747 1,2748 1,2749 1,2750 1,111 1,112 1,113 1,114 1,115 1,116 1,117 1,118 1,119 1,120 1,121 1,122 1,1073 1,1074 1,1075 1,1076 1,1077 1,1078 1,1079 1,1080 1,1081 1,1082 1,1083 1,1084 1,1085 1,1086 1,2125 1,2126 1,2127 1,2128 1,2129 1,2130 1,2131 1,2132 1,2133 1,2134 1,2135 1,2136 1,2137 1,2138 1,3503 1,360 1,361 1,362 1,363 1,364 1,365 1,366 1,367 1,368 1,369 1,370 1,371 1,372 1,373 1,3354 1,3351 1,1532 1,1533 1,1534 1,1535 1,1536 1,1537 1,1538 1,1539 1,1540 1,1541 1,1542 1,1543 1,1544 1,1545 1,1957 1,1958 1,1959 1,1960 1,1961 1,1962 1,1963 1,1964 1,1965 1,1966 1,1967 1,1968 3,3250 3,2819 3,2820 3,2821 3,2822 3,2823 3,2824 3,2825 3,2826 3,2827 3,2828 3,2829 3,2830 3,2831 3,2832 3,2833 3,2834 3,2835 3,2836 3,2837 3,2838 3,3226 3,3227 3,3228 3,3229 3,3230 3,3231 3,3232 3,3233 3,3234 3,3235 3,3236 3,3237 3,3238 3,3239 3,3240 3,3241 3,3242 3,3243 3,3244 3,3245 3,3246 3,3247 3,3248 3,3249 3,2839 3,2840 3,2841 3,2842 3,2843 3,2844 3,2845 3,2846 3,2847 3,2848 3,2849 3,2850 3,2851 3,2852 3,2853 3,2854 3,2855 3,2856 3,3166 3,3167 3,3168 3,3171 3,3223 3,2858 3,2861 3,2865 3,2868 3,2871 3,2873 3,2877 3,2880 3,2883 3,2885 3,2888 3,2893 3,2894 3,2898 3,2901 3,2904 3,2906 3,2911 3,2913 3,2915 3,2917 3,2919 3,2921 3,2923 3,2925 3,2859 3,2860 3,2864 3,2867 3,2869 3,2872 3,2878 3,2879 3,2884 3,2887 3,2889 3,2892 3,2896 3,2897 3,2902 3,2905 3,2907 3,2910 3,2914 3,2916 3,2918 3,2920 3,2922 3,2924 3,2857 3,2862 3,2863 3,2866 3,2870 3,2874 3,2875 3,2876 3,2881 3,2882 3,2886 3,2890 3,2891 3,2895 3,2899 3,2900 3,2903 3,2908 3,2909 3,2912 3,3165 3,3169 3,3170 3,3252 3,3224 3,3251 3,3340 3,3339 3,3338 3,3337 3,3341 3,3345 3,3342 3,3346 3,3343 3,3347 3,3344 3,3348 3,3360 3,3361 3,3362 3,3363 3,3364 3,3172 3,3173 3,3174 3,3175 3,3176 3,3177 3,3178 3,3179 3,3180 3,3181 3,3182 3,3183 3,3184 3,3185 3,3186 3,3187 3,3188 3,3189 3,3190 3,3191 3,3192 3,3193 3,3194 3,3195 3,3196 3,3197 3,3198 3,3199 3,3200 3,3201 3,3202 3,3203 3,3204 3,3205 3,3206 3,3428 3,3207 3,3208 3,3209 3,3210 3,3211 3,3212 3,3429 3,3213 3,3214 3,3215 3,3216 3,3217 3,3218 3,3219 3,3220 3,3221 3,3222 5,51 5,52 5,53 5,54 5,55 5,56 5,57 5,58 5,59 5,60 5,61 5,62 5,798 5,799 5,800 5,801 5,802 5,803 5,804 5,805 5,806 5,3225 5,1325 5,1326 5,1327 5,1328 5,1329 5,1330 5,1331 5,1332 5,1333 5,1334 5,1557 5,2506 5,2591 5,2592 5,2593 5,2594 5,2595 5,2596 5,2597 5,2598 5,2599 5,2600 5,2601 5,2602 5,2603 5,2604 5,2605 5,2606 5,2607 5,2608 5,2627 5,2631 5,2638 5,1158 5,1159 5,1160 5,1161 5,1162 5,1163 5,1164 5,1165 5,1166 5,1167 5,1168 5,1169 5,1170 5,1171 5,1172 5,1173 5,1174 5,1175 5,1176 5,1177 5,1178 5,1179 5,1180 5,1181 5,1182 5,1183 5,1184 5,1185 5,1186 5,1187 5,1414 5,1415 5,1416 5,1417 5,1418 5,1419 5,1420 5,1421 5,1422 5,1423 5,1424 5,1425 5,1426 5,1427 5,1428 5,1429 5,1430 5,1431 5,1432 5,1433 5,1801 5,1802 5,1803 5,1804 5,1805 5,1806 5,1807 5,1808 5,1809 5,1810 5,1811 5,1812 5,2003 5,2004 5,2005 5,2006 5,2007 5,2008 5,2009 5,2010 5,2011 5,2012 5,2013 5,2014 5,2193 5,2194 5,2195 5,2196 5,2197 5,2198 5,2199 5,2200 5,2201 5,2202 5,2203 5,424 5,428 5,430 5,434 5,2310 5,2311 5,2312 5,2313 5,2314 5,2315 5,2316 5,2317 5,2282 5,2283 5,2284 5,2358 5,2359 5,2360 5,2361 5,2362 5,2363 5,2364 5,2365 5,2366 5,2367 5,2368 5,2369 5,2370 5,2371 5,2372 5,2373 5,2374 5,2420 5,2421 5,2422 5,2423 5,2424 5,2425 5,2426 5,2427 5,2488 5,2489 5,2511 5,2512 5,2513 5,2711 5,2715 5,3365 5,3366 5,3367 5,3368 5,3369 5,3370 5,3371 5,3372 5,3373 5,3374 5,2926 5,2927 5,2928 5,2929 5,2930 5,2931 5,2932 5,2933 5,2934 5,2935 5,2936 5,2937 5,3075 5,3076 5,166 5,167 5,168 5,169 5,170 5,171 5,172 5,173 5,174 5,175 5,176 5,177 5,178 5,179 5,180 5,181 5,182 5,3426 5,2625 5,816 5,817 5,818 5,819 5,820 5,821 5,822 5,823 5,824 5,825 5,768 5,769 5,770 5,771 5,772 5,773 5,774 5,775 5,776 5,777 5,778 5,909 5,910 5,911 5,912 5,913 5,914 5,915 5,916 5,917 5,918 5,919 5,920 5,921 5,922 5,935 5,936 5,937 5,938 5,939 5,940 5,941 5,942 5,943 5,944 5,945 5,946 5,947 5,948 5,3301 5,3300 5,3302 5,3303 5,3304 5,3305 5,3306 5,3307 5,3308 5,3309 5,3310 5,3311 5,3312 5,3313 5,3314 5,3315 5,3316 5,3317 5,3318 5,1256 5,1257 5,1258 5,1259 5,1260 5,1261 5,1262 5,1263 5,1264 5,1265 5,1266 5,1267 5,2490 5,2542 5,2543 5,2544 5,2545 5,2546 5,2547 5,2548 5,2549 5,2550 5,2551 5,2552 5,2553 5,3411 5,3403 5,3423 5,1212 5,1213 5,1214 5,1215 5,1216 5,1217 5,1218 5,1219 5,1220 5,1221 5,1222 5,1223 5,1224 5,1225 5,1226 5,1227 5,1228 5,1229 5,1230 5,1231 5,1232 5,1233 5,1234 5,1434 5,1435 5,1436 5,1437 5,1438 5,1439 5,1440 5,1441 5,1442 5,1443 5,2204 5,2205 5,2206 5,2207 5,2208 5,2209 5,2210 5,2211 5,2212 5,2213 5,2214 5,2215 5,3404 5,2491 5,2492 5,2493 5,3028 5,3029 5,3030 5,3031 5,3032 5,3033 5,3034 5,3035 5,3036 5,3037 5,23 5,24 5,25 5,26 5,27 5,28 5,29 5,30 5,31 5,32 5,33 5,34 5,35 5,36 5,37 5,111 5,112 5,113 5,114 5,115 5,116 5,117 5,118 5,119 5,120 5,121 5,122 5,515 5,516 5,517 5,518 5,519 5,520 5,521 5,522 5,523 5,524 5,525 5,526 5,527 5,528 5,269 5,270 5,271 5,272 5,273 5,274 5,275 5,276 5,277 5,278 5,279 5,280 5,281 5,891 5,892 5,893 5,894 5,895 5,896 5,897 5,898 5,899 5,900 5,901 5,902 5,903 5,904 5,905 5,906 5,907 5,908 5,1105 5,1106 5,1107 5,1108 5,1109 5,1110 5,1111 5,1112 5,1113 5,1114 5,1115 5,1116 5,1117 5,1118 5,1119 5,1120 5,470 5,471 5,472 5,473 5,474 5,3424 5,2690 5,2691 5,2692 5,2693 5,2694 5,2695 5,2696 5,2697 5,2698 5,2699 5,2700 5,2701 5,2702 5,2703 5,2704 5,2494 5,2514 5,2515 5,2516 5,2517 5,3132 5,3133 5,3134 5,3135 5,3136 5,3137 5,3138 5,3139 5,3140 5,3141 5,3142 5,3143 5,3144 5,3145 5,3408 5,3 5,4 5,5 5,38 5,39 5,40 5,41 5,42 5,43 5,44 5,45 5,46 5,47 5,48 5,49 5,50 5,826 5,827 5,828 5,829 5,830 5,831 5,832 5,833 5,834 5,835 5,836 5,837 5,838 5,839 5,840 5,841 5,949 5,950 5,951 5,952 5,953 5,954 5,955 5,956 5,957 5,958 5,959 5,960 5,961 5,962 5,963 5,475 5,476 5,477 5,478 5,479 5,480 5,3354 5,3351 5,1395 5,1396 5,1397 5,1398 5,1399 5,1400 5,1401 5,1402 5,1403 5,1404 5,1405 5,1455 5,1456 5,1457 5,1458 5,1459 5,1460 5,1461 5,1462 5,1463 5,1464 5,1465 5,1520 5,1521 5,1522 5,1523 5,1524 5,1525 5,1526 5,1527 5,1528 5,1529 5,1530 5,1531 5,3276 5,3277 5,3278 5,3279 5,3280 5,3281 5,3282 5,3283 5,3284 5,3285 5,3286 5,3287 5,2125 5,2126 5,2127 5,2128 5,2129 5,2130 5,2131 5,2132 5,2133 5,2134 5,2135 5,2136 5,2137 5,2138 5,3410 5,2476 5,2484 5,2495 5,2496 5,2497 5,2498 5,2709 5,2710 5,2712 5,3038 5,3039 5,3040 5,3041 5,3042 5,3043 5,3044 5,3045 5,3046 5,3047 5,3048 5,3049 5,3050 5,3051 5,3077 5,77 5,78 5,79 5,80 5,81 5,82 5,83 5,84 5,3421 5,246 5,247 5,248 5,249 5,250 5,251 5,252 5,253 5,254 5,255 5,256 5,257 5,258 5,259 5,260 5,261 5,262 5,263 5,264 5,265 5,266 5,267 5,268 5,786 5,787 5,788 5,789 5,790 5,791 5,792 5,793 5,794 5,795 5,796 5,797 5,1562 5,1563 5,1564 5,1565 5,1566 5,1567 5,1568 5,1569 5,1570 5,1571 5,1572 5,1573 5,1574 5,1575 5,1576 5,1839 5,1840 5,1841 5,1842 5,1843 5,1844 5,1845 5,1846 5,1847 5,1848 5,1849 5,1850 5,1851 5,1852 5,1986 5,1987 5,1988 5,1989 5,1990 5,1991 5,1992 5,1993 5,1994 5,1995 5,1996 5,1997 5,1998 5,1999 5,2000 5,2001 5,2002 5,3415 5,2650 5,2651 5,2652 5,2653 5,2654 5,2655 5,2656 5,2657 5,2658 5,2659 5,2660 5,2661 5,2662 5,2663 5,2296 5,2297 5,2298 5,2299 5,2300 5,2301 5,2302 5,2303 5,2304 5,2305 5,2306 5,2307 5,2308 5,2309 5,2334 5,2335 5,2336 5,2337 5,2338 5,2339 5,2340 5,2341 5,2342 5,2343 5,2434 5,2435 5,2436 5,2437 5,2438 5,2439 5,2440 5,2441 5,2442 5,2443 5,2444 5,2445 5,2446 5,2447 5,2448 5,2461 5,2462 5,2463 5,2464 5,2465 5,2466 5,2467 5,2468 5,2469 5,2470 5,2471 5,2478 5,2518 5,2519 5,2520 5,2521 5,2522 5,456 5,457 5,458 5,459 5,460 5,461 5,462 5,463 5,464 5,465 5,466 5,467 5,3078 5,3079 5,3080 5,3416 5,923 5,924 5,925 5,926 5,927 5,928 5,929 5,930 5,931 5,932 5,933 5,934 5,1020 5,1021 5,1022 5,1023 5,1024 5,1025 5,1026 5,1027 5,1028 5,1029 5,1030 5,1031 5,1032 5,481 5,482 5,483 5,484 5,1188 5,1189 5,1190 5,1191 5,1192 5,1193 5,1194 5,1195 5,1196 5,1197 5,1198 5,1199 5,1200 5,436 5,437 5,438 5,439 5,440 5,441 5,442 5,443 5,444 5,445 5,446 5,447 5,448 5,449 5,450 5,451 5,453 5,454 5,455 5,337 5,338 5,339 5,340 5,341 5,342 5,343 5,344 5,345 5,346 5,347 5,348 5,349 5,350 5,1577 5,1578 5,1579 5,1580 5,1581 5,1582 5,1583 5,1584 5,1585 5,1586 5,1861 5,1862 5,1863 5,1864 5,1865 5,1866 5,1867 5,1868 5,1869 5,1870 5,1871 5,1872 5,1873 5,3359 5,2406 5,2407 5,2408 5,2409 5,2410 5,2411 5,2412 5,2413 5,2414 5,2415 5,2416 5,2417 5,2418 5,2419 5,2499 5,2706 5,2708 5,2713 5,2716 5,2720 5,2721 5,2722 5,2723 5,2724 5,2725 5,2726 5,2727 5,2728 5,2729 5,2730 5,2565 5,2566 5,2567 5,2568 5,2569 5,2570 5,2571 5,2781 5,2782 5,2783 5,2784 5,2785 5,2786 5,2787 5,2788 5,2789 5,2790 5,2791 5,2792 5,2793 5,2794 5,2795 5,2796 5,2797 5,2798 5,2799 5,2800 5,2801 5,2802 5,2975 5,2976 5,2977 5,2978 5,2979 5,2980 5,2981 5,2982 5,2983 5,2984 5,2985 5,2986 5,183 5,184 5,185 5,186 5,187 5,188 5,189 5,190 5,191 5,192 5,193 5,205 5,206 5,207 5,208 5,209 5,210 5,211 5,212 5,213 5,214 5,215 5,216 5,217 5,218 5,219 5,220 5,221 5,222 5,3417 5,583 5,584 5,585 5,586 5,587 5,588 5,589 5,590 5,591 5,592 5,593 5,594 5,595 5,596 5,976 5,977 5,978 5,979 5,984 5,1087 5,1088 5,1089 5,1090 5,1091 5,1092 5,1093 5,1094 5,1095 5,1096 5,1097 5,1098 5,1099 5,1100 5,1101 5,1305 5,1306 5,1307 5,1308 5,1309 5,1310 5,1311 5,1312 5,1313 5,1314 5,1315 5,1316 5,1317 5,1318 5,1319 5,1320 5,1321 5,1322 5,1323 5,1324 5,1406 5,1407 5,1408 5,1409 5,1410 5,1411 5,1412 5,1413 5,1686 5,1687 5,1688 5,1689 5,1690 5,1691 5,1692 5,1693 5,1694 5,1695 5,1696 5,1697 5,1698 5,1699 5,1700 5,1701 5,408 5,409 5,410 5,411 5,412 5,413 5,414 5,415 5,416 5,417 5,418 5,1813 5,1814 5,1815 5,1816 5,1817 5,1818 5,1819 5,1820 5,1821 5,1822 5,1823 5,1824 5,1825 5,1826 5,1827 5,1828 5,1969 5,1970 5,1971 5,1972 5,1973 5,1974 5,1975 5,1976 5,1977 5,1978 5,1979 5,1980 5,1981 5,1982 5,1983 5,1984 5,1985 5,2113 5,2114 5,2115 5,2116 5,2117 5,2118 5,2119 5,2120 5,2121 5,2122 5,2123 5,2124 5,2149 5,2150 5,2151 5,2152 5,2153 5,2154 5,2155 5,2156 5,2157 5,2158 5,2159 5,2160 5,2161 5,2162 5,2163 5,2164 5,2676 5,2677 5,2678 5,2679 5,2680 5,2681 5,2682 5,2683 5,2684 5,2685 5,2686 5,2687 5,2688 5,2689 5,3418 5,2500 5,2501 5,2803 5,2804 5,2805 5,2806 5,2807 5,2808 5,2809 5,2810 5,2811 5,2812 5,2813 5,2814 5,2815 5,2816 5,2817 5,2818 5,2949 5,2950 5,2951 5,2952 5,2953 5,2954 5,2955 5,2956 5,2957 5,2958 5,2959 5,2960 5,2961 5,2962 5,2963 5,3004 5,3005 5,3006 5,3007 5,3008 5,3009 5,3010 5,3011 5,3012 5,3013 5,3014 5,3015 5,3016 5,3017 5,3092 5,3093 5,3094 5,3095 5,3096 5,3097 5,3098 5,3099 5,3100 5,3101 5,3102 5,3103 5,3409 5,299 5,300 5,301 5,302 5,303 5,304 5,305 5,306 5,307 5,308 5,309 5,310 5,311 5,312 5,851 5,852 5,853 5,854 5,855 5,856 5,857 5,858 5,859 5,860 5,861 5,862 5,863 5,864 5,865 5,866 5,867 5,868 5,869 5,870 5,871 5,872 5,873 5,874 5,875 5,876 5,1057 5,1058 5,1059 5,1060 5,1061 5,1062 5,1063 5,1064 5,1065 5,1066 5,1067 5,1068 5,1069 5,1070 5,1071 5,1072 5,501 5,502 5,503 5,504 5,505 5,506 5,507 5,508 5,509 5,510 5,511 5,512 5,513 5,514 5,1444 5,1445 5,1446 5,1447 5,1448 5,1449 5,1450 5,1451 5,1452 5,1453 5,1454 5,1496 5,1497 5,1498 5,1499 5,1500 5,1501 5,1502 5,1503 5,1504 5,1505 5,1671 5,1672 5,1673 5,1674 5,1675 5,1676 5,1677 5,1678 5,1679 5,1680 5,1681 5,1682 5,1683 5,1684 5,1685 5,2044 5,2045 5,2046 5,2047 5,2048 5,2049 5,2050 5,2051 5,2052 5,2053 5,2054 5,2055 5,2056 5,2057 5,2058 5,2059 5,2060 5,2061 5,2062 5,2063 5,2064 5,2238 5,2239 5,2240 5,2241 5,2242 5,2243 5,2244 5,2245 5,2246 5,2247 5,2248 5,2249 5,2250 5,2251 5,2252 5,2253 5,2391 5,2392 5,2393 5,2394 5,2395 5,2396 5,2397 5,2398 5,2399 5,2400 5,2401 5,2402 5,2403 5,2404 5,2405 5,570 5,573 5,577 5,580 5,581 5,571 5,579 5,582 5,572 5,575 5,578 5,574 5,576 5,2707 5,2714 5,2717 5,2718 5,3146 5,3147 5,3148 5,3149 5,3150 5,3151 5,3152 5,3153 5,3154 5,3155 5,3156 5,3157 5,3158 5,3159 5,3160 5,3161 5,3162 5,3163 5,3164 5,3438 5,3442 5,3436 5,3454 5,3432 5,3447 5,3434 5,3449 5,3445 5,3439 5,3435 5,3448 5,3437 5,3446 5,3444 5,3451 5,3430 5,3482 5,3485 5,3499 5,3490 5,3489 5,3492 5,3493 5,3498 5,3481 5,3503 8,3427 8,3357 8,1 8,6 8,7 8,8 8,9 8,10 8,11 8,12 8,13 8,14 8,15 8,16 8,17 8,18 8,19 8,20 8,21 8,22 8,3411 8,3412 8,3419 8,2 8,3 8,4 8,5 8,23 8,24 8,25 8,26 8,27 8,28 8,29 8,30 8,31 8,32 8,33 8,34 8,35 8,36 8,37 8,3256 8,3350 8,3349 8,38 8,39 8,40 8,41 8,42 8,43 8,44 8,45 8,46 8,47 8,48 8,49 8,50 8,3403 8,51 8,52 8,53 8,54 8,55 8,56 8,57 8,58 8,59 8,60 8,61 8,62 8,3406 8,379 8,391 8,63 8,64 8,65 8,66 8,67 8,68 8,69 8,70 8,71 8,72 8,73 8,74 8,75 8,76 8,77 8,78 8,79 8,80 8,81 8,82 8,83 8,84 8,85 8,86 8,87 8,88 8,89 8,90 8,91 8,92 8,93 8,94 8,95 8,96 8,97 8,98 8,99 8,100 8,101 8,102 8,103 8,104 8,105 8,106 8,107 8,108 8,109 8,110 8,3402 8,3389 8,3390 8,3391 8,3392 8,3393 8,3394 8,3395 8,3396 8,3397 8,3398 8,3399 8,3400 8,3401 8,3262 8,376 8,397 8,382 8,111 8,112 8,113 8,114 8,115 8,116 8,117 8,118 8,119 8,120 8,121 8,122 8,389 8,404 8,406 8,3421 8,380 8,394 8,3268 8,3413 8,3263 8,123 8,124 8,125 8,126 8,127 8,128 8,129 8,130 8,2572 8,2573 8,2574 8,2575 8,2576 8,2577 8,2578 8,2579 8,2580 8,2581 8,2582 8,2583 8,2584 8,2585 8,2586 8,2587 8,2588 8,2589 8,2590 8,3266 8,131 8,132 8,133 8,134 8,135 8,136 8,137 8,138 8,139 8,140 8,141 8,142 8,143 8,144 8,145 8,146 8,147 8,148 8,149 8,150 8,151 8,152 8,153 8,154 8,155 8,156 8,157 8,158 8,159 8,160 8,161 8,162 8,163 8,164 8,165 8,166 8,167 8,168 8,169 8,170 8,171 8,172 8,173 8,174 8,175 8,176 8,177 8,178 8,179 8,180 8,181 8,182 8,3426 8,3416 8,183 8,184 8,185 8,186 8,187 8,188 8,189 8,190 8,191 8,192 8,193 8,194 8,195 8,196 8,197 8,198 8,199 8,200 8,201 8,202 8,203 8,204 8,515 8,516 8,517 8,518 8,519 8,520 8,521 8,522 8,523 8,524 8,525 8,526 8,527 8,528 8,205 8,206 8,207 8,208 8,209 8,210 8,211 8,212 8,213 8,214 8,215 8,216 8,217 8,218 8,219 8,220 8,221 8,222 8,223 8,224 8,225 8,3336 8,715 8,716 8,717 8,718 8,719 8,720 8,721 8,722 8,723 8,724 8,725 8,726 8,727 8,728 8,729 8,730 8,731 8,732 8,733 8,734 8,735 8,736 8,737 8,738 8,739 8,740 8,741 8,742 8,743 8,744 8,3324 8,3417 8,226 8,227 8,228 8,229 8,230 8,231 8,232 8,233 8,234 8,235 8,236 8,237 8,238 8,239 8,240 8,241 8,242 8,243 8,244 8,245 8,246 8,247 8,248 8,249 8,250 8,251 8,252 8,253 8,254 8,255 8,256 8,257 8,258 8,259 8,260 8,261 8,262 8,263 8,264 8,265 8,266 8,267 8,268 8,269 8,270 8,271 8,272 8,273 8,274 8,275 8,276 8,277 8,278 8,279 8,280 8,281 8,3375 8,3376 8,3377 8,3378 8,3379 8,3380 8,3381 8,3382 8,3383 8,3384 8,3385 8,3386 8,3387 8,3388 8,3255 8,282 8,283 8,284 8,285 8,286 8,287 8,288 8,289 8,290 8,291 8,292 8,293 8,294 8,295 8,296 8,297 8,298 8,299 8,300 8,301 8,302 8,303 8,304 8,305 8,306 8,307 8,308 8,309 8,310 8,311 8,312 8,2591 8,2592 8,2593 8,2594 8,2595 8,2596 8,2597 8,2598 8,2599 8,2600 8,2601 8,2602 8,2603 8,2604 8,2605 8,2606 8,2607 8,2608 8,313 8,314 8,315 8,316 8,317 8,318 8,319 8,320 8,321 8,322 8,399 8,3259 8,675 8,676 8,677 8,678 8,679 8,680 8,681 8,682 8,683 8,684 8,685 8,686 8,687 8,688 8,689 8,690 8,691 8,692 8,693 8,694 8,695 8,696 8,697 8,698 8,699 8,700 8,701 8,702 8,703 8,704 8,705 8,706 8,707 8,708 8,709 8,710 8,711 8,712 8,713 8,714 8,2609 8,2610 8,2611 8,2612 8,2613 8,2614 8,2615 8,2616 8,2617 8,2618 8,2619 8,2620 8,2621 8,2622 8,2623 8,2624 8,2625 8,2626 8,2627 8,2628 8,2629 8,2630 8,2631 8,2632 8,2633 8,2634 8,2635 8,2636 8,2637 8,2638 8,489 8,490 8,491 8,492 8,493 8,494 8,495 8,496 8,497 8,498 8,499 8,500 8,816 8,817 8,818 8,819 8,820 8,821 8,822 8,823 8,824 8,825 8,745 8,746 8,747 8,748 8,749 8,750 8,751 8,752 8,753 8,754 8,755 8,756 8,757 8,758 8,759 8,760 8,620 8,621 8,622 8,623 8,761 8,762 8,763 8,764 8,765 8,766 8,767 8,768 8,769 8,770 8,771 8,772 8,773 8,774 8,775 8,776 8,777 8,778 8,779 8,780 8,781 8,782 8,783 8,784 8,785 8,543 8,544 8,545 8,546 8,547 8,548 8,549 8,786 8,787 8,788 8,789 8,790 8,791 8,792 8,793 8,794 8,795 8,796 8,797 8,798 8,799 8,800 8,801 8,802 8,803 8,804 8,805 8,806 8,807 8,808 8,809 8,810 8,811 8,812 8,813 8,814 8,815 8,826 8,827 8,828 8,829 8,830 8,831 8,832 8,833 8,834 8,835 8,836 8,837 8,838 8,839 8,840 8,841 8,842 8,843 8,844 8,845 8,846 8,847 8,848 8,849 8,850 8,3260 8,3331 8,851 8,852 8,853 8,854 8,855 8,856 8,857 8,858 8,859 8,860 8,861 8,862 8,863 8,864 8,865 8,866 8,867 8,868 8,869 8,870 8,871 8,872 8,873 8,874 8,875 8,876 8,2639 8,2640 8,2641 8,2642 8,2643 8,2644 8,2645 8,2646 8,2647 8,2648 8,2649 8,3225 8,583 8,584 8,585 8,586 8,587 8,588 8,589 8,590 8,591 8,592 8,593 8,594 8,595 8,596 8,388 8,402 8,407 8,396 8,877 8,878 8,879 8,880 8,881 8,882 8,883 8,884 8,885 8,886 8,887 8,888 8,889 8,890 8,3405 8,891 8,892 8,893 8,894 8,895 8,896 8,897 8,898 8,899 8,900 8,901 8,902 8,903 8,904 8,905 8,906 8,907 8,908 8,909 8,910 8,911 8,912 8,913 8,914 8,915 8,916 8,917 8,918 8,919 8,920 8,921 8,922 8,3423 8,923 8,924 8,925 8,926 8,927 8,928 8,929 8,930 8,931 8,932 8,933 8,934 8,935 8,936 8,937 8,938 8,939 8,940 8,941 8,942 8,943 8,944 8,945 8,946 8,947 8,948 8,949 8,950 8,951 8,952 8,953 8,954 8,955 8,956 8,957 8,958 8,959 8,960 8,961 8,962 8,963 8,964 8,965 8,966 8,967 8,968 8,969 8,970 8,971 8,972 8,973 8,974 8,975 8,976 8,977 8,978 8,979 8,980 8,981 8,982 8,983 8,984 8,985 8,986 8,987 8,988 8,390 8,3273 8,1020 8,1021 8,1022 8,1023 8,1024 8,1025 8,1026 8,1027 8,1028 8,1029 8,1030 8,1031 8,1032 8,989 8,990 8,991 8,992 8,993 8,994 8,995 8,996 8,997 8,998 8,999 8,1000 8,1001 8,1002 8,1003 8,1004 8,1005 8,1006 8,1007 8,1008 8,1009 8,1010 8,1011 8,1012 8,1013 8,1014 8,1015 8,1016 8,1017 8,1018 8,1019 8,1033 8,1034 8,1035 8,1036 8,1037 8,1038 8,1039 8,1040 8,1041 8,1042 8,1043 8,1044 8,1045 8,1046 8,1047 8,1048 8,1049 8,1050 8,1051 8,1052 8,1053 8,1054 8,1055 8,1056 8,351 8,352 8,353 8,354 8,355 8,356 8,357 8,358 8,359 8,3332 8,1057 8,1058 8,1059 8,1060 8,1061 8,1062 8,1063 8,1064 8,1065 8,1066 8,1067 8,1068 8,1069 8,1070 8,1071 8,1072 8,624 8,625 8,626 8,627 8,628 8,629 8,630 8,631 8,632 8,633 8,634 8,635 8,636 8,637 8,638 8,639 8,640 8,641 8,642 8,643 8,644 8,645 8,1073 8,1074 8,1075 8,1076 8,1077 8,1078 8,1079 8,1080 8,1081 8,1082 8,1083 8,1084 8,1085 8,1086 8,377 8,395 8,1102 8,1103 8,1104 8,1087 8,1088 8,1089 8,1090 8,1091 8,1092 8,1093 8,1094 8,1095 8,1096 8,1097 8,1098 8,1099 8,1100 8,1101 8,1105 8,1106 8,1107 8,1108 8,1109 8,1110 8,1111 8,1112 8,1113 8,1114 8,1115 8,1116 8,1117 8,1118 8,1119 8,1120 8,1121 8,1122 8,1123 8,1124 8,1125 8,1126 8,1127 8,1128 8,1129 8,1130 8,1131 8,1132 8,501 8,502 8,503 8,504 8,505 8,506 8,507 8,508 8,509 8,510 8,511 8,512 8,513 8,514 8,1133 8,1134 8,1135 8,1136 8,1137 8,1138 8,1139 8,1140 8,1141 8,1142 8,1143 8,1144 8,1145 8,3265 8,468 8,469 8,470 8,471 8,472 8,473 8,474 8,475 8,476 8,477 8,478 8,479 8,480 8,481 8,482 8,483 8,484 8,485 8,486 8,487 8,488 8,1146 8,1147 8,1148 8,1149 8,1150 8,1151 8,1152 8,1153 8,1154 8,1155 8,1156 8,1157 8,1158 8,1159 8,1160 8,1161 8,1162 8,1163 8,1164 8,1165 8,1166 8,1167 8,1168 8,1169 8,1170 8,1171 8,1172 8,1173 8,1174 8,1175 8,1176 8,1177 8,1178 8,1179 8,1180 8,1181 8,1182 8,1183 8,1184 8,1185 8,1186 8,1187 8,3322 8,3354 8,3351 8,3422 8,405 8,3407 8,3301 8,3300 8,3302 8,3303 8,3304 8,3305 8,3306 8,3307 8,3308 8,3309 8,3310 8,3311 8,3312 8,3313 8,3314 8,3315 8,3316 8,3317 8,3318 8,1188 8,1189 8,1190 8,1191 8,1192 8,1193 8,1194 8,1195 8,1196 8,1197 8,1198 8,1199 8,1200 8,3329 8,1235 8,1236 8,1237 8,1238 8,1239 8,1240 8,1241 8,1242 8,1243 8,1244 8,1245 8,1246 8,1247 8,1248 8,1249 8,1250 8,1251 8,1252 8,1253 8,1254 8,1255 8,1256 8,1257 8,1258 8,1259 8,1260 8,1261 8,1262 8,1263 8,1264 8,1265 8,1266 8,1267 8,1268 8,1269 8,1270 8,1271 8,1272 8,1273 8,1274 8,1275 8,1276 8,1277 8,1278 8,1279 8,1280 8,1281 8,1282 8,1283 8,1284 8,1285 8,1286 8,1287 8,1288 8,1289 8,1290 8,1291 8,1292 8,1293 8,1294 8,1295 8,1296 8,1297 8,1298 8,1299 8,1300 8,1301 8,1302 8,1303 8,1304 8,1305 8,1306 8,1307 8,1308 8,1309 8,1310 8,1311 8,1312 8,1313 8,1314 8,1315 8,1316 8,1317 8,1318 8,1319 8,1320 8,1321 8,1322 8,1323 8,1324 8,1201 8,1202 8,1203 8,1204 8,1205 8,1206 8,1207 8,1208 8,1209 8,1210 8,1211 8,1325 8,1326 8,1327 8,1328 8,1329 8,1330 8,1331 8,1332 8,1333 8,1334 8,1391 8,1393 8,1388 8,1394 8,1387 8,1392 8,1389 8,1390 8,1335 8,1336 8,1337 8,1338 8,1339 8,1340 8,1341 8,1342 8,1343 8,1344 8,1345 8,1346 8,1347 8,1348 8,1349 8,1350 8,1351 8,1212 8,1213 8,1214 8,1215 8,1216 8,1217 8,1218 8,1219 8,1220 8,1221 8,1222 8,1223 8,1224 8,1225 8,1226 8,1227 8,1228 8,1229 8,1230 8,1231 8,1232 8,1233 8,1234 8,1352 8,1353 8,1354 8,1355 8,1356 8,1357 8,1358 8,1359 8,1360 8,1361 8,1362 8,1363 8,1364 8,1365 8,1366 8,1367 8,1368 8,1369 8,1370 8,1371 8,1372 8,1373 8,1374 8,1375 8,1376 8,1377 8,1378 8,1379 8,1380 8,1381 8,1382 8,1386 8,1383 8,1385 8,1384 8,1406 8,1407 8,1408 8,1409 8,1410 8,1411 8,1412 8,1413 8,1395 8,1396 8,1397 8,1398 8,1399 8,1400 8,1401 8,1402 8,1403 8,1404 8,1405 8,3274 8,3267 8,3261 8,3272 8,1414 8,1415 8,1416 8,1417 8,1418 8,1419 8,1420 8,1421 8,1422 8,1423 8,1424 8,1425 8,1426 8,1427 8,1428 8,1429 8,1430 8,1431 8,1432 8,1433 8,1434 8,1435 8,1436 8,1437 8,1438 8,1439 8,1440 8,1441 8,1442 8,1443 8,1455 8,1456 8,1457 8,1458 8,1459 8,1460 8,1461 8,1462 8,1463 8,1464 8,1465 8,1444 8,1445 8,1446 8,1447 8,1448 8,1449 8,1450 8,1451 8,1452 8,1453 8,1454 8,1466 8,1467 8,1468 8,1469 8,1470 8,1471 8,1472 8,1473 8,1474 8,1475 8,1476 8,1477 8,1478 8,1479 8,1480 8,1481 8,1482 8,1483 8,1484 8,1485 8,1486 8,1487 8,1488 8,1489 8,1490 8,1491 8,1492 8,1493 8,1494 8,1495 8,378 8,392 8,1532 8,1533 8,1534 8,1535 8,1536 8,1537 8,1538 8,1539 8,1540 8,1541 8,1542 8,1543 8,1544 8,1545 8,1496 8,1497 8,1498 8,1499 8,1500 8,1501 8,1502 8,1503 8,1504 8,1505 8,403 8,1506 8,1507 8,1508 8,1509 8,1510 8,1511 8,1512 8,1513 8,1514 8,1515 8,1516 8,1517 8,1518 8,1519 8,381 8,1520 8,1521 8,1522 8,1523 8,1524 8,1525 8,1526 8,1527 8,1528 8,1529 8,1530 8,1531 8,1546 8,1547 8,1548 8,1549 8,1550 8,1551 8,1552 8,1553 8,1554 8,1555 8,1556 8,1557 8,1558 8,1559 8,1560 8,1561 8,3352 8,3358 8,400 8,436 8,437 8,438 8,439 8,440 8,441 8,442 8,443 8,444 8,445 8,446 8,447 8,448 8,449 8,450 8,451 8,452 8,453 8,454 8,455 8,1562 8,1563 8,1564 8,1565 8,1566 8,1567 8,1568 8,1569 8,1570 8,1571 8,1572 8,1573 8,1574 8,1575 8,1576 8,337 8,338 8,339 8,340 8,341 8,342 8,343 8,344 8,345 8,346 8,347 8,348 8,349 8,350 8,1577 8,1578 8,1579 8,1580 8,1581 8,1582 8,1583 8,1584 8,1585 8,1586 8,1587 8,1588 8,1589 8,1590 8,1591 8,1592 8,1593 8,1594 8,1595 8,1596 8,1597 8,1598 8,1599 8,1600 8,1601 8,1602 8,1603 8,1604 8,1605 8,1606 8,1607 8,1608 8,1609 8,1610 8,1611 8,1612 8,1613 8,1614 8,1615 8,1616 8,1617 8,1618 8,1619 8,1620 8,1621 8,1622 8,1623 8,1624 8,1625 8,1626 8,1627 8,1628 8,1629 8,1630 8,1631 8,1632 8,1633 8,1634 8,1635 8,1636 8,1637 8,1638 8,1639 8,1640 8,1641 8,1642 8,1643 8,1644 8,1645 8,550 8,551 8,552 8,553 8,554 8,555 8,1646 8,1647 8,1648 8,1649 8,1650 8,1651 8,1652 8,1653 8,1654 8,1655 8,1656 8,1657 8,1658 8,1659 8,1660 8,1661 8,1662 8,1663 8,1664 8,1665 8,1666 8,1667 8,1668 8,1669 8,1670 8,1686 8,1687 8,1688 8,1689 8,1690 8,1691 8,1692 8,1693 8,1694 8,1695 8,1696 8,1697 8,1698 8,1699 8,1700 8,1701 8,1671 8,1672 8,1673 8,1674 8,1675 8,1676 8,1677 8,1678 8,1679 8,1680 8,1681 8,1682 8,1683 8,1684 8,1685 8,1702 8,1703 8,1704 8,1705 8,1706 8,1707 8,1708 8,1709 8,1710 8,1711 8,1712 8,1713 8,1714 8,1715 8,1716 8,3257 8,3425 8,3420 8,3326 8,3258 8,3356 8,3424 8,384 8,1717 8,1720 8,1722 8,1723 8,1726 8,1727 8,1730 8,1731 8,1733 8,1736 8,1737 8,1740 8,1742 8,1743 8,1718 8,1719 8,1721 8,1724 8,1725 8,1728 8,1729 8,1732 8,1734 8,1735 8,1738 8,1739 8,1741 8,1744 8,374 8,1745 8,1746 8,1747 8,1748 8,1749 8,1750 8,1751 8,1752 8,1753 8,1754 8,1755 8,1762 8,1763 8,1756 8,1764 8,1757 8,1758 8,1765 8,1766 8,1759 8,1760 8,1767 8,1761 8,1768 8,1769 8,1770 8,1771 8,1772 8,1773 8,1774 8,1775 8,1776 8,1777 8,1778 8,1779 8,1780 8,1781 8,1782 8,1783 8,1784 8,1785 8,1786 8,1787 8,1788 8,1789 8,1790 8,3270 8,1791 8,1792 8,1793 8,1794 8,1795 8,1796 8,1797 8,1798 8,1799 8,1800 8,1893 8,1894 8,1895 8,1896 8,1897 8,1898 8,1899 8,1900 8,1901 8,1801 8,1802 8,1803 8,1804 8,1805 8,1806 8,1807 8,1808 8,1809 8,1810 8,1811 8,1812 8,408 8,409 8,410 8,411 8,412 8,413 8,414 8,415 8,416 8,417 8,418 8,1813 8,1814 8,1815 8,1816 8,1817 8,1818 8,1819 8,1820 8,1821 8,1822 8,1823 8,1824 8,1825 8,1826 8,1827 8,1828 8,1829 8,1830 8,1831 8,1832 8,1833 8,1834 8,1835 8,1836 8,1837 8,1838 8,1839 8,1840 8,1841 8,1842 8,1843 8,1844 8,1845 8,1846 8,1847 8,1848 8,1849 8,1850 8,1851 8,1852 8,1853 8,1854 8,1855 8,1856 8,1857 8,1858 8,1859 8,1860 8,1861 8,1862 8,1863 8,1864 8,1865 8,1866 8,1867 8,1868 8,1869 8,1870 8,1871 8,1872 8,1873 8,1874 8,1875 8,1876 8,1877 8,1878 8,1879 8,1880 8,1881 8,1882 8,1883 8,1884 8,1885 8,1886 8,1887 8,1888 8,1889 8,1890 8,1891 8,1892 8,597 8,598 8,599 8,600 8,601 8,602 8,603 8,604 8,605 8,606 8,607 8,608 8,609 8,610 8,611 8,612 8,613 8,614 8,615 8,616 8,617 8,618 8,619 8,1902 8,1903 8,1904 8,1905 8,1906 8,1907 8,1908 8,1909 8,1910 8,1911 8,1912 8,1913 8,1914 8,1915 8,398 8,1916 8,1917 8,1918 8,1919 8,1920 8,1921 8,1922 8,1923 8,1924 8,1925 8,1926 8,1927 8,1928 8,1929 8,1930 8,1931 8,1932 8,1933 8,1934 8,1935 8,1936 8,1937 8,1938 8,1939 8,1940 8,1941 8,375 8,1957 8,1958 8,1959 8,1960 8,1961 8,1962 8,1963 8,1964 8,1965 8,1966 8,1967 8,1968 8,1969 8,1970 8,1971 8,1972 8,1973 8,1974 8,1975 8,1976 8,1977 8,1978 8,1979 8,1980 8,1981 8,1982 8,1983 8,1984 8,1985 8,1942 8,1943 8,1944 8,1945 8,1946 8,1947 8,1948 8,1949 8,1950 8,1951 8,1952 8,1953 8,1954 8,1955 8,1956 8,3327 8,3330 8,385 8,3321 8,383 8,3359 8,1986 8,1987 8,1988 8,1989 8,1990 8,1991 8,1992 8,1993 8,1994 8,1995 8,1996 8,1997 8,1998 8,1999 8,2000 8,2001 8,2002 8,2003 8,2004 8,2005 8,2006 8,2007 8,2008 8,2009 8,2010 8,2011 8,2012 8,2013 8,2014 8,387 8,3319 8,2015 8,2016 8,2017 8,2018 8,2019 8,2020 8,2021 8,2022 8,2023 8,2024 8,2025 8,2026 8,2027 8,2028 8,2029 8,2030 8,2031 8,2032 8,2033 8,2034 8,2035 8,2036 8,2037 8,2038 8,2039 8,2040 8,2041 8,2042 8,2043 8,3415 8,393 8,529 8,530 8,531 8,532 8,533 8,534 8,535 8,536 8,537 8,538 8,539 8,540 8,541 8,542 8,2044 8,2045 8,2046 8,2047 8,2048 8,2049 8,2050 8,2051 8,2052 8,2053 8,2054 8,2055 8,2056 8,2057 8,2058 8,2059 8,2060 8,2061 8,2062 8,2063 8,2064 8,2065 8,2066 8,2067 8,2068 8,2069 8,2070 8,2071 8,2072 8,2073 8,2074 8,2075 8,2076 8,2077 8,2078 8,2079 8,2080 8,2081 8,2082 8,2083 8,2084 8,2085 8,2086 8,2087 8,2088 8,2089 8,2090 8,2091 8,2092 8,3328 8,2093 8,2094 8,2095 8,2096 8,2097 8,2098 8,3276 8,3277 8,3278 8,3279 8,3280 8,3281 8,3282 8,3283 8,3284 8,3285 8,3286 8,3287 8,2099 8,2100 8,2101 8,2102 8,2103 8,2104 8,2105 8,2106 8,2107 8,2108 8,2109 8,2110 8,2111 8,2112 8,2113 8,2114 8,2115 8,2116 8,2117 8,2118 8,2119 8,2120 8,2121 8,2122 8,2123 8,2124 8,2125 8,2126 8,2127 8,2128 8,2129 8,2130 8,2131 8,2132 8,2133 8,2134 8,2135 8,2136 8,2137 8,2138 8,2139 8,2140 8,2141 8,2142 8,2143 8,2144 8,2145 8,2146 8,2147 8,2148 8,2149 8,2150 8,2151 8,2152 8,2153 8,2154 8,2155 8,2156 8,2157 8,2158 8,2159 8,2160 8,2161 8,2162 8,2163 8,2164 8,2165 8,2166 8,2167 8,2168 8,2169 8,2170 8,2171 8,2172 8,2173 8,2174 8,2175 8,2176 8,2177 8,2178 8,2179 8,2180 8,2181 8,2182 8,2183 8,2184 8,2185 8,2186 8,2187 8,2188 8,2189 8,2190 8,2191 8,2192 8,2193 8,2194 8,2195 8,2196 8,2197 8,2198 8,2199 8,2200 8,2201 8,2202 8,2203 8,2204 8,2205 8,2206 8,2207 8,2208 8,2209 8,2210 8,2211 8,2212 8,2213 8,2214 8,2215 8,386 8,3325 8,2216 8,2217 8,2218 8,2219 8,2220 8,2221 8,2222 8,2223 8,2224 8,2225 8,2226 8,2227 8,2228 8,2229 8,2230 8,2231 8,2232 8,2233 8,2234 8,2235 8,2236 8,2237 8,2238 8,2239 8,2240 8,2241 8,2242 8,2243 8,2244 8,2245 8,2246 8,2247 8,2248 8,2249 8,2250 8,2251 8,2252 8,2253 8,2650 8,2651 8,2652 8,2653 8,2654 8,2655 8,2656 8,2657 8,2658 8,2659 8,2660 8,2661 8,2662 8,2663 8,3353 8,3355 8,3271 8,2254 8,2255 8,2256 8,2257 8,2258 8,2259 8,2260 8,2261 8,2262 8,2263 8,2264 8,2265 8,2266 8,2267 8,2268 8,2269 8,2270 8,419 8,420 8,421 8,422 8,423 8,424 8,425 8,426 8,427 8,428 8,429 8,430 8,431 8,432 8,433 8,434 8,435 8,2271 8,2272 8,2273 8,2274 8,2275 8,2276 8,2277 8,2278 8,2279 8,2280 8,2281 8,2318 8,2319 8,2320 8,2321 8,2322 8,2323 8,2324 8,2325 8,2326 8,2327 8,2328 8,2329 8,2330 8,2331 8,2332 8,2333 8,2285 8,2286 8,2287 8,2288 8,2289 8,2290 8,2291 8,2292 8,2293 8,2294 8,2295 8,3254 8,2296 8,2297 8,2298 8,2299 8,2300 8,2301 8,2302 8,2303 8,2304 8,2305 8,2306 8,2307 8,2308 8,2309 8,2310 8,2311 8,2312 8,2313 8,2314 8,2315 8,2316 8,2317 8,2282 8,2283 8,2284 8,2334 8,2335 8,2336 8,2337 8,2338 8,2339 8,2340 8,2341 8,2342 8,2343 8,2344 8,2345 8,2346 8,2347 8,2348 8,2349 8,2350 8,2351 8,2352 8,2353 8,2354 8,2355 8,2356 8,2357 8,2358 8,2359 8,2360 8,2361 8,2362 8,2363 8,2364 8,2365 8,2366 8,2367 8,2368 8,2369 8,2370 8,2371 8,2372 8,2373 8,2374 8,2375 8,2376 8,2377 8,2378 8,2379 8,2380 8,2381 8,2382 8,2383 8,2384 8,2385 8,2386 8,2387 8,2388 8,2389 8,2390 8,2391 8,2392 8,2393 8,2394 8,2395 8,2396 8,2397 8,2398 8,2399 8,2400 8,2401 8,2402 8,2403 8,2404 8,2405 8,3275 8,3404 8,3323 8,2664 8,2665 8,2666 8,2667 8,2668 8,2669 8,2670 8,2671 8,2672 8,2673 8,2674 8,2675 8,2676 8,2677 8,2678 8,2679 8,2680 8,2681 8,2682 8,2683 8,2684 8,2685 8,2686 8,2687 8,2688 8,2689 8,2690 8,2691 8,2692 8,2693 8,2694 8,2695 8,2696 8,2697 8,2698 8,2699 8,2700 8,2701 8,2702 8,2703 8,2704 8,3414 8,2406 8,2407 8,2408 8,2409 8,2410 8,2411 8,2412 8,2413 8,2414 8,2415 8,2416 8,2417 8,2418 8,2419 8,3334 8,401 8,2420 8,2421 8,2422 8,2423 8,2424 8,2425 8,2426 8,2427 8,2428 8,2429 8,2430 8,2431 8,2432 8,2433 8,570 8,573 8,577 8,580 8,581 8,571 8,579 8,582 8,572 8,575 8,578 8,574 8,576 8,3410 8,3288 8,3289 8,3290 8,3291 8,3292 8,3293 8,3294 8,3295 8,3296 8,3297 8,3298 8,3299 8,3333 8,2434 8,2435 8,2436 8,2437 8,2438 8,2439 8,2440 8,2441 8,2442 8,2443 8,2444 8,2445 8,2446 8,2447 8,2448 8,3418 8,2449 8,2450 8,2451 8,2452 8,2453 8,2454 8,2455 8,2456 8,2457 8,2458 8,2459 8,2460 8,2461 8,2462 8,2463 8,2464 8,2465 8,2466 8,2467 8,2468 8,2469 8,2470 8,2471 8,2472 8,2473 8,2474 8,2475 8,2476 8,2477 8,2478 8,2479 8,2480 8,2481 8,2482 8,2483 8,2484 8,2485 8,2486 8,2487 8,2488 8,2489 8,2490 8,2491 8,2492 8,2493 8,2494 8,2495 8,2496 8,2497 8,2498 8,2499 8,2500 8,2501 8,2502 8,2503 8,2504 8,2505 8,3269 8,2506 8,2507 8,2508 8,2509 8,2510 8,2511 8,2512 8,2513 8,2514 8,2515 8,2516 8,2517 8,2518 8,2519 8,2520 8,2521 8,2522 8,456 8,457 8,458 8,459 8,460 8,461 8,462 8,463 8,464 8,465 8,466 8,467 8,2523 8,2524 8,2525 8,2526 8,2527 8,2528 8,2529 8,2530 8,2531 8,3335 8,2532 8,2533 8,2534 8,2535 8,2536 8,2537 8,2538 8,2539 8,2540 8,2541 8,2542 8,2543 8,2544 8,2545 8,2546 8,2547 8,2548 8,2549 8,2550 8,2551 8,2552 8,2553 8,2554 8,2555 8,2556 8,2557 8,2558 8,2559 8,2560 8,2561 8,2562 8,2563 8,2564 8,2705 8,2706 8,2707 8,2708 8,2709 8,2710 8,2711 8,2712 8,2713 8,2714 8,2715 8,2716 8,2717 8,2718 8,2719 8,2720 8,2721 8,2722 8,2723 8,2724 8,2725 8,2726 8,2727 8,2728 8,2729 8,2730 8,3365 8,3366 8,3367 8,3368 8,3369 8,3370 8,3371 8,3372 8,3373 8,3374 8,2565 8,2566 8,2567 8,2568 8,2569 8,2570 8,2571 8,2751 8,2752 8,2753 8,2754 8,2755 8,2756 8,2757 8,2758 8,2759 8,2760 8,2761 8,2762 8,2763 8,2764 8,2765 8,2766 8,2767 8,2768 8,2769 8,2770 8,2771 8,2772 8,2773 8,2774 8,2775 8,2776 8,2777 8,2778 8,2779 8,2780 8,2781 8,2782 8,2783 8,2784 8,2785 8,2786 8,2787 8,2788 8,2789 8,2790 8,2791 8,2792 8,2793 8,2794 8,2795 8,2796 8,2797 8,2798 8,2799 8,2800 8,2801 8,2802 8,2803 8,2804 8,2805 8,2806 8,2807 8,2808 8,2809 8,2810 8,2811 8,2812 8,2813 8,2814 8,2815 8,2816 8,2817 8,2818 8,646 8,647 8,648 8,649 8,651 8,653 8,655 8,658 8,2926 8,2927 8,2928 8,2929 8,2930 8,2931 8,2932 8,2933 8,2934 8,2935 8,2936 8,2937 8,2938 8,2939 8,2940 8,2941 8,2942 8,2943 8,2944 8,2945 8,2946 8,2947 8,2948 8,2949 8,2950 8,2951 8,2952 8,2953 8,2954 8,2955 8,2956 8,2957 8,2958 8,2959 8,2960 8,2961 8,2962 8,2963 8,3004 8,3005 8,3006 8,3007 8,3008 8,3009 8,3010 8,3011 8,3012 8,3013 8,3014 8,3015 8,3016 8,3017 8,2964 8,2965 8,2966 8,2967 8,2968 8,2969 8,2970 8,2971 8,2972 8,2973 8,2974 8,3253 8,2975 8,2976 8,2977 8,2978 8,2979 8,2980 8,2981 8,2982 8,2983 8,2984 8,2985 8,2986 8,2987 8,2988 8,2989 8,2990 8,2991 8,2992 8,2993 8,2994 8,2995 8,2996 8,2997 8,2998 8,2999 8,3000 8,3001 8,3002 8,3003 8,3018 8,3019 8,3020 8,3021 8,3022 8,3023 8,3024 8,3025 8,3026 8,3027 8,3028 8,3029 8,3030 8,3031 8,3032 8,3033 8,3034 8,3035 8,3036 8,3037 8,3038 8,3039 8,3040 8,3041 8,3042 8,3043 8,3044 8,3045 8,3046 8,3047 8,3048 8,3049 8,3050 8,3051 8,3064 8,3065 8,3066 8,3067 8,3068 8,3069 8,3070 8,3071 8,3072 8,3073 8,3074 8,3075 8,3076 8,3077 8,3078 8,3079 8,3080 8,3052 8,3053 8,3054 8,3055 8,3056 8,3057 8,3058 8,3059 8,3060 8,3061 8,3062 8,3063 8,3081 8,3082 8,3083 8,3084 8,3085 8,3086 8,3087 8,3088 8,3089 8,3090 8,3091 8,3092 8,3093 8,3094 8,3095 8,3096 8,3097 8,3098 8,3099 8,3100 8,3101 8,3102 8,3103 8,323 8,324 8,325 8,326 8,327 8,328 8,329 8,330 8,331 8,332 8,333 8,334 8,335 8,336 8,360 8,361 8,362 8,363 8,364 8,365 8,366 8,367 8,368 8,369 8,370 8,371 8,372 8,373 8,556 8,557 8,558 8,559 8,560 8,561 8,562 8,563 8,564 8,565 8,566 8,567 8,568 8,569 8,661 8,662 8,663 8,664 8,665 8,666 8,667 8,668 8,669 8,670 8,671 8,672 8,673 8,674 8,3104 8,3105 8,3106 8,3107 8,3108 8,3109 8,3110 8,3111 8,3112 8,3113 8,3114 8,3115 8,3116 8,3117 8,3118 8,3119 8,3120 8,3121 8,3122 8,3123 8,3124 8,3125 8,3126 8,3127 8,3128 8,3129 8,3130 8,3131 8,652 8,656 8,657 8,650 8,659 8,654 8,660 8,3132 8,3133 8,3134 8,3135 8,3136 8,3137 8,3138 8,3139 8,3140 8,3141 8,3142 8,3143 8,3144 8,3145 8,2731 8,2732 8,2733 8,2734 8,2735 8,2736 8,2737 8,2738 8,2739 8,2740 8,2741 8,2742 8,2743 8,2744 8,2745 8,2746 8,2747 8,2748 8,2749 8,2750 8,3408 8,3320 8,3409 8,3264 8,3146 8,3147 8,3148 8,3149 8,3150 8,3151 8,3152 8,3153 8,3154 8,3155 8,3156 8,3157 8,3158 8,3159 8,3160 8,3161 8,3162 8,3163 8,3164 8,3438 8,3442 8,3436 8,3450 8,3454 8,3432 8,3443 8,3447 8,3452 8,3441 8,3434 8,3449 8,3445 8,3440 8,3453 8,3439 8,3435 8,3448 8,3437 8,3446 8,3444 8,3433 8,3431 8,3451 8,3430 8,3455 8,3456 8,3457 8,3458 8,3459 8,3460 8,3461 8,3462 8,3463 8,3464 8,3465 8,3466 8,3467 8,3468 8,3469 8,3470 8,3471 8,3472 8,3473 8,3474 8,3475 8,3476 8,3477 8,3478 8,3482 8,3485 8,3491 8,3501 8,3487 8,3500 8,3488 8,3499 8,3497 8,3494 8,3495 8,3490 8,3489 8,3492 8,3483 8,3493 8,3498 8,3496 8,3502 8,3479 8,3481 8,3503 8,3486 8,3480 8,3484 9,3402 10,3250 10,2819 10,2820 10,2821 10,2822 10,2823 10,2824 10,2825 10,2826 10,2827 10,2828 10,2829 10,2830 10,2831 10,2832 10,2833 10,2834 10,2835 10,2836 10,2837 10,2838 10,3226 10,3227 10,3228 10,3229 10,3230 10,3231 10,3232 10,3233 10,3234 10,3235 10,3236 10,3237 10,3238 10,3239 10,3240 10,3241 10,3242 10,3243 10,3244 10,3245 10,3246 10,3247 10,3248 10,3249 10,2839 10,2840 10,2841 10,2842 10,2843 10,2844 10,2845 10,2846 10,2847 10,2848 10,2849 10,2850 10,2851 10,2852 10,2853 10,2854 10,2855 10,2856 10,3166 10,3167 10,3168 10,3171 10,3223 10,2858 10,2861 10,2865 10,2868 10,2871 10,2873 10,2877 10,2880 10,2883 10,2885 10,2888 10,2893 10,2894 10,2898 10,2901 10,2904 10,2906 10,2911 10,2913 10,2915 10,2917 10,2919 10,2921 10,2923 10,2925 10,2859 10,2860 10,2864 10,2867 10,2869 10,2872 10,2878 10,2879 10,2884 10,2887 10,2889 10,2892 10,2896 10,2897 10,2902 10,2905 10,2907 10,2910 10,2914 10,2916 10,2918 10,2920 10,2922 10,2924 10,2857 10,2862 10,2863 10,2866 10,2870 10,2874 10,2875 10,2876 10,2881 10,2882 10,2886 10,2890 10,2891 10,2895 10,2899 10,2900 10,2903 10,2908 10,2909 10,2912 10,3165 10,3169 10,3170 10,3252 10,3224 10,3251 10,3340 10,3339 10,3338 10,3337 10,3341 10,3345 10,3342 10,3346 10,3343 10,3347 10,3344 10,3348 10,3360 10,3361 10,3362 10,3363 10,3364 10,3172 10,3173 10,3174 10,3175 10,3176 10,3177 10,3178 10,3179 10,3180 10,3181 10,3182 10,3183 10,3184 10,3185 10,3186 10,3187 10,3188 10,3189 10,3190 10,3191 10,3192 10,3193 10,3194 10,3195 10,3196 10,3197 10,3198 10,3199 10,3200 10,3201 10,3202 10,3203 10,3204 10,3205 10,3206 10,3207 10,3208 10,3209 10,3210 10,3211 10,3212 10,3213 10,3214 10,3215 10,3216 10,3217 10,3218 10,3219 10,3220 10,3221 10,3222 10,3428 10,3429 11,391 11,516 11,523 11,219 11,220 11,215 11,730 11,738 11,228 11,230 11,236 11,852 11,858 11,864 11,867 11,874 11,877 11,885 11,888 11,1088 11,1093 11,1099 11,1105 11,501 11,504 11,1518 11,1519 11,1514 11,1916 11,1928 11,1921 11,2752 11,2753 11,2754 11,2758 11,2767 11,2768 11,2769 11,393 12,3479 12,3480 12,3481 12,3482 12,3483 12,3484 12,3485 12,3486 12,3487 12,3488 12,3489 12,3490 12,3491 12,3492 12,3493 12,3494 12,3495 12,3496 12,3497 12,3498 12,3499 12,3500 12,3501 12,3502 12,3503 12,3430 12,3431 12,3432 12,3433 12,3434 12,3435 12,3436 12,3437 12,3438 12,3439 12,3440 12,3441 12,3442 12,3443 12,3444 12,3445 12,3446 12,3447 12,3448 12,3449 12,3450 12,3451 12,3452 12,3453 12,3454 12,3403 12,3404 12,3405 12,3406 12,3407 12,3408 12,3409 12,3410 12,3411 12,3412 12,3413 12,3414 12,3415 12,3416 12,3417 12,3418 12,3419 12,3420 12,3421 12,3422 12,3423 12,3424 12,3425 12,3426 12,3427 13,3479 13,3480 13,3481 13,3482 13,3483 13,3484 13,3485 13,3486 13,3487 13,3488 13,3489 13,3490 13,3491 13,3492 13,3493 13,3494 13,3495 13,3496 13,3497 13,3498 13,3499 13,3500 13,3501 13,3502 13,3503 14,3430 14,3431 14,3432 14,3433 14,3434 14,3435 14,3436 14,3437 14,3438 14,3439 14,3440 14,3441 14,3442 14,3443 14,3444 14,3445 14,3446 14,3447 14,3448 14,3449 14,3450 14,3451 14,3452 14,3453 14,3454 15,3403 15,3404 15,3405 15,3406 15,3407 15,3408 15,3409 15,3410 15,3411 15,3412 15,3413 15,3414 15,3415 15,3416 15,3417 15,3418 15,3419 15,3420 15,3421 15,3422 15,3423 15,3424 15,3425 15,3426 15,3427 16,3367 16,52 16,2194 16,2195 16,2198 16,2206 16,2512 16,2516 16,2550 16,2003 16,2004 16,2005 16,2007 16,2010 16,2013 17,1 17,2 17,3 17,4 17,5 17,152 17,160 17,1278 17,1283 17,1392 17,1335 17,1345 17,1380 17,1801 17,1830 17,1837 17,1854 17,1876 17,1880 17,1984 17,1942 17,1945 17,2094 17,2095 17,2096 17,3290 18,597 ================================================ FILE: prqlc/prqlc/tests/integration/data/chinook/playlists.csv ================================================ playlist_id,name 1,Music 2,Movies 3,TV Shows 4,Audiobooks 5,90’s Music 6,Audiobooks 7,Movies 8,Music 9,Music Videos 10,TV Shows 11,Brazilian Music 12,Classical 13,Classical 101 - Deep Cuts 14,Classical 101 - Next Steps 15,Classical 101 - The Basics 16,Grunge 17,Heavy Metal Classic 18,On-The-Go 1 ================================================ FILE: prqlc/prqlc/tests/integration/data/chinook/schema.sql ================================================ DROP TABLE IF EXISTS invoices; DROP TABLE IF EXISTS customers; DROP TABLE IF EXISTS employees; DROP TABLE IF EXISTS tracks; DROP TABLE IF EXISTS albums; DROP TABLE IF EXISTS genres; DROP TABLE IF EXISTS playlist_track; DROP TABLE IF EXISTS playlists; DROP TABLE IF EXISTS media_types; DROP TABLE IF EXISTS artists; DROP TABLE IF EXISTS invoice_items; CREATE TABLE invoices ( invoice_id INTEGER, customer_id INTEGER, invoice_date TIMESTAMP, billing_address VARCHAR(255), billing_city VARCHAR(255), billing_state VARCHAR(255), billing_country VARCHAR(255), billing_postal_code VARCHAR(255), total REAL ); CREATE TABLE customers ( customer_id INTEGER, first_name VARCHAR(255), last_name VARCHAR(255), company VARCHAR(255), address VARCHAR(255), city VARCHAR(255), state VARCHAR(255), country VARCHAR(255), postal_code VARCHAR(255), phone VARCHAR(255), fax VARCHAR(255), email VARCHAR(255), support_rep_id INTEGER ); CREATE TABLE employees ( employee_id INTEGER, last_name VARCHAR(255), first_name VARCHAR(255), title VARCHAR(255), reports_to INTEGER, birth_date TIMESTAMP, hire_date TIMESTAMP, address VARCHAR(255), city VARCHAR(255), state VARCHAR(255), country VARCHAR(255), postal_code VARCHAR(255), phone VARCHAR(255), fax VARCHAR(255), email VARCHAR(255) ); CREATE TABLE tracks ( track_id INTEGER, name VARCHAR(255), album_id INTEGER, media_type_id INTEGER, genre_id INTEGER, composer VARCHAR(255), milliseconds INTEGER, bytes INTEGER, unit_price REAL ); CREATE TABLE albums (album_id INTEGER, title VARCHAR(255), artist_id INTEGER); CREATE TABLE genres (genre_id INTEGER, name VARCHAR(255)); CREATE TABLE playlist_track (playlist_id INTEGER, track_id INTEGER); CREATE TABLE playlists (playlist_id INTEGER, name VARCHAR(255)); CREATE TABLE media_types (media_type_id INTEGER, name VARCHAR(255)); CREATE TABLE artists (artist_id INTEGER, name VARCHAR(255)); CREATE TABLE invoice_items ( invoice_line_id INTEGER, invoice_id INTEGER, track_id INTEGER, unit_price REAL, quantity INTEGER ); ================================================ FILE: prqlc/prqlc/tests/integration/data/chinook/tracks.csv ================================================ track_id,name,album_id,media_type_id,genre_id,composer,milliseconds,bytes,unit_price 1,For Those About To Rock (We Salute You),1,1,1,"Angus Young, Malcolm Young, Brian Johnson",343719,11170334,0.99 2,Balls to the Wall,2,2,1,,342562,5510424,0.99 3,Fast As a Shark,3,2,1,"F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman",230619,3990994,0.99 4,Restless and Wild,3,2,1,"F. Baltes, R.A. Smith-Diesel, S. Kaufman, U. Dirkscneider & W. Hoffman",252051,4331779,0.99 5,Princess of the Dawn,3,2,1,Deaffy & R.A. Smith-Diesel,375418,6290521,0.99 6,Put The Finger On You,1,1,1,"Angus Young, Malcolm Young, Brian Johnson",205662,6713451,0.99 7,Let's Get It Up,1,1,1,"Angus Young, Malcolm Young, Brian Johnson",233926,7636561,0.99 8,Inject The Venom,1,1,1,"Angus Young, Malcolm Young, Brian Johnson",210834,6852860,0.99 9,Snowballed,1,1,1,"Angus Young, Malcolm Young, Brian Johnson",203102,6599424,0.99 10,Evil Walks,1,1,1,"Angus Young, Malcolm Young, Brian Johnson",263497,8611245,0.99 11,C.O.D.,1,1,1,"Angus Young, Malcolm Young, Brian Johnson",199836,6566314,0.99 12,Breaking The Rules,1,1,1,"Angus Young, Malcolm Young, Brian Johnson",263288,8596840,0.99 13,Night Of The Long Knives,1,1,1,"Angus Young, Malcolm Young, Brian Johnson",205688,6706347,0.99 14,Spellbound,1,1,1,"Angus Young, Malcolm Young, Brian Johnson",270863,8817038,0.99 15,Go Down,4,1,1,AC/DC,331180,10847611,0.99 16,Dog Eat Dog,4,1,1,AC/DC,215196,7032162,0.99 17,Let There Be Rock,4,1,1,AC/DC,366654,12021261,0.99 18,Bad Boy Boogie,4,1,1,AC/DC,267728,8776140,0.99 19,Problem Child,4,1,1,AC/DC,325041,10617116,0.99 20,Overdose,4,1,1,AC/DC,369319,12066294,0.99 21,Hell Ain't A Bad Place To Be,4,1,1,AC/DC,254380,8331286,0.99 22,Whole Lotta Rosie,4,1,1,AC/DC,323761,10547154,0.99 23,Walk On Water,5,1,1,"Steven Tyler, Joe Perry, Jack Blades, Tommy Shaw",295680,9719579,0.99 24,Love In An Elevator,5,1,1,"Steven Tyler, Joe Perry",321828,10552051,0.99 25,Rag Doll,5,1,1,"Steven Tyler, Joe Perry, Jim Vallance, Holly Knight",264698,8675345,0.99 26,What It Takes,5,1,1,"Steven Tyler, Joe Perry, Desmond Child",310622,10144730,0.99 27,Dude (Looks Like A Lady),5,1,1,"Steven Tyler, Joe Perry, Desmond Child",264855,8679940,0.99 28,Janie's Got A Gun,5,1,1,"Steven Tyler, Tom Hamilton",330736,10869391,0.99 29,Cryin',5,1,1,"Steven Tyler, Joe Perry, Taylor Rhodes",309263,10056995,0.99 30,Amazing,5,1,1,"Steven Tyler, Richie Supa",356519,11616195,0.99 31,Blind Man,5,1,1,"Steven Tyler, Joe Perry, Taylor Rhodes",240718,7877453,0.99 32,Deuces Are Wild,5,1,1,"Steven Tyler, Jim Vallance",215875,7074167,0.99 33,The Other Side,5,1,1,"Steven Tyler, Jim Vallance",244375,7983270,0.99 34,Crazy,5,1,1,"Steven Tyler, Joe Perry, Desmond Child",316656,10402398,0.99 35,Eat The Rich,5,1,1,"Steven Tyler, Joe Perry, Jim Vallance",251036,8262039,0.99 36,Angel,5,1,1,"Steven Tyler, Desmond Child",307617,9989331,0.99 37,Livin' On The Edge,5,1,1,"Steven Tyler, Joe Perry, Mark Hudson",381231,12374569,0.99 38,All I Really Want,6,1,1,Alanis Morissette & Glenn Ballard,284891,9375567,0.99 39,You Oughta Know,6,1,1,Alanis Morissette & Glenn Ballard,249234,8196916,0.99 40,Perfect,6,1,1,Alanis Morissette & Glenn Ballard,188133,6145404,0.99 41,Hand In My Pocket,6,1,1,Alanis Morissette & Glenn Ballard,221570,7224246,0.99 42,Right Through You,6,1,1,Alanis Morissette & Glenn Ballard,176117,5793082,0.99 43,Forgiven,6,1,1,Alanis Morissette & Glenn Ballard,300355,9753256,0.99 44,You Learn,6,1,1,Alanis Morissette & Glenn Ballard,239699,7824837,0.99 45,Head Over Feet,6,1,1,Alanis Morissette & Glenn Ballard,267493,8758008,0.99 46,Mary Jane,6,1,1,Alanis Morissette & Glenn Ballard,280607,9163588,0.99 47,Ironic,6,1,1,Alanis Morissette & Glenn Ballard,229825,7598866,0.99 48,Not The Doctor,6,1,1,Alanis Morissette & Glenn Ballard,227631,7604601,0.99 49,Wake Up,6,1,1,Alanis Morissette & Glenn Ballard,293485,9703359,0.99 50,You Oughta Know (Alternate),6,1,1,Alanis Morissette & Glenn Ballard,491885,16008629,0.99 51,We Die Young,7,1,1,Jerry Cantrell,152084,4925362,0.99 52,Man In The Box,7,1,1,"Jerry Cantrell, Layne Staley",286641,9310272,0.99 53,Sea Of Sorrow,7,1,1,Jerry Cantrell,349831,11316328,0.99 54,Bleed The Freak,7,1,1,Jerry Cantrell,241946,7847716,0.99 55,I Can't Remember,7,1,1,"Jerry Cantrell, Layne Staley",222955,7302550,0.99 56,"Love, Hate, Love",7,1,1,"Jerry Cantrell, Layne Staley",387134,12575396,0.99 57,It Ain't Like That,7,1,1,"Jerry Cantrell, Michael Starr, Sean Kinney",277577,8993793,0.99 58,Sunshine,7,1,1,Jerry Cantrell,284969,9216057,0.99 59,Put You Down,7,1,1,Jerry Cantrell,196231,6420530,0.99 60,Confusion,7,1,1,"Jerry Cantrell, Michael Starr, Layne Staley",344163,11183647,0.99 61,I Know Somethin (Bout You),7,1,1,Jerry Cantrell,261955,8497788,0.99 62,Real Thing,7,1,1,"Jerry Cantrell, Layne Staley",243879,7937731,0.99 63,Desafinado,8,1,2,,185338,5990473,0.99 64,Garota De Ipanema,8,1,2,,285048,9348428,0.99 65,Samba De Uma Nota Só (One Note Samba),8,1,2,,137273,4535401,0.99 66,Por Causa De Você,8,1,2,,169900,5536496,0.99 67,Ligia,8,1,2,,251977,8226934,0.99 68,Fotografia,8,1,2,,129227,4198774,0.99 69,Dindi (Dindi),8,1,2,,253178,8149148,0.99 70,Se Todos Fossem Iguais A Você (Instrumental),8,1,2,,134948,4393377,0.99 71,Falando De Amor,8,1,2,,219663,7121735,0.99 72,Angela,8,1,2,,169508,5574957,0.99 73,Corcovado (Quiet Nights Of Quiet Stars),8,1,2,,205662,6687994,0.99 74,Outra Vez,8,1,2,,126511,4110053,0.99 75,O Boto (Bôto),8,1,2,,366837,12089673,0.99 76,"Canta, Canta Mais",8,1,2,,271856,8719426,0.99 77,Enter Sandman,9,1,3,Apocalyptica,221701,7286305,0.99 78,Master Of Puppets,9,1,3,Apocalyptica,436453,14375310,0.99 79,Harvester Of Sorrow,9,1,3,Apocalyptica,374543,12372536,0.99 80,The Unforgiven,9,1,3,Apocalyptica,322925,10422447,0.99 81,Sad But True,9,1,3,Apocalyptica,288208,9405526,0.99 82,Creeping Death,9,1,3,Apocalyptica,308035,10110980,0.99 83,Wherever I May Roam,9,1,3,Apocalyptica,369345,12033110,0.99 84,Welcome Home (Sanitarium),9,1,3,Apocalyptica,350197,11406431,0.99 85,Cochise,10,1,1,Audioslave/Chris Cornell,222380,5339931,0.99 86,Show Me How to Live,10,1,1,Audioslave/Chris Cornell,277890,6672176,0.99 87,Gasoline,10,1,1,Audioslave/Chris Cornell,279457,6709793,0.99 88,What You Are,10,1,1,Audioslave/Chris Cornell,249391,5988186,0.99 89,Like a Stone,10,1,1,Audioslave/Chris Cornell,294034,7059624,0.99 90,Set It Off,10,1,1,Audioslave/Chris Cornell,263262,6321091,0.99 91,Shadow on the Sun,10,1,1,Audioslave/Chris Cornell,343457,8245793,0.99 92,I am the Highway,10,1,1,Audioslave/Chris Cornell,334942,8041411,0.99 93,Exploder,10,1,1,Audioslave/Chris Cornell,206053,4948095,0.99 94,Hypnotize,10,1,1,Audioslave/Chris Cornell,206628,4961887,0.99 95,Bring'em Back Alive,10,1,1,Audioslave/Chris Cornell,329534,7911634,0.99 96,Light My Way,10,1,1,Audioslave/Chris Cornell,303595,7289084,0.99 97,Getaway Car,10,1,1,Audioslave/Chris Cornell,299598,7193162,0.99 98,The Last Remaining Light,10,1,1,Audioslave/Chris Cornell,317492,7622615,0.99 99,Your Time Has Come,11,1,4,"Cornell, Commerford, Morello, Wilk",255529,8273592,0.99 100,Out Of Exile,11,1,4,"Cornell, Commerford, Morello, Wilk",291291,9506571,0.99 101,Be Yourself,11,1,4,"Cornell, Commerford, Morello, Wilk",279484,9106160,0.99 102,Doesn't Remind Me,11,1,4,"Cornell, Commerford, Morello, Wilk",255869,8357387,0.99 103,Drown Me Slowly,11,1,4,"Cornell, Commerford, Morello, Wilk",233691,7609178,0.99 104,Heaven's Dead,11,1,4,"Cornell, Commerford, Morello, Wilk",276688,9006158,0.99 105,The Worm,11,1,4,"Cornell, Commerford, Morello, Wilk",237714,7710800,0.99 106,Man Or Animal,11,1,4,"Cornell, Commerford, Morello, Wilk",233195,7542942,0.99 107,Yesterday To Tomorrow,11,1,4,"Cornell, Commerford, Morello, Wilk",273763,8944205,0.99 108,Dandelion,11,1,4,"Cornell, Commerford, Morello, Wilk",278125,9003592,0.99 109,#1 Zero,11,1,4,"Cornell, Commerford, Morello, Wilk",299102,9731988,0.99 110,The Curse,11,1,4,"Cornell, Commerford, Morello, Wilk",309786,10029406,0.99 111,Money,12,1,5,"Berry Gordy, Jr./Janie Bradford",147591,2365897,0.99 112,Long Tall Sally,12,1,5,"Enotris Johnson/Little Richard/Robert ""Bumps"" Blackwell",106396,1707084,0.99 113,Bad Boy,12,1,5,Larry Williams,116088,1862126,0.99 114,Twist And Shout,12,1,5,Bert Russell/Phil Medley,161123,2582553,0.99 115,Please Mr. Postman,12,1,5,Brian Holland/Freddie Gorman/Georgia Dobbins/Robert Bateman/William Garrett,137639,2206986,0.99 116,C'Mon Everybody,12,1,5,Eddie Cochran/Jerry Capehart,140199,2247846,0.99 117,Rock 'N' Roll Music,12,1,5,Chuck Berry,141923,2276788,0.99 118,Slow Down,12,1,5,Larry Williams,163265,2616981,0.99 119,Roadrunner,12,1,5,Bo Diddley,143595,2301989,0.99 120,Carol,12,1,5,Chuck Berry,143830,2306019,0.99 121,Good Golly Miss Molly,12,1,5,Little Richard,106266,1704918,0.99 122,20 Flight Rock,12,1,5,Ned Fairchild,107807,1299960,0.99 123,Quadrant,13,1,2,Billy Cobham,261851,8538199,0.99 124,Snoopy's search-Red baron,13,1,2,Billy Cobham,456071,15075616,0.99 125,"Spanish moss-""A sound portrait""-Spanish moss",13,1,2,Billy Cobham,248084,8217867,0.99 126,Moon germs,13,1,2,Billy Cobham,294060,9714812,0.99 127,Stratus,13,1,2,Billy Cobham,582086,19115680,0.99 128,The pleasant pheasant,13,1,2,Billy Cobham,318066,10630578,0.99 129,Solo-Panhandler,13,1,2,Billy Cobham,246151,8230661,0.99 130,Do what cha wanna,13,1,2,George Duke,274155,9018565,0.99 131,Intro/ Low Down,14,1,3,,323683,10642901,0.99 132,13 Years Of Grief,14,1,3,,246987,8137421,0.99 133,Stronger Than Death,14,1,3,,300747,9869647,0.99 134,All For You,14,1,3,,235833,7726948,0.99 135,Super Terrorizer,14,1,3,,319373,10513905,0.99 136,Phoney Smile Fake Hellos,14,1,3,,273606,9011701,0.99 137,Lost My Better Half,14,1,3,,284081,9355309,0.99 138,Bored To Tears,14,1,3,,247327,8130090,0.99 139,A.N.D.R.O.T.A.Z.,14,1,3,,266266,8574746,0.99 140,Born To Booze,14,1,3,,282122,9257358,0.99 141,World Of Trouble,14,1,3,,359157,11820932,0.99 142,No More Tears,14,1,3,,555075,18041629,0.99 143,The Begining... At Last,14,1,3,,365662,11965109,0.99 144,Heart Of Gold,15,1,3,,194873,6417460,0.99 145,Snowblind,15,1,3,,420022,13842549,0.99 146,Like A Bird,15,1,3,,276532,9115657,0.99 147,Blood In The Wall,15,1,3,,284368,9359475,0.99 148,The Beginning...At Last,15,1,3,,271960,8975814,0.99 149,Black Sabbath,16,1,3,,382066,12440200,0.99 150,The Wizard,16,1,3,,264829,8646737,0.99 151,Behind The Wall Of Sleep,16,1,3,,217573,7169049,0.99 152,N.I.B.,16,1,3,,368770,12029390,0.99 153,Evil Woman,16,1,3,,204930,6655170,0.99 154,Sleeping Village,16,1,3,,644571,21128525,0.99 155,Warning,16,1,3,,212062,6893363,0.99 156,Wheels Of Confusion / The Straightener,17,1,3,"Tony Iommi, Bill Ward, Geezer Butler, Ozzy Osbourne",494524,16065830,0.99 157,Tomorrow's Dream,17,1,3,"Tony Iommi, Bill Ward, Geezer Butler, Ozzy Osbourne",192496,6252071,0.99 158,Changes,17,1,3,"Tony Iommi, Bill Ward, Geezer Butler, Ozzy Osbourne",286275,9175517,0.99 159,FX,17,1,3,"Tony Iommi, Bill Ward, Geezer Butler, Ozzy Osbourne",103157,3331776,0.99 160,Supernaut,17,1,3,"Tony Iommi, Bill Ward, Geezer Butler, Ozzy Osbourne",285779,9245971,0.99 161,Snowblind,17,1,3,"Tony Iommi, Bill Ward, Geezer Butler, Ozzy Osbourne",331676,10813386,0.99 162,Cornucopia,17,1,3,"Tony Iommi, Bill Ward, Geezer Butler, Ozzy Osbourne",234814,7653880,0.99 163,Laguna Sunrise,17,1,3,"Tony Iommi, Bill Ward, Geezer Butler, Ozzy Osbourne",173087,5671374,0.99 164,St. Vitus Dance,17,1,3,"Tony Iommi, Bill Ward, Geezer Butler, Ozzy Osbourne",149655,4884969,0.99 165,Under The Sun/Every Day Comes and Goes,17,1,3,"Tony Iommi, Bill Ward, Geezer Butler, Ozzy Osbourne",350458,11360486,0.99 166,Smoked Pork,18,1,4,,47333,1549074,0.99 167,Body Count's In The House,18,1,4,,204251,6715413,0.99 168,Now Sports,18,1,4,,4884,161266,0.99 169,Body Count,18,1,4,,317936,10489139,0.99 170,A Statistic,18,1,4,,6373,211997,0.99 171,Bowels Of The Devil,18,1,4,,223216,7324125,0.99 172,The Real Problem,18,1,4,,11650,387360,0.99 173,KKK Bitch,18,1,4,,173008,5709631,0.99 174,D Note,18,1,4,,95738,3067064,0.99 175,Voodoo,18,1,4,,300721,9875962,0.99 176,The Winner Loses,18,1,4,,392254,12843821,0.99 177,There Goes The Neighborhood,18,1,4,,350171,11443471,0.99 178,Oprah,18,1,4,,6635,224313,0.99 179,Evil Dick,18,1,4,,239020,7828873,0.99 180,Body Count Anthem,18,1,4,,166426,5463690,0.99 181,Momma's Gotta Die Tonight,18,1,4,,371539,12122946,0.99 182,Freedom Of Speech,18,1,4,,281234,9337917,0.99 183,King In Crimson,19,1,3,Roy Z,283167,9218499,0.99 184,Chemical Wedding,19,1,3,Roy Z,246177,8022764,0.99 185,The Tower,19,1,3,Roy Z,285257,9435693,0.99 186,Killing Floor,19,1,3,Adrian Smith,269557,8854240,0.99 187,Book Of Thel,19,1,3,Eddie Casillas/Roy Z,494393,16034404,0.99 188,Gates Of Urizen,19,1,3,Roy Z,265351,8627004,0.99 189,Jerusalem,19,1,3,Roy Z,402390,13194463,0.99 190,Trupets Of Jericho,19,1,3,Roy Z,359131,11820908,0.99 191,Machine Men,19,1,3,Adrian Smith,341655,11138147,0.99 192,The Alchemist,19,1,3,Roy Z,509413,16545657,0.99 193,Realword,19,1,3,Roy Z,237531,7802095,0.99 194,First Time I Met The Blues,20,1,6,Eurreal Montgomery,140434,4604995,0.99 195,Let Me Love You Baby,20,1,6,Willie Dixon,175386,5716994,0.99 196,Stone Crazy,20,1,6,Buddy Guy,433397,14184984,0.99 197,Pretty Baby,20,1,6,Willie Dixon,237662,7848282,0.99 198,When My Left Eye Jumps,20,1,6,Al Perkins/Willie Dixon,235311,7685363,0.99 199,Leave My Girl Alone,20,1,6,Buddy Guy,204721,6859518,0.99 200,She Suits Me To A Tee,20,1,6,Buddy Guy,136803,4456321,0.99 201,Keep It To Myself (Aka Keep It To Yourself),20,1,6,Sonny Boy Williamson [I],166060,5487056,0.99 202,My Time After Awhile,20,1,6,Robert Geddins/Ron Badger/Sheldon Feinberg,182491,6022698,0.99 203,Too Many Ways (Alternate),20,1,6,Willie Dixon,135053,4459946,0.99 204,Talkin' 'Bout Women Obviously,20,1,6,Amos Blakemore/Buddy Guy,589531,19161377,0.99 205,Jorge Da Capadócia,21,1,7,Jorge Ben,177397,5842196,0.99 206,Prenda Minha,21,1,7,Tradicional,99369,3225364,0.99 207,Meditação,21,1,7,Tom Jobim - Newton Mendoça,148793,4865597,0.99 208,Terra,21,1,7,Caetano Veloso,482429,15889054,0.99 209,Eclipse Oculto,21,1,7,Caetano Veloso,221936,7382703,0.99 210,"Texto ""Verdade Tropical""",21,1,7,Caetano Veloso,84088,2752161,0.99 211,Bem Devagar,21,1,7,Gilberto Gil,133172,4333651,0.99 212,Drão,21,1,7,Gilberto Gil,156264,5065932,0.99 213,Saudosismo,21,1,7,Caetano Veloso,144326,4726981,0.99 214,Carolina,21,1,7,Chico Buarque,181812,5924159,0.99 215,Sozinho,21,1,7,Peninha,190589,6253200,0.99 216,Esse Cara,21,1,7,Caetano Veloso,223111,7217126,0.99 217,Mel,21,1,7,Caetano Veloso - Waly Salomão,294765,9854062,0.99 218,Linha Do Equador,21,1,7,Caetano Veloso - Djavan,299337,10003747,0.99 219,Odara,21,1,7,Caetano Veloso,141270,4704104,0.99 220,A Luz De Tieta,21,1,7,Caetano Veloso,251742,8507446,0.99 221,Atrás Da Verd-E-Rosa Só Não Vai Quem Já Morreu,21,1,7,David Corrêa - Paulinho Carvalho - Carlos Sena - Bira do Ponto,307252,10364247,0.99 222,Vida Boa,21,1,7,Fausto Nilo - Armandinho,281730,9411272,0.99 223,Sozinho (Hitmakers Classic Mix),22,1,7,,436636,14462072,0.99 224,Sozinho (Hitmakers Classic Radio Edit),22,1,7,,195004,6455134,0.99 225,Sozinho (Caêdrum 'n' Bass),22,1,7,,328071,10975007,0.99 226,Carolina,23,1,7,,163056,5375395,0.99 227,Essa Moça Ta Diferente,23,1,7,,167235,5568574,0.99 228,Vai Passar,23,1,7,,369763,12359161,0.99 229,Samba De Orly,23,1,7,,162429,5431854,0.99 230,"Bye, Bye Brasil",23,1,7,,283402,9499590,0.99 231,Atras Da Porta,23,1,7,,189675,6132843,0.99 232,Tatuagem,23,1,7,,172120,5645703,0.99 233,O Que Será (À Flor Da Terra),23,1,7,,167288,5574848,0.99 234,Morena De Angola,23,1,7,,186801,6373932,0.99 235,Apesar De Você,23,1,7,,234501,7886937,0.99 236,A Banda,23,1,7,,132493,4349539,0.99 237,Minha Historia,23,1,7,,182256,6029673,0.99 238,Com Açúcar E Com Afeto,23,1,7,,175386,5846442,0.99 239,Brejo Da Cruz,23,1,7,,214099,7270749,0.99 240,Meu Caro Amigo,23,1,7,,260257,8778172,0.99 241,Geni E O Zepelim,23,1,7,,317570,10342226,0.99 242,Trocando Em Miúdos,23,1,7,,169717,5461468,0.99 243,Vai Trabalhar Vagabundo,23,1,7,,139154,4693941,0.99 244,Gota D'água,23,1,7,,153208,5074189,0.99 245,Construção / Deus Lhe Pague,23,1,7,,383059,12675305,0.99 246,Mateus Enter,24,1,7,Chico Science,33149,1103013,0.99 247,O Cidadão Do Mundo,24,1,7,Chico Science,200933,6724966,0.99 248,Etnia,24,1,7,Chico Science,152555,5061413,0.99 249,Quilombo Groove [Instrumental],24,1,7,Chico Science,151823,5042447,0.99 250,Macô,24,1,7,Chico Science,249600,8253934,0.99 251,Um Passeio No Mundo Livre,24,1,7,Chico Science,240091,7984291,0.99 252,Samba Do Lado,24,1,7,Chico Science,227317,7541688,0.99 253,Maracatu Atômico,24,1,7,Chico Science,284264,9670057,0.99 254,O Encontro De Isaac Asimov Com Santos Dumont No Céu,24,1,7,Chico Science,99108,3240816,0.99 255,Corpo De Lama,24,1,7,Chico Science,232672,7714954,0.99 256,Sobremesa,24,1,7,Chico Science,240091,7960868,0.99 257,Manguetown,24,1,7,Chico Science,194560,6475159,0.99 258,Um Satélite Na Cabeça,24,1,7,Chico Science,126615,4272821,0.99 259,Baião Ambiental [Instrumental],24,1,7,Chico Science,152659,5198539,0.99 260,Sangue De Bairro,24,1,7,Chico Science,132231,4415557,0.99 261,Enquanto O Mundo Explode,24,1,7,Chico Science,88764,2968650,0.99 262,Interlude Zumbi,24,1,7,Chico Science,71627,2408550,0.99 263,Criança De Domingo,24,1,7,Chico Science,208222,6984813,0.99 264,Amor De Muito,24,1,7,Chico Science,175333,5881293,0.99 265,Samidarish [Instrumental],24,1,7,Chico Science,272431,8911641,0.99 266,Maracatu Atômico [Atomic Version],24,1,7,Chico Science,273084,9019677,0.99 267,Maracatu Atômico [Ragga Mix],24,1,7,Chico Science,210155,6986421,0.99 268,Maracatu Atômico [Trip Hop],24,1,7,Chico Science,221492,7380787,0.99 269,Banditismo Por Uma Questa,25,1,7,,307095,10251097,0.99 270,Banditismo Por Uma Questa,25,1,7,,243644,8147224,0.99 271,Rios Pontes & Overdrives,25,1,7,,286720,9659152,0.99 272,Cidade,25,1,7,,216346,7241817,0.99 273,Praiera,25,1,7,,183640,6172781,0.99 274,Samba Makossa,25,1,7,,271856,9095410,0.99 275,Da Lama Ao Caos,25,1,7,,251559,8378065,0.99 276,Maracatu De Tiro Certeiro,25,1,7,,88868,2901397,0.99 277,Salustiano Song,25,1,7,,215405,7183969,0.99 278,Antene Se,25,1,7,,248372,8253618,0.99 279,Risoflora,25,1,7,,105586,3536938,0.99 280,Lixo Do Mangue,25,1,7,,193253,6534200,0.99 281,Computadores Fazem Arte,25,1,7,,404323,13702771,0.99 282,Girassol,26,1,8,Bino Farias/Da Gama/Lazão/Pedro Luis/Toni Garrido,249808,8327676,0.99 283,A Sombra Da Maldade,26,1,8,Da Gama/Toni Garrido,230922,7697230,0.99 284,Johnny B. Goode,26,1,8,Chuck Berry,254615,8505985,0.99 285,Soldado Da Paz,26,1,8,Herbert Vianna,194220,6455080,0.99 286,Firmamento,26,1,8,Bino Farias/Da Gama/Henry Lawes/Lazão/Toni Garrido/Winston Foser-Vers,222145,7402658,0.99 287,Extra,26,1,8,Gilberto Gil,304352,10078050,0.99 288,O Erê,26,1,8,Bernardo Vilhena/Bino Farias/Da Gama/Lazão/Toni Garrido,236382,7866924,0.99 289,Podes Crer,26,1,8,Bino Farias/Da Gama/Lazão/Toni Garrido,232280,7747747,0.99 290,A Estrada,26,1,8,Bino Farias/Da Gama/Lazão/Toni Garrido,248842,8275673,0.99 291,Berlim,26,1,8,Da Gama/Toni Garrido,207542,6920424,0.99 292,Já Foi,26,1,8,Bino Farias/Da Gama/Lazão/Toni Garrido,221544,7388466,0.99 293,Onde Você Mora?,26,1,8,Marisa Monte/Nando Reis,256026,8502588,0.99 294,Pensamento,26,1,8,Bino Farias/Da Gamma/Lazão/Rás Bernard,173008,5748424,0.99 295,Conciliação,26,1,8,Da Gama/Lazão/Rás Bernardo,257619,8552474,0.99 296,Realidade Virtual,26,1,8,Bino Farias/Da Gama/Lazão/Toni Garrido,195239,6503533,0.99 297,Mensagem,26,1,8,Bino Farias/Da Gama/Lazão/Rás Bernardo,225332,7488852,0.99 298,A Cor Do Sol,26,1,8,Bernardo Vilhena/Da Gama/Lazão,231392,7663348,0.99 299,Onde Você Mora?,27,1,8,Marisa Monte/Nando Reis,298396,10056970,0.99 300,O Erê,27,1,8,Bernardo Vilhena/Bino/Da Gama/Lazao/Toni Garrido,206942,6950332,0.99 301,A Sombra Da Maldade,27,1,8,Da Gama/Toni Garrido,285231,9544383,0.99 302,A Estrada,27,1,8,Da Gama/Lazao/Toni Garrido,282174,9344477,0.99 303,Falar A Verdade,27,1,8,Bino/Da Gama/Ras Bernardo,244950,8189093,0.99 304,Firmamento,27,1,8,Harry Lawes/Winston Foster-Vers,225488,7507866,0.99 305,Pensamento,27,1,8,Bino/Da Gama/Ras Bernardo,192391,6399761,0.99 306,Realidade Virtual,27,1,8,Bino/Da Gamma/Lazao/Toni Garrido,240300,8069934,0.99 307,Doutor,27,1,8,Bino/Da Gama/Toni Garrido,178155,5950952,0.99 308,Na Frente Da TV,27,1,8,Bino/Da Gama/Lazao/Ras Bernardo,289750,9633659,0.99 309,Downtown,27,1,8,Cidade Negra,239725,8024386,0.99 310,Sábado A Noite,27,1,8,Lulu Santos,267363,8895073,0.99 311,A Cor Do Sol,27,1,8,Bernardo Vilhena/Da Gama/Lazao,273031,9142937,0.99 312,Eu Também Quero Beijar,27,1,8,Fausto Nilo/Moraes Moreira/Pepeu Gomes,211147,7029400,0.99 313,Noite Do Prazer,28,1,7,,311353,10309980,0.99 314,À Francesa,28,1,7,,244532,8150846,0.99 315,Cada Um Cada Um (A Namoradeira),28,1,7,,253492,8441034,0.99 316,Linha Do Equador,28,1,7,,244715,8123466,0.99 317,Amor Demais,28,1,7,,254040,8420093,0.99 318,Férias,28,1,7,,264202,8731945,0.99 319,Gostava Tanto De Você,28,1,7,,230452,7685326,0.99 320,Flor Do Futuro,28,1,7,,275748,9205941,0.99 321,Felicidade Urgente,28,1,7,,266605,8873358,0.99 322,Livre Pra Viver,28,1,7,,214595,7111596,0.99 323,"Dig-Dig, Lambe-Lambe (Ao Vivo)",29,1,9,Cassiano Costa/Cintia Maviane/J.F./Lucas Costa,205479,6892516,0.99 324,Pererê,29,1,9,Augusto Conceição/Chiclete Com Banana,198661,6643207,0.99 325,TriboTchan,29,1,9,Cal Adan/Paulo Levi,194194,6507950,0.99 326,"Tapa Aqui, Descobre Ali",29,1,9,Paulo Levi/W. Rangel,188630,6327391,0.99 327,Daniela,29,1,9,Jorge Cardoso/Pierre Onasis,230791,7748006,0.99 328,Bate Lata,29,1,9,Fábio Nolasco/Gal Sales/Ivan Brasil,206733,7034985,0.99 329,Garotas do Brasil,29,1,9,"Garay, Ricardo Engels/Luca Predabom/Ludwig, Carlos Henrique/Maurício Vieira",210155,6973625,0.99 330,Levada do Amor (Ailoviu),29,1,9,Luiz Wanderley/Paulo Levi,190093,6457752,0.99 331,Lavadeira,29,1,9,"Do Vale, Valverde/Gal Oliveira/Luciano Pinto",214256,7254147,0.99 332,Reboladeira,29,1,9,Cal Adan/Ferrugem/Julinho Carioca/Tríona Ní Dhomhnaill,210599,7027525,0.99 333,É que Nessa Encarnação Eu Nasci Manga,29,1,9,Lucina/Luli,196519,6568081,0.99 334,Reggae Tchan,29,1,9,"Cal Adan/Del Rey, Tension/Edu Casanova",206654,6931328,0.99 335,My Love,29,1,9,Jauperi/Zeu Góes,203493,6772813,0.99 336,Latinha de Cerveja,29,1,9,Adriano Bernandes/Edmar Neves,166687,5532564,0.99 337,You Shook Me,30,1,1,J B Lenoir/Willie Dixon,315951,10249958,0.99 338,I Can't Quit You Baby,30,1,1,Willie Dixon,263836,8581414,0.99 339,Communication Breakdown,30,1,1,Jimmy Page/John Bonham/John Paul Jones,192653,6287257,0.99 340,Dazed and Confused,30,1,1,Jimmy Page,401920,13035765,0.99 341,The Girl I Love She Got Long Black Wavy Hair,30,1,1,Jimmy Page/John Bonham/John Estes/John Paul Jones/Robert Plant,183327,5995686,0.99 342,What is and Should Never Be,30,1,1,Jimmy Page/Robert Plant,260675,8497116,0.99 343,Communication Breakdown(2),30,1,1,Jimmy Page/John Bonham/John Paul Jones,161149,5261022,0.99 344,Travelling Riverside Blues,30,1,1,Jimmy Page/Robert Johnson/Robert Plant,312032,10232581,0.99 345,Whole Lotta Love,30,1,1,Jimmy Page/John Bonham/John Paul Jones/Robert Plant/Willie Dixon,373394,12258175,0.99 346,Somethin' Else,30,1,1,Bob Cochran/Sharon Sheeley,127869,4165650,0.99 347,Communication Breakdown(3),30,1,1,Jimmy Page/John Bonham/John Paul Jones,185260,6041133,0.99 348,I Can't Quit You Baby(2),30,1,1,Willie Dixon,380551,12377615,0.99 349,You Shook Me(2),30,1,1,J B Lenoir/Willie Dixon,619467,20138673,0.99 350,How Many More Times,30,1,1,Chester Burnett/Jimmy Page/John Bonham/John Paul Jones/Robert Plant,711836,23092953,0.99 351,Debra Kadabra,31,1,1,Frank Zappa,234553,7649679,0.99 352,Carolina Hard-Core Ecstasy,31,1,1,Frank Zappa,359680,11731061,0.99 353,Sam With The Showing Scalp Flat Top,31,1,1,Don Van Vliet,171284,5572993,0.99 354,Poofter's Froth Wyoming Plans Ahead,31,1,1,Frank Zappa,183902,6007019,0.99 355,200 Years Old,31,1,1,Frank Zappa,272561,8912465,0.99 356,Cucamonga,31,1,1,Frank Zappa,144483,4728586,0.99 357,Advance Romance,31,1,1,Frank Zappa,677694,22080051,0.99 358,Man With The Woman Head,31,1,1,Don Van Vliet,88894,2922044,0.99 359,Muffin Man,31,1,1,Frank Zappa,332878,10891682,0.99 360,Vai-Vai 2001,32,1,10,,276349,9402241,0.99 361,X-9 2001,32,1,10,,273920,9310370,0.99 362,Gavioes 2001,32,1,10,,282723,9616640,0.99 363,Nene 2001,32,1,10,,284969,9694508,0.99 364,Rosas De Ouro 2001,32,1,10,,284342,9721084,0.99 365,Mocidade Alegre 2001,32,1,10,,282488,9599937,0.99 366,Camisa Verde 2001,32,1,10,,283454,9633755,0.99 367,Leandro De Itaquera 2001,32,1,10,,274808,9451845,0.99 368,Tucuruvi 2001,32,1,10,,287921,9883335,0.99 369,Aguia De Ouro 2001,32,1,10,,284160,9698729,0.99 370,Ipiranga 2001,32,1,10,,248293,8522591,0.99 371,Morro Da Casa Verde 2001,32,1,10,,284708,9718778,0.99 372,Perola Negra 2001,32,1,10,,281626,9619196,0.99 373,Sao Lucas 2001,32,1,10,,296254,10020122,0.99 374,Guanabara,33,1,7,Marcos Valle,247614,8499591,0.99 375,Mas Que Nada,33,1,7,Jorge Ben,248398,8255254,0.99 376,Vôo Sobre o Horizonte,33,1,7,J.r.Bertami/Parana,225097,7528825,0.99 377,A Paz,33,1,7,Donato/Gilberto Gil,263183,8619173,0.99 378,Wave (Vou te Contar),33,1,7,Antonio Carlos Jobim,271647,9057557,0.99 379,Água de Beber,33,1,7,Antonio Carlos Jobim/Vinicius de Moraes,146677,4866476,0.99 380,Samba da Bençaco,33,1,7,Baden Powell/Vinicius de Moraes,282200,9440676,0.99 381,Pode Parar,33,1,7,Jorge Vercilo/Jota Maranhao,179408,6046678,0.99 382,Menino do Rio,33,1,7,Caetano Veloso,262713,8737489,0.99 383,Ando Meio Desligado,33,1,7,Caetano Veloso,195813,6547648,0.99 384,Mistério da Raça,33,1,7,Luiz Melodia/Ricardo Augusto,184320,6191752,0.99 385,All Star,33,1,7,Nando Reis,176326,5891697,0.99 386,Menina Bonita,33,1,7,Alexandre Brazil/Pedro Luis/Rodrigo Cabelo,237087,7938246,0.99 387,Pescador de Ilusões,33,1,7,Macelo Yuka/O Rappa,245524,8267067,0.99 388,À Vontade (Live Mix),33,1,7,Bombom/Ed Motta,180636,5972430,0.99 389,Maria Fumaça,33,1,7,Luiz Carlos/Oberdan,141008,4743149,0.99 390,Sambassim (dj patife remix),33,1,7,Alba Carvalho/Fernando Porto,213655,7243166,0.99 391,Garota De Ipanema,34,1,7,Vários,279536,9141343,0.99 392,Tim Tim Por Tim Tim,34,1,7,Vários,213237,7143328,0.99 393,Tarde Em Itapoã,34,1,7,Vários,313704,10344491,0.99 394,Tanto Tempo,34,1,7,Vários,170292,5572240,0.99 395,Eu Vim Da Bahia - Live,34,1,7,Vários,157988,5115428,0.99 396,Alô Alô Marciano,34,1,7,Vários,238106,8013065,0.99 397,Linha Do Horizonte,34,1,7,Vários,279484,9275929,0.99 398,Only A Dream In Rio,34,1,7,Vários,371356,12192989,0.99 399,Abrir A Porta,34,1,7,Vários,271960,8991141,0.99 400,Alice,34,1,7,Vários,165982,5594341,0.99 401,Momentos Que Marcam,34,1,7,Vários,280137,9313740,0.99 402,Um Jantar Pra Dois,34,1,7,Vários,237714,7819755,0.99 403,Bumbo Da Mangueira,34,1,7,Vários,270158,9073350,0.99 404,Mr Funk Samba,34,1,7,Vários,213890,7102545,0.99 405,Santo Antonio,34,1,7,Vários,162716,5492069,0.99 406,Por Você,34,1,7,Vários,205557,6792493,0.99 407,Só Tinha De Ser Com Você,34,1,7,Vários,389642,13085596,0.99 408,Free Speech For The Dumb,35,1,3,Molaney/Morris/Roberts/Wainwright,155428,5076048,0.99 409,It's Electric,35,1,3,Harris/Tatler,213995,6978601,0.99 410,Sabbra Cadabra,35,1,3,Black Sabbath,380342,12418147,0.99 411,Turn The Page,35,1,3,Seger,366524,11946327,0.99 412,Die Die My Darling,35,1,3,Danzig,149315,4867667,0.99 413,Loverman,35,1,3,Cave,472764,15446975,0.99 414,Mercyful Fate,35,1,3,Diamond/Shermann,671712,21942829,0.99 415,Astronomy,35,1,3,A.Bouchard/J.Bouchard/S.Pearlman,397531,13065612,0.99 416,Whiskey In The Jar,35,1,3,Traditional,305005,9943129,0.99 417,Tuesday's Gone,35,1,3,Collins/Van Zandt,545750,17900787,0.99 418,The More I See,35,1,3,Molaney/Morris/Roberts/Wainwright,287973,9378873,0.99 419,A Kind Of Magic,36,1,1,Roger Taylor,262608,8689618,0.99 420,Under Pressure,36,1,1,Queen & David Bowie,236617,7739042,0.99 421,Radio GA GA,36,1,1,Roger Taylor,343745,11358573,0.99 422,I Want It All,36,1,1,Queen,241684,7876564,0.99 423,I Want To Break Free,36,1,1,John Deacon,259108,8552861,0.99 424,Innuendo,36,1,1,Queen,387761,12664591,0.99 425,It's A Hard Life,36,1,1,Freddie Mercury,249417,8112242,0.99 426,Breakthru,36,1,1,Queen,249234,8150479,0.99 427,Who Wants To Live Forever,36,1,1,Brian May,297691,9577577,0.99 428,Headlong,36,1,1,Queen,273057,8921404,0.99 429,The Miracle,36,1,1,Queen,294974,9671923,0.99 430,I'm Going Slightly Mad,36,1,1,Queen,248032,8192339,0.99 431,The Invisible Man,36,1,1,Queen,238994,7920353,0.99 432,Hammer To Fall,36,1,1,Brian May,220316,7255404,0.99 433,Friends Will Be Friends,36,1,1,Freddie Mercury & John Deacon,248920,8114582,0.99 434,The Show Must Go On,36,1,1,Queen,263784,8526760,0.99 435,One Vision,36,1,1,Queen,242599,7936928,0.99 436,Detroit Rock City,37,1,1,"Paul Stanley, B. Ezrin",218880,7146372,0.99 437,Black Diamond,37,1,1,Paul Stanley,314148,10266007,0.99 438,Hard Luck Woman,37,1,1,Paul Stanley,216032,7109267,0.99 439,Sure Know Something,37,1,1,"Paul Stanley, Vincent Poncia",242468,7939886,0.99 440,Love Gun,37,1,1,Paul Stanley,196257,6424915,0.99 441,Deuce,37,1,1,Gene Simmons,185077,6097210,0.99 442,Goin' Blind,37,1,1,"Gene Simmons, S. Coronel",216215,7045314,0.99 443,Shock Me,37,1,1,Ace Frehley,227291,7529336,0.99 444,Do You Love Me,37,1,1,"Paul Stanley, B. Ezrin, K. Fowley",214987,6976194,0.99 445,She,37,1,1,"Gene Simmons, S. Coronel",248346,8229734,0.99 446,I Was Made For Loving You,37,1,1,"Paul Stanley, Vincent Poncia, Desmond Child",271360,9018078,0.99 447,Shout It Out Loud,37,1,1,"Paul Stanley, Gene Simmons, B. Ezrin",219742,7194424,0.99 448,God Of Thunder,37,1,1,Paul Stanley,255791,8309077,0.99 449,Calling Dr. Love,37,1,1,Gene Simmons,225332,7395034,0.99 450,Beth,37,1,1,"S. Penridge, Bob Ezrin, Peter Criss",166974,5360574,0.99 451,Strutter,37,1,1,"Paul Stanley, Gene Simmons",192496,6317021,0.99 452,Rock And Roll All Nite,37,1,1,"Paul Stanley, Gene Simmons",173609,5735902,0.99 453,Cold Gin,37,1,1,Ace Frehley,262243,8609783,0.99 454,Plaster Caster,37,1,1,Gene Simmons,207333,6801116,0.99 455,God Gave Rock 'n' Roll To You,37,1,1,"Paul Stanley, Gene Simmons, Rus Ballard, Bob Ezrin",320444,10441590,0.99 456,Heart of the Night,38,1,2,,273737,9098263,0.99 457,De La Luz,38,1,2,,315219,10518284,0.99 458,Westwood Moon,38,1,2,,295627,9765802,0.99 459,Midnight,38,1,2,,266866,8851060,0.99 460,Playtime,38,1,2,,273580,9070880,0.99 461,Surrender,38,1,2,,287634,9422926,0.99 462,Valentino's,38,1,2,,296124,9848545,0.99 463,Believe,38,1,2,,310778,10317185,0.99 464,As We Sleep,38,1,2,,316865,10429398,0.99 465,When Evening Falls,38,1,2,,298135,9863942,0.99 466,J Squared,38,1,2,,288757,9480777,0.99 467,Best Thing,38,1,2,,274259,9069394,0.99 468,Maria,39,1,4,Billie Joe Armstrong -Words Green Day -Music,167262,5484747,0.99 469,Poprocks And Coke,39,1,4,Billie Joe Armstrong -Words Green Day -Music,158354,5243078,0.99 470,Longview,39,1,4,Billie Joe Armstrong -Words Green Day -Music,234083,7714939,0.99 471,Welcome To Paradise,39,1,4,Billie Joe Armstrong -Words Green Day -Music,224208,7406008,0.99 472,Basket Case,39,1,4,Billie Joe Armstrong -Words Green Day -Music,181629,5951736,0.99 473,When I Come Around,39,1,4,Billie Joe Armstrong -Words Green Day -Music,178364,5839426,0.99 474,She,39,1,4,Billie Joe Armstrong -Words Green Day -Music,134164,4425128,0.99 475,J.A.R. (Jason Andrew Relva),39,1,4,Mike Dirnt -Words Green Day -Music,170997,5645755,0.99 476,Geek Stink Breath,39,1,4,Billie Joe Armstrong -Words Green Day -Music,135888,4408983,0.99 477,Brain Stew,39,1,4,Billie Joe Armstrong -Words Green Day -Music,193149,6305550,0.99 478,Jaded,39,1,4,Billie Joe Armstrong -Words Green Day -Music,90331,2950224,0.99 479,Walking Contradiction,39,1,4,Billie Joe Armstrong -Words Green Day -Music,151170,4932366,0.99 480,Stuck With Me,39,1,4,Billie Joe Armstrong -Words Green Day -Music,135523,4431357,0.99 481,Hitchin' A Ride,39,1,4,Billie Joe Armstrong -Words Green Day -Music,171546,5616891,0.99 482,Good Riddance (Time Of Your Life),39,1,4,Billie Joe Armstrong -Words Green Day -Music,153600,5075241,0.99 483,Redundant,39,1,4,Billie Joe Armstrong -Words Green Day -Music,198164,6481753,0.99 484,Nice Guys Finish Last,39,1,4,Billie Joe Armstrong -Words Green Day -Music,170187,5604618,0.99 485,Minority,39,1,4,Billie Joe Armstrong -Words Green Day -Music,168803,5535061,0.99 486,Warning,39,1,4,Billie Joe Armstrong -Words Green Day -Music,221910,7343176,0.99 487,Waiting,39,1,4,Billie Joe Armstrong -Words Green Day -Music,192757,6316430,0.99 488,Macy's Day Parade,39,1,4,Billie Joe Armstrong -Words Green Day -Music,213420,7075573,0.99 489,Into The Light,40,1,1,David Coverdale,76303,2452653,0.99 490,River Song,40,1,1,David Coverdale,439510,14359478,0.99 491,She Give Me ...,40,1,1,David Coverdale,252551,8385478,0.99 492,Don't You Cry,40,1,1,David Coverdale,347036,11269612,0.99 493,Love Is Blind,40,1,1,David Coverdale/Earl Slick,344999,11409720,0.99 494,Slave,40,1,1,David Coverdale/Earl Slick,291892,9425200,0.99 495,Cry For Love,40,1,1,Bossi/David Coverdale/Earl Slick,293015,9567075,0.99 496,Living On Love,40,1,1,Bossi/David Coverdale/Earl Slick,391549,12785876,0.99 497,Midnight Blue,40,1,1,David Coverdale/Earl Slick,298631,9750990,0.99 498,Too Many Tears,40,1,1,Adrian Vanderberg/David Coverdale,359497,11810238,0.99 499,Don't Lie To Me,40,1,1,David Coverdale/Earl Slick,283585,9288007,0.99 500,Wherever You May Go,40,1,1,David Coverdale,239699,7803074,0.99 501,Grito De Alerta,41,1,7,Gonzaga Jr.,202213,6539422,0.99 502,Não Dá Mais Pra Segurar (Explode Coração),41,1,7,,219768,7083012,0.99 503,Começaria Tudo Outra Vez,41,1,7,,196545,6473395,0.99 504,O Que É O Que É ?,41,1,7,,259291,8650647,0.99 505,Sangrando,41,1,7,Gonzaga Jr/Gonzaguinha,169717,5494406,0.99 506,"Diga Lá, Coração",41,1,7,,255921,8280636,0.99 507,Lindo Lago Do Amor,41,1,7,Gonzaga Jr.,249678,8353191,0.99 508,Eu Apenas Queria Que Voçê Soubesse,41,1,7,,155637,5130056,0.99 509,Com A Perna No Mundo,41,1,7,Gonzaga Jr.,227448,7747108,0.99 510,E Vamos À Luta,41,1,7,,222406,7585112,0.99 511,Um Homem Também Chora (Guerreiro Menino),41,1,7,,207229,6854219,0.99 512,Comportamento Geral,41,1,7,Gonzaga Jr,181577,5997444,0.99 513,Ponto De Interrogação,41,1,7,,180950,5946265,0.99 514,"Espere Por Mim, Morena",41,1,7,Gonzaguinha,207072,6796523,0.99 515,Meia-Lua Inteira,23,1,7,,222093,7466288,0.99 516,Voce e Linda,23,1,7,,242938,8050268,0.99 517,Um Indio,23,1,7,,195944,6453213,0.99 518,Podres Poderes,23,1,7,,259761,8622495,0.99 519,Voce Nao Entende Nada - Cotidiano,23,1,7,,421982,13885612,0.99 520,O Estrangeiro,23,1,7,,374700,12472890,0.99 521,Menino Do Rio,23,1,7,,147670,4862277,0.99 522,Qualquer Coisa,23,1,7,,193410,6372433,0.99 523,Sampa,23,1,7,,185051,6151831,0.99 524,Queixa,23,1,7,,299676,9953962,0.99 525,O Leaozinho,23,1,7,,184398,6098150,0.99 526,Fora Da Ordem,23,1,7,,354011,11746781,0.99 527,Terra,23,1,7,,401319,13224055,0.99 528,"Alegria, Alegria",23,1,7,,169221,5497025,0.99 529,Balada Do Louco,42,1,4,Arnaldo Baptista - Rita Lee,241057,7852328,0.99 530,Ando Meio Desligado,42,1,4,Arnaldo Baptista - Rita Lee - Sérgio Dias,287817,9484504,0.99 531,Top Top,42,1,4,Os Mutantes - Arnolpho Lima Filho,146938,4875374,0.99 532,Baby,42,1,4,Caetano Veloso,177188,5798202,0.99 533,A E O Z,42,1,4,Mutantes,518556,16873005,0.99 534,Panis Et Circenses,42,1,4,Caetano Veloso - Gilberto Gil,125152,4069688,0.99 535,Chão De Estrelas,42,1,4,Orestes Barbosa-Sílvio Caldas,284813,9433620,0.99 536,Vida De Cachorro,42,1,4,Rita Lee - Arnaldo Baptista - Sérgio Baptista,195186,6411149,0.99 537,Bat Macumba,42,1,4,Gilberto Gil - Caetano Veloso,187794,6295223,0.99 538,Desculpe Babe,42,1,4,Arnaldo Baptista - Rita Lee,170422,5637959,0.99 539,Rita Lee,42,1,4,Arnaldo Baptista/Rita Lee/Sérgio Dias,189257,6270503,0.99 540,"Posso Perder Minha Mulher, Minha Mãe, Desde Que Eu Tenha O Rock And Roll",42,1,4,Arnaldo Baptista - Rita Lee - Arnolpho Lima Filho,222955,7346254,0.99 541,Banho De Lua,42,1,4,B. de Filippi - F. Migliaci - Versão: Fred Jorge,221831,7232123,0.99 542,Meu Refrigerador Não Funciona,42,1,4,Arnaldo Baptista - Rita Lee - Sérgio Dias,382981,12495906,0.99 543,Burn,43,1,1,Coverdale/Lord/Paice,453955,14775708,0.99 544,Stormbringer,43,1,1,Coverdale,277133,9050022,0.99 545,Gypsy,43,1,1,Coverdale/Hughes/Lord/Paice,339173,11046952,0.99 546,Lady Double Dealer,43,1,1,Coverdale,233586,7608759,0.99 547,Mistreated,43,1,1,Coverdale,758648,24596235,0.99 548,Smoke On The Water,43,1,1,Gillan/Glover/Lord/Paice,618031,20103125,0.99 549,You Fool No One,43,1,1,Coverdale/Lord/Paice,804101,26369966,0.99 550,Custard Pie,44,1,1,Jimmy Page/Robert Plant,253962,8348257,0.99 551,The Rover,44,1,1,Jimmy Page/Robert Plant,337084,11011286,0.99 552,In My Time Of Dying,44,1,1,John Bonham/John Paul Jones,666017,21676727,0.99 553,Houses Of The Holy,44,1,1,Jimmy Page/Robert Plant,242494,7972503,0.99 554,Trampled Under Foot,44,1,1,John Paul Jones,336692,11154468,0.99 555,Kashmir,44,1,1,John Bonham,508604,16686580,0.99 556,Imperatriz,45,1,7,Guga/Marquinho Lessa/Tuninho Professor,339173,11348710,0.99 557,Beija-Flor,45,1,7,Caruso/Cleber/Deo/Osmar,327000,10991159,0.99 558,Viradouro,45,1,7,Dadinho/Gilbreto Gomes/Gustavo/P.C. Portugal/R. Mocoto,344320,11484362,0.99 559,Mocidade,45,1,7,"Domenil/J. Brito/Joaozinho/Rap, Marcelo Do",261720,8817757,0.99 560,Unidos Da Tijuca,45,1,7,"Douglas/Neves, Vicente Das/Silva, Gilmar L./Toninho Gentil/Wantuir",338834,11440689,0.99 561,Salgueiro,45,1,7,"Augusto/Craig Negoescu/Rocco Filho/Saara, Ze Carlos Da",305920,10294741,0.99 562,Mangueira,45,1,7,Bizuca/Clóvis Pê/Gilson Bernini/Marelo D'Aguia,298318,9999506,0.99 563,União Da Ilha,45,1,7,"Dito/Djalma Falcao/Ilha, Almir Da/Márcio André",330945,11100945,0.99 564,Grande Rio,45,1,7,Carlos Santos/Ciro/Claudio Russo/Zé Luiz,307252,10251428,0.99 565,Portela,45,1,7,Flavio Bororo/Paulo Apparicio/Wagner Alves/Zeca Sereno,319608,10712216,0.99 566,Caprichosos,45,1,7,Gule/Jorge 101/Lequinho/Luiz Piao,351320,11870956,0.99 567,Tradição,45,1,7,Adalto Magalha/Lourenco,269165,9114880,0.99 568,Império Serrano,45,1,7,Arlindo Cruz/Carlos Sena/Elmo Caetano/Mauricao,334942,11161196,0.99 569,Tuiuti,45,1,7,"Claudio Martins/David Lima/Kleber Rodrigues/Livre, Cesare Som",259657,8749492,0.99 570,(Da Le) Yaleo,46,1,1,Santana,353488,11769507,0.99 571,Love Of My Life,46,1,1,Carlos Santana & Dave Matthews,347820,11634337,0.99 572,Put Your Lights On,46,1,1,E. Shrody,285178,9394769,0.99 573,Africa Bamba,46,1,1,"I. Toure, S. Tidiane Toure, Carlos Santana & K. Perazzo",282827,9492487,0.99 574,Smooth,46,1,1,M. Itaal Shur & Rob Thomas,298161,9867455,0.99 575,Do You Like The Way,46,1,1,L. Hill,354899,11741062,0.99 576,Maria Maria,46,1,1,"W. Jean, J. Duplessis, Carlos Santana, K. Perazzo & R. Rekow",262635,8664601,0.99 577,Migra,46,1,1,"R. Taha, Carlos Santana & T. Lindsay",329064,10963305,0.99 578,Corazon Espinado,46,1,1,F. Olivera,276114,9206802,0.99 579,Wishing It Was,46,1,1,"Eale-Eye Cherry, M. Simpson, J. King & M. Nishita",292832,9771348,0.99 580,El Farol,46,1,1,Carlos Santana & KC Porter,291160,9599353,0.99 581,Primavera,46,1,1,KC Porter & JB Eckl,378618,12504234,0.99 582,The Calling,46,1,1,Carlos Santana & C. Thompson,747755,24703884,0.99 583,Solução,47,1,7,,247431,8100449,0.99 584,Manuel,47,1,7,,230269,7677671,0.99 585,Entre E Ouça,47,1,7,,286302,9391004,0.99 586,Um Contrato Com Deus,47,1,7,,202501,6636465,0.99 587,Um Jantar Pra Dois,47,1,7,,244009,8021589,0.99 588,Vamos Dançar,47,1,7,,226194,7617432,0.99 589,Um Love,47,1,7,,181603,6095524,0.99 590,Seis Da Tarde,47,1,7,,238445,7935898,0.99 591,Baixo Rio,47,1,7,,198008,6521676,0.99 592,Sombras Do Meu Destino,47,1,7,,280685,9161539,0.99 593,Do You Have Other Loves?,47,1,7,,295235,9604273,0.99 594,Agora Que O Dia Acordou,47,1,7,,323213,10572752,0.99 595,Já!!!,47,1,7,,217782,7103608,0.99 596,A Rua,47,1,7,,238027,7930264,0.99 597,Now's The Time,48,1,2,Miles Davis,197459,6358868,0.99 598,Jeru,48,1,2,Miles Davis,193410,6222536,0.99 599,Compulsion,48,1,2,Miles Davis,345025,11254474,0.99 600,Tempus Fugit,48,1,2,Miles Davis,231784,7548434,0.99 601,Walkin',48,1,2,Miles Davis,807392,26411634,0.99 602,'Round Midnight,48,1,2,Miles Davis,357459,11590284,0.99 603,Bye Bye Blackbird,48,1,2,Miles Davis,476003,15549224,0.99 604,New Rhumba,48,1,2,Miles Davis,277968,9018024,0.99 605,Generique,48,1,2,Miles Davis,168777,5437017,0.99 606,Summertime,48,1,2,Miles Davis,200437,6461370,0.99 607,So What,48,1,2,Miles Davis,564009,18360449,0.99 608,The Pan Piper,48,1,2,Miles Davis,233769,7593713,0.99 609,Someday My Prince Will Come,48,1,2,Miles Davis,544078,17890773,0.99 610,My Funny Valentine (Live),49,1,2,Miles Davis,907520,29416781,0.99 611,E.S.P.,49,1,2,Miles Davis,330684,11079866,0.99 612,Nefertiti,49,1,2,Miles Davis,473495,15478450,0.99 613,Petits Machins (Little Stuff),49,1,2,Miles Davis,487392,16131272,0.99 614,Miles Runs The Voodoo Down,49,1,2,Miles Davis,843964,27967919,0.99 615,Little Church (Live),49,1,2,Miles Davis,196101,6273225,0.99 616,Black Satin,49,1,2,Miles Davis,316682,10529483,0.99 617,Jean Pierre (Live),49,1,2,Miles Davis,243461,7955114,0.99 618,Time After Time,49,1,2,Miles Davis,220734,7292197,0.99 619,Portia,49,1,2,Miles Davis,378775,12520126,0.99 620,Space Truckin',50,1,1,Blackmore/Gillan/Glover/Lord/Paice,1196094,39267613,0.99 621,Going Down / Highway Star,50,1,1,Gillan/Glover/Lord/Nix - Blackmore/Paice,913658,29846063,0.99 622,Mistreated (Alternate Version),50,1,1,Blackmore/Coverdale,854700,27775442,0.99 623,You Fool No One (Alternate Version),50,1,1,Blackmore/Coverdale/Lord/Paice,763924,24887209,0.99 624,Jeepers Creepers,51,1,2,,185965,5991903,0.99 625,Blue Rythm Fantasy,51,1,2,,348212,11204006,0.99 626,Drum Boogie,51,1,2,,191555,6185636,0.99 627,Let Me Off Uptown,51,1,2,,187637,6034685,0.99 628,Leave Us Leap,51,1,2,,182726,5898810,0.99 629,Opus No.1,51,1,2,,179800,5846041,0.99 630,Boogie Blues,51,1,2,,204199,6603153,0.99 631,How High The Moon,51,1,2,,201430,6529487,0.99 632,Disc Jockey Jump,51,1,2,,193149,6260820,0.99 633,Up An' Atom,51,1,2,,179565,5822645,0.99 634,Bop Boogie,51,1,2,,189596,6093124,0.99 635,Lemon Drop,51,1,2,,194089,6287531,0.99 636,Coronation Drop,51,1,2,,176222,5899898,0.99 637,Overtime,51,1,2,,163030,5432236,0.99 638,Imagination,51,1,2,,289306,9444385,0.99 639,Don't Take Your Love From Me,51,1,2,,282331,9244238,0.99 640,Midget,51,1,2,,217025,7257663,0.99 641,I'm Coming Virginia,51,1,2,,280163,9209827,0.99 642,Payin' Them Dues Blues,51,1,2,,198556,6536918,0.99 643,Jungle Drums,51,1,2,,199627,6546063,0.99 644,Showcase,51,1,2,,201560,6697510,0.99 645,Swedish Schnapps,51,1,2,,191268,6359750,0.99 646,Samba Da Bênção,52,1,11,,409965,13490008,0.99 647,Pot-Pourri N.º 4,52,1,11,,392437,13125975,0.99 648,Onde Anda Você,52,1,11,,168437,5550356,0.99 649,Samba Da Volta,52,1,11,,170631,5676090,0.99 650,Canto De Ossanha,52,1,11,,204956,6771624,0.99 651,Pot-Pourri N.º 5,52,1,11,,219898,7117769,0.99 652,Formosa,52,1,11,,137482,4560873,0.99 653,Como É Duro Trabalhar,52,1,11,,226168,7541177,0.99 654,Minha Namorada,52,1,11,,244297,7927967,0.99 655,Por Que Será,52,1,11,,162142,5371483,0.99 656,Berimbau,52,1,11,,190667,6335548,0.99 657,Deixa,52,1,11,,179826,5932799,0.99 658,Pot-Pourri N.º 2,52,1,11,,211748,6878359,0.99 659,Samba Em Prelúdio,52,1,11,,212636,6923473,0.99 660,Carta Ao Tom 74,52,1,11,,162560,5382354,0.99 661,Linha de Passe (João Bosco),53,1,7,,230948,7902328,0.99 662,Pela Luz dos Olhos Teus (Miúcha e Tom Jobim),53,1,7,,163970,5399626,0.99 663,Chão de Giz (Elba Ramalho),53,1,7,,274834,9016916,0.99 664,Marina (Dorival Caymmi),53,1,7,,172643,5523628,0.99 665,Aquarela (Toquinho),53,1,7,,259944,8480140,0.99 666,Coração do Agreste (Fafá de Belém),53,1,7,,258194,8380320,0.99 667,Dona (Roupa Nova),53,1,7,,243356,7991295,0.99 668,Começaria Tudo Outra Vez (Maria Creuza),53,1,7,,206994,6851151,0.99 669,Caçador de Mim (Sá & Guarabyra),53,1,7,,238341,7751360,0.99 670,Romaria (Renato Teixeira),53,1,7,,244793,8033885,0.99 671,As Rosas Não Falam (Beth Carvalho),53,1,7,,116767,3836641,0.99 672,Wave (Os Cariocas),53,1,7,,130063,4298006,0.99 673,Garota de Ipanema (Dick Farney),53,1,7,,174367,5767474,0.99 674,Preciso Apender a Viver Só (Maysa),53,1,7,,143464,4642359,0.99 675,Susie Q,54,1,1,Hawkins-Lewis-Broadwater,275565,9043825,0.99 676,I Put A Spell On You,54,1,1,Jay Hawkins,272091,8943000,0.99 677,Proud Mary,54,1,1,J. C. Fogerty,189022,6229590,0.99 678,Bad Moon Rising,54,1,1,J. C. Fogerty,140146,4609835,0.99 679,Lodi,54,1,1,J. C. Fogerty,191451,6260214,0.99 680,Green River,54,1,1,J. C. Fogerty,154279,5105874,0.99 681,Commotion,54,1,1,J. C. Fogerty,162899,5354252,0.99 682,Down On The Corner,54,1,1,J. C. Fogerty,164858,5521804,0.99 683,Fortunate Son,54,1,1,J. C. Fogerty,140329,4617559,0.99 684,Travelin' Band,54,1,1,J. C. Fogerty,129358,4270414,0.99 685,Who'll Stop The Rain,54,1,1,J. C. Fogerty,149394,4899579,0.99 686,Up Around The Bend,54,1,1,J. C. Fogerty,162429,5368701,0.99 687,Run Through The Jungle,54,1,1,J. C. Fogerty,186044,6156567,0.99 688,Lookin' Out My Back Door,54,1,1,J. C. Fogerty,152946,5034670,0.99 689,Long As I Can See The Light,54,1,1,J. C. Fogerty,213237,6924024,0.99 690,I Heard It Through The Grapevine,54,1,1,Whitfield-Strong,664894,21947845,0.99 691,Have You Ever Seen The Rain?,54,1,1,J. C. Fogerty,160052,5263675,0.99 692,Hey Tonight,54,1,1,J. C. Fogerty,162847,5343807,0.99 693,Sweet Hitch-Hiker,54,1,1,J. C. Fogerty,175490,5716603,0.99 694,Someday Never Comes,54,1,1,J. C. Fogerty,239360,7945235,0.99 695,Walking On The Water,55,1,1,J.C. Fogerty,281286,9302129,0.99 696,"Suzie-Q, Pt. 2",55,1,1,J.C. Fogerty,244114,7986637,0.99 697,Born On The Bayou,55,1,1,J.C. Fogerty,316630,10361866,0.99 698,Good Golly Miss Molly,55,1,1,J.C. Fogerty,163604,5348175,0.99 699,Tombstone Shadow,55,1,1,J.C. Fogerty,218880,7209080,0.99 700,Wrote A Song For Everyone,55,1,1,J.C. Fogerty,296385,9675875,0.99 701,Night Time Is The Right Time,55,1,1,J.C. Fogerty,190119,6211173,0.99 702,Cotton Fields,55,1,1,J.C. Fogerty,178181,5919224,0.99 703,It Came Out Of The Sky,55,1,1,J.C. Fogerty,176718,5807474,0.99 704,Don't Look Now,55,1,1,J.C. Fogerty,131918,4366455,0.99 705,The Midnight Special,55,1,1,J.C. Fogerty,253596,8297482,0.99 706,Before You Accuse Me,55,1,1,J.C. Fogerty,207804,6815126,0.99 707,My Baby Left Me,55,1,1,J.C. Fogerty,140460,4633440,0.99 708,Pagan Baby,55,1,1,J.C. Fogerty,385619,12713813,0.99 709,(Wish I Could) Hideaway,55,1,1,J.C. Fogerty,228466,7432978,0.99 710,It's Just A Thought,55,1,1,J.C. Fogerty,237374,7778319,0.99 711,Molina,55,1,1,J.C. Fogerty,163239,5390811,0.99 712,Born To Move,55,1,1,J.C. Fogerty,342804,11260814,0.99 713,Lookin' For A Reason,55,1,1,J.C. Fogerty,209789,6933135,0.99 714,Hello Mary Lou,55,1,1,J.C. Fogerty,132832,4476563,0.99 715,Gatas Extraordinárias,56,1,7,,212506,7095702,0.99 716,Brasil,56,1,7,,243696,7911683,0.99 717,Eu Sou Neguinha (Ao Vivo),56,1,7,,251768,8376000,0.99 718,Geração Coca-Cola (Ao Vivo),56,1,7,,228153,7573301,0.99 719,Lanterna Dos Afogados,56,1,7,,204538,6714582,0.99 720,Coroné Antonio Bento,56,1,7,,200437,6713066,0.99 721,"Você Passa, Eu Acho Graça (Ao Vivo)",56,1,7,,206733,6943576,0.99 722,Meu Mundo Fica Completo (Com Você),56,1,7,,247771,8322240,0.99 723,1° De Julho,56,1,7,,270262,9017535,0.99 724,Música Urbana 2,56,1,7,,194899,6383472,0.99 725,Vida Bandida (Ao Vivo),56,1,7,,192626,6360785,0.99 726,Palavras Ao Vento,56,1,7,,212453,7048676,0.99 727,Não Sei O Que Eu Quero Da Vida,56,1,7,,151849,5024963,0.99 728,Woman Is The Nigger Of The World (Ao Vivo),56,1,7,,298919,9724145,0.99 729,Juventude Transviada (Ao Vivo),56,1,7,,278622,9183808,0.99 730,Malandragem,57,1,7,,247588,8165048,0.99 731,O Segundo Sol,57,1,7,,252133,8335629,0.99 732,Smells Like Teen Spirit (Ao Vivo),57,1,7,,316865,10384506,0.99 733,E.C.T.,57,1,7,,227500,7571834,0.99 734,Todo Amor Que Houver Nesta Vida,57,1,7,,227160,7420347,0.99 735,Metrô. Linha 743,57,1,7,,174654,5837495,0.99 736,Nós (Ao Vivo),57,1,7,,193828,6498661,0.99 737,Na Cadência Do Samba,57,1,7,,196075,6483952,0.99 738,Admirável Gado Novo,57,1,7,,274390,9144031,0.99 739,Eleanor Rigby,57,1,7,,189466,6303205,0.99 740,Socorro,57,1,7,,258586,8549393,0.99 741,Blues Da Piedade,57,1,7,,257123,8472964,0.99 742,Rubens,57,1,7,,211853,7026317,0.99 743,Não Deixe O Samba Morrer - Cassia Eller e Alcione,57,1,7,,268173,8936345,0.99 744,Mis Penas Lloraba Yo (Ao Vivo) Soy Gitano (Tangos),57,1,7,,188473,6195854,0.99 745,Comin' Home,58,1,1,Bolin/Coverdale/Paice,235781,7644604,0.99 746,Lady Luck,58,1,1,Cook/Coverdale,168202,5501379,0.99 747,Gettin' Tighter,58,1,1,Bolin/Hughes,218044,7176909,0.99 748,Dealer,58,1,1,Bolin/Coverdale,230922,7591066,0.99 749,I Need Love,58,1,1,Bolin/Coverdale,263836,8701064,0.99 750,Drifter,58,1,1,Bolin/Coverdale,242834,8001505,0.99 751,Love Child,58,1,1,Bolin/Coverdale,188160,6173806,0.99 752,This Time Around / Owed to 'G' [Instrumental],58,1,1,Bolin/Hughes/Lord,370102,11995679,0.99 753,You Keep On Moving,58,1,1,Coverdale/Hughes,319111,10447868,0.99 754,Speed King,59,1,1,"Blackmore, Gillan, Glover, Lord, Paice",264385,8587578,0.99 755,Bloodsucker,59,1,1,"Blackmore, Gillan, Glover, Lord, Paice",256261,8344405,0.99 756,Child In Time,59,1,1,"Blackmore, Gillan, Glover, Lord, Paice",620460,20230089,0.99 757,Flight Of The Rat,59,1,1,"Blackmore, Gillan, Glover, Lord, Paice",478302,15563967,0.99 758,Into The Fire,59,1,1,"Blackmore, Gillan, Glover, Lord, Paice",210259,6849310,0.99 759,Living Wreck,59,1,1,"Blackmore, Gillan, Glover, Lord, Paice",274886,8993056,0.99 760,Hard Lovin' Man,59,1,1,"Blackmore, Gillan, Glover, Lord, Paice",431203,13931179,0.99 761,Fireball,60,1,1,"Ritchie Blackmore, Ian Gillan, Roger Glover, Jon Lord, Ian Paice",204721,6714807,0.99 762,No No No,60,1,1,"Ritchie Blackmore, Ian Gillan, Roger Glover, Jon Lord, Ian Paice",414902,13646606,0.99 763,Strange Kind Of Woman,60,1,1,"Ritchie Blackmore, Ian Gillan, Roger Glover, Jon Lord, Ian Paice",247092,8072036,0.99 764,Anyone's Daughter,60,1,1,"Ritchie Blackmore, Ian Gillan, Roger Glover, Jon Lord, Ian Paice",284682,9354480,0.99 765,The Mule,60,1,1,"Ritchie Blackmore, Ian Gillan, Roger Glover, Jon Lord, Ian Paice",322063,10638390,0.99 766,Fools,60,1,1,"Ritchie Blackmore, Ian Gillan, Roger Glover, Jon Lord, Ian Paice",500427,16279366,0.99 767,No One Came,60,1,1,"Ritchie Blackmore, Ian Gillan, Roger Glover, Jon Lord, Ian Paice",385880,12643813,0.99 768,Knocking At Your Back Door,61,1,1,"Richie Blackmore, Ian Gillian, Roger Glover",424829,13779332,0.99 769,Bad Attitude,61,1,1,"Richie Blackmore, Ian Gillian, Roger Glover, Jon Lord",307905,10035180,0.99 770,Child In Time (Son Of Aleric - Instrumental),61,1,1,"Richie Blackmore, Ian Gillian, Roger Glover, Jon Lord, Ian Paice",602880,19712753,0.99 771,Nobody's Home,61,1,1,"Richie Blackmore, Ian Gillian, Roger Glover, Jon Lord, Ian Paice",243017,7929493,0.99 772,Black Night,61,1,1,"Richie Blackmore, Ian Gillian, Roger Glover, Jon Lord, Ian Paice",368770,12058906,0.99 773,Perfect Strangers,61,1,1,"Richie Blackmore, Ian Gillian, Roger Glover",321149,10445353,0.99 774,The Unwritten Law,61,1,1,"Richie Blackmore, Ian Gillian, Roger Glover, Ian Paice",295053,9740361,0.99 775,Call Of The Wild,61,1,1,"Richie Blackmore, Ian Gillian, Roger Glover, Jon Lord",293851,9575295,0.99 776,Hush,61,1,1,South,213054,6944928,0.99 777,Smoke On The Water,61,1,1,"Richie Blackmore, Ian Gillian, Roger Glover, Jon Lord, Ian Paice",464378,15180849,0.99 778,Space Trucking,61,1,1,"Richie Blackmore, Ian Gillian, Roger Glover, Jon Lord, Ian Paice",341185,11122183,0.99 779,Highway Star,62,1,1,Ian Gillan/Ian Paice/Jon Lord/Ritchie Blckmore/Roger Glover,368770,12012452,0.99 780,Maybe I'm A Leo,62,1,1,Ian Gillan/Ian Paice/Jon Lord/Ritchie Blckmore/Roger Glover,290455,9502646,0.99 781,Pictures Of Home,62,1,1,Ian Gillan/Ian Paice/Jon Lord/Ritchie Blckmore/Roger Glover,303777,9903835,0.99 782,Never Before,62,1,1,Ian Gillan/Ian Paice/Jon Lord/Ritchie Blckmore/Roger Glover,239830,7832790,0.99 783,Smoke On The Water,62,1,1,Ian Gillan/Ian Paice/Jon Lord/Ritchie Blckmore/Roger Glover,340871,11246496,0.99 784,Lazy,62,1,1,Ian Gillan/Ian Paice/Jon Lord/Ritchie Blckmore/Roger Glover,442096,14397671,0.99 785,Space Truckin',62,1,1,Ian Gillan/Ian Paice/Jon Lord/Ritchie Blckmore/Roger Glover,272796,8981030,0.99 786,Vavoom : Ted The Mechanic,63,1,1,"Ian Gillan, Roger Glover, Jon Lord, Steve Morse, Ian Paice",257384,8510755,0.99 787,Loosen My Strings,63,1,1,"Ian Gillan, Roger Glover, Jon Lord, Steve Morse, Ian Paice",359680,11702232,0.99 788,Soon Forgotten,63,1,1,"Ian Gillan, Roger Glover, Jon Lord, Steve Morse, Ian Paice",287791,9401383,0.99 789,Sometimes I Feel Like Screaming,63,1,1,"Ian Gillan, Roger Glover, Jon Lord, Steve Morse, Ian Paice",451840,14789410,0.99 790,Cascades : I'm Not Your Lover,63,1,1,"Ian Gillan, Roger Glover, Jon Lord, Steve Morse, Ian Paice",283689,9209693,0.99 791,The Aviator,63,1,1,"Ian Gillan, Roger Glover, Jon Lord, Steve Morse, Ian Paice",320992,10532053,0.99 792,Rosa's Cantina,63,1,1,"Ian Gillan, Roger Glover, Jon Lord, Steve Morse, Ian Paice",312372,10323804,0.99 793,A Castle Full Of Rascals,63,1,1,"Ian Gillan, Roger Glover, Jon Lord, Steve Morse, Ian Paice",311693,10159566,0.99 794,A Touch Away,63,1,1,"Ian Gillan, Roger Glover, Jon Lord, Steve Morse, Ian Paice",276323,9098561,0.99 795,Hey Cisco,63,1,1,"Ian Gillan, Roger Glover, Jon Lord, Steve Morse, Ian Paice",354089,11600029,0.99 796,Somebody Stole My Guitar,63,1,1,"Ian Gillan, Roger Glover, Jon Lord, Steve Morse, Ian Paice",249443,8180421,0.99 797,The Purpendicular Waltz,63,1,1,"Ian Gillan, Roger Glover, Jon Lord, Steve Morse, Ian Paice",283924,9299131,0.99 798,King Of Dreams,64,1,1,"Blackmore, Glover, Turner",328385,10733847,0.99 799,The Cut Runs Deep,64,1,1,"Blackmore, Glover, Turner, Lord, Paice",342752,11191650,0.99 800,Fire In The Basement,64,1,1,"Blackmore, Glover, Turner, Lord, Paice",283977,9267550,0.99 801,Truth Hurts,64,1,1,"Blackmore, Glover, Turner",314827,10224612,0.99 802,Breakfast In Bed,64,1,1,"Blackmore, Glover, Turner",317126,10323804,0.99 803,Love Conquers All,64,1,1,"Blackmore, Glover, Turner",227186,7328516,0.99 804,Fortuneteller,64,1,1,"Blackmore, Glover, Turner, Lord, Paice",349335,11369671,0.99 805,Too Much Is Not Enough,64,1,1,"Turner, Held, Greenwood",257724,8382800,0.99 806,Wicked Ways,64,1,1,"Blackmore, Glover, Turner, Lord, Paice",393691,12826582,0.99 807,Stormbringer,65,1,1,D.Coverdale/R.Blackmore/Ritchie Blackmore,246413,8044864,0.99 808,Love Don't Mean a Thing,65,1,1,D.Coverdale/G.Hughes/Glenn Hughes/I.Paice/Ian Paice/J.Lord/John Lord/R.Blackmore/Ritchie Blackmore,263862,8675026,0.99 809,Holy Man,65,1,1,D.Coverdale/G.Hughes/Glenn Hughes/J.Lord/John Lord,270236,8818093,0.99 810,Hold On,65,1,1,D.Coverdal/G.Hughes/Glenn Hughes/I.Paice/Ian Paice/J.Lord/John Lord,306860,10022428,0.99 811,Lady Double Dealer,65,1,1,D.Coverdale/R.Blackmore/Ritchie Blackmore,201482,6554330,0.99 812,You Can't Do it Right (With the One You Love),65,1,1,D.Coverdale/G.Hughes/Glenn Hughes/R.Blackmore/Ritchie Blackmore,203755,6709579,0.99 813,High Ball Shooter,65,1,1,D.Coverdale/G.Hughes/Glenn Hughes/I.Paice/Ian Paice/J.Lord/John Lord/R.Blackmore/Ritchie Blackmore,267833,8772471,0.99 814,The Gypsy,65,1,1,D.Coverdale/G.Hughes/Glenn Hughes/I.Paice/Ian Paice/J.Lord/John Lord/R.Blackmore/Ritchie Blackmore,242886,7946614,0.99 815,Soldier Of Fortune,65,1,1,D.Coverdale/R.Blackmore/Ritchie Blackmore,193750,6315321,0.99 816,The Battle Rages On,66,1,1,ian paice/jon lord,356963,11626228,0.99 817,Lick It Up,66,1,1,roger glover,240274,7792604,0.99 818,Anya,66,1,1,jon lord/roger glover,392437,12754921,0.99 819,Talk About Love,66,1,1,roger glover,247823,8072171,0.99 820,Time To Kill,66,1,1,roger glover,351033,11354742,0.99 821,Ramshackle Man,66,1,1,roger glover,334445,10874679,0.99 822,A Twist In The Tail,66,1,1,roger glover,257462,8413103,0.99 823,Nasty Piece Of Work,66,1,1,jon lord/roger glover,276662,9076997,0.99 824,Solitaire,66,1,1,roger glover,282226,9157021,0.99 825,One Man's Meat,66,1,1,roger glover,278804,9068960,0.99 826,Pour Some Sugar On Me,67,1,1,,292519,9518842,0.99 827,Photograph,67,1,1,,248633,8108507,0.99 828,Love Bites,67,1,1,,346853,11305791,0.99 829,Let's Get Rocked,67,1,1,,296019,9724150,0.99 830,Two Steps Behind [Acoustic Version],67,1,1,,259787,8523388,0.99 831,Animal,67,1,1,,244741,7985133,0.99 832,Heaven Is,67,1,1,,214021,6988128,0.99 833,Rocket,67,1,1,,247248,8092463,0.99 834,When Love & Hate Collide,67,1,1,,257280,8364633,0.99 835,Action,67,1,1,,220604,7130830,0.99 836,Make Love Like A Man,67,1,1,,255660,8309725,0.99 837,Armageddon It,67,1,1,,322455,10522352,0.99 838,Have You Ever Needed Someone So Bad,67,1,1,,319320,10400020,0.99 839,Rock Of Ages,67,1,1,,248424,8150318,0.99 840,Hysteria,67,1,1,,355056,11622738,0.99 841,Bringin' On The Heartbreak,67,1,1,,272457,8853324,0.99 842,Roll Call,68,1,2,Jim Beard,321358,10653494,0.99 843,Otay,68,1,2,"John Scofield, Robert Aries, Milton Chambers and Gary Grainger",423653,14176083,0.99 844,Groovus Interruptus,68,1,2,Jim Beard,319373,10602166,0.99 845,Paris On Mine,68,1,2,Jon Herington,368875,12059507,0.99 846,In Time,68,1,2,Sylvester Stewart,368953,12287103,0.99 847,Plan B,68,1,2,"Dean Brown, Dennis Chambers & Jim Beard",272039,9032315,0.99 848,Outbreak,68,1,2,Jim Beard & Jon Herington,659226,21685807,0.99 849,"Baltimore, DC",68,1,2,John Scofield,346932,11394473,0.99 850,Talkin Loud and Saying Nothin,68,1,2,James Brown & Bobby Byrd,360411,11994859,0.99 851,Pétala,69,1,7,,270080,8856165,0.99 852,Meu Bem-Querer,69,1,7,,255608,8330047,0.99 853,Cigano,69,1,7,,304692,10037362,0.99 854,Boa Noite,69,1,7,,338755,11283582,0.99 855,Fato Consumado,69,1,7,,211565,7018586,0.99 856,Faltando Um Pedaço,69,1,7,,267728,8788760,0.99 857,Álibi,69,1,7,,213237,6928434,0.99 858,Esquinas,69,1,7,,280999,9096726,0.99 859,Se...,69,1,7,,286432,9413777,0.99 860,Eu Te Devoro,69,1,7,,311614,10312775,0.99 861,Lilás,69,1,7,,274181,9049542,0.99 862,Acelerou,69,1,7,,284081,9396942,0.99 863,Um Amor Puro,69,1,7,,327784,10687311,0.99 864,Samurai,70,1,7,Djavan,330997,10872787,0.99 865,Nem Um Dia,70,1,7,Djavan,337423,11181446,0.99 866,Oceano,70,1,7,Djavan,217338,7026441,0.99 867,Açai,70,1,7,Djavan,270968,8893682,0.99 868,Serrado,70,1,7,Djavan,295314,9842240,0.99 869,Flor De Lis,70,1,7,Djavan,236355,7801108,0.99 870,Amar É Tudo,70,1,7,Djavan,211617,7073899,0.99 871,Azul,70,1,7,Djavan,253962,8381029,0.99 872,Seduzir,70,1,7,Djavan,277524,9163253,0.99 873,A Carta,70,1,7,"Djavan - Gabriel, O Pensador",347297,11493463,0.99 874,Sina,70,1,7,Djavan,268173,8906539,0.99 875,Acelerou,70,1,7,Djavan,284133,9391439,0.99 876,Um Amor Puro,70,1,7,Djavan,327105,10664698,0.99 877,O Bêbado e a Equilibrista,71,1,7,,223059,7306143,0.99 878,O Mestre-Sala dos Mares,71,1,7,,186226,6180414,0.99 879,Atrás da Porta,71,1,7,,166608,5432518,0.99 880,"Dois Pra Lá, Dois Pra Cá",71,1,7,,263026,8684639,0.99 881,Casa no Campo,71,1,7,,170788,5531841,0.99 882,Romaria,71,1,7,,242834,7968525,0.99 883,"Alô, Alô, Marciano",71,1,7,,241397,8137254,0.99 884,Me Deixas Louca,71,1,7,,214831,6888030,0.99 885,Fascinação,71,1,7,,180793,5793959,0.99 886,Saudosa Maloca,71,1,7,,278125,9059416,0.99 887,As Aparências Enganam,71,1,7,,247379,8014346,0.99 888,Madalena,71,1,7,,157387,5243721,0.99 889,Maria Rosa,71,1,7,,232803,7592504,0.99 890,Aprendendo A Jogar,71,1,7,,290664,9391041,0.99 891,Layla,72,1,6,Clapton/Gordon,430733,14115792,0.99 892,Badge,72,1,6,Clapton/Harrison,163552,5322942,0.99 893,I Feel Free,72,1,6,Bruce/Clapton,174576,5725684,0.99 894,Sunshine Of Your Love,72,1,6,Bruce/Clapton,252891,8225889,0.99 895,Crossroads,72,1,6,Clapton/Robert Johnson Arr: Eric Clapton,253335,8273540,0.99 896,Strange Brew,72,1,6,Clapton/Collins/Pappalardi,167810,5489787,0.99 897,White Room,72,1,6,Bruce/Clapton,301583,9872606,0.99 898,Bell Bottom Blues,72,1,6,Clapton,304744,9946681,0.99 899,Cocaine,72,1,6,Cale/Clapton,215928,7138399,0.99 900,I Shot The Sheriff,72,1,6,Marley,263862,8738973,0.99 901,After Midnight,72,1,6,Clapton/J. J. Cale,191320,6460941,0.99 902,Swing Low Sweet Chariot,72,1,6,Clapton/Trad. Arr. Clapton,208143,6896288,0.99 903,Lay Down Sally,72,1,6,Clapton/Levy,231732,7774207,0.99 904,Knockin On Heavens Door,72,1,6,Clapton/Dylan,264411,8758819,0.99 905,Wonderful Tonight,72,1,6,Clapton,221387,7326923,0.99 906,Let It Grow,72,1,6,Clapton,297064,9742568,0.99 907,Promises,72,1,6,Clapton/F.eldman/Linn,180401,6006154,0.99 908,I Can't Stand It,72,1,6,Clapton,249730,8271980,0.99 909,Signe,73,1,6,Eric Clapton,193515,6475042,0.99 910,Before You Accuse Me,73,1,6,Eugene McDaniel,224339,7456807,0.99 911,Hey Hey,73,1,6,Big Bill Broonzy,196466,6543487,0.99 912,Tears In Heaven,73,1,6,"Eric Clapton, Will Jennings",274729,9032835,0.99 913,Lonely Stranger,73,1,6,Eric Clapton,328724,10894406,0.99 914,Nobody Knows You When You're Down & Out,73,1,6,Jimmy Cox,231836,7669922,0.99 915,Layla,73,1,6,"Eric Clapton, Jim Gordon",285387,9490542,0.99 916,Running On Faith,73,1,6,Jerry Lynn Williams,378984,12536275,0.99 917,Walkin' Blues,73,1,6,Robert Johnson,226429,7435192,0.99 918,Alberta,73,1,6,Traditional,222406,7412975,0.99 919,San Francisco Bay Blues,73,1,6,Jesse Fuller,203363,6724021,0.99 920,Malted Milk,73,1,6,Robert Johnson,216528,7096781,0.99 921,Old Love,73,1,6,"Eric Clapton, Robert Cray",472920,15780747,0.99 922,Rollin' And Tumblin',73,1,6,McKinley Morgenfield (Muddy Waters),251768,8407355,0.99 923,Collision,74,1,4,Jon Hudson/Mike Patton,204303,6656596,0.99 924,Stripsearch,74,1,4,Jon Hudson/Mike Bordin/Mike Patton,270106,8861119,0.99 925,Last Cup Of Sorrow,74,1,4,Bill Gould/Mike Patton,251663,8221247,0.99 926,Naked In Front Of The Computer,74,1,4,Mike Patton,128757,4225077,0.99 927,Helpless,74,1,4,Bill Gould/Mike Bordin/Mike Patton,326217,10753135,0.99 928,Mouth To Mouth,74,1,4,Bill Gould/Jon Hudson/Mike Bordin/Mike Patton,228493,7505887,0.99 929,Ashes To Ashes,74,1,4,Bill Gould/Jon Hudson/Mike Bordin/Mike Patton/Roddy Bottum,217391,7093746,0.99 930,She Loves Me Not,74,1,4,Bill Gould/Mike Bordin/Mike Patton,209867,6887544,0.99 931,Got That Feeling,74,1,4,Mike Patton,140852,4643227,0.99 932,Paths Of Glory,74,1,4,Bill Gould/Jon Hudson/Mike Bordin/Mike Patton/Roddy Bottum,257253,8436300,0.99 933,Home Sick Home,74,1,4,Mike Patton,119040,3898976,0.99 934,Pristina,74,1,4,Bill Gould/Mike Patton,232698,7497361,0.99 935,Land Of Sunshine,75,1,4,,223921,7353567,0.99 936,Caffeine,75,1,4,,267937,8747367,0.99 937,Midlife Crisis,75,1,4,,263235,8628841,0.99 938,RV,75,1,4,,223242,7288162,0.99 939,Smaller And Smaller,75,1,4,,310831,10180103,0.99 940,Everything's Ruined,75,1,4,,273658,9010917,0.99 941,Malpractice,75,1,4,,241371,7900683,0.99 942,Kindergarten,75,1,4,,270680,8853647,0.99 943,Be Aggressive,75,1,4,,222432,7298027,0.99 944,A Small Victory,75,1,4,,297168,9733572,0.99 945,Crack Hitler,75,1,4,,279144,9162435,0.99 946,Jizzlobber,75,1,4,,398341,12926140,0.99 947,Midnight Cowboy,75,1,4,,251924,8242626,0.99 948,Easy,75,1,4,,185835,6073008,0.99 949,Get Out,76,1,1,"Mike Bordin, Billy Gould, Mike Patton",137482,4524972,0.99 950,Ricochet,76,1,1,"Mike Bordin, Billy Gould, Mike Patton",269400,8808812,0.99 951,Evidence,76,1,1,"Mike Bordin, Billy Gould, Mike Patton, Trey Spruance",293590,9626136,0.99 952,The Gentle Art Of Making Enemies,76,1,1,"Mike Bordin, Billy Gould, Mike Patton",209319,6908609,0.99 953,Star A.D.,76,1,1,"Mike Bordin, Billy Gould, Mike Patton",203807,6747658,0.99 954,Cuckoo For Caca,76,1,1,"Mike Bordin, Billy Gould, Mike Patton, Trey Spruance",222902,7388369,0.99 955,Caralho Voador,76,1,1,"Mike Bordin, Billy Gould, Mike Patton, Trey Spruance",242102,8029054,0.99 956,Ugly In The Morning,76,1,1,"Mike Bordin, Billy Gould, Mike Patton",186435,6224997,0.99 957,Digging The Grave,76,1,1,"Mike Bordin, Billy Gould, Mike Patton",185129,6109259,0.99 958,Take This Bottle,76,1,1,"Mike Bordin, Billy Gould, Mike Patton, Trey Spruance",298997,9779971,0.99 959,King For A Day,76,1,1,"Mike Bordin, Billy Gould, Mike Patton, Trey Spruance",395859,13163733,0.99 960,What A Day,76,1,1,"Mike Bordin, Billy Gould, Mike Patton",158275,5203430,0.99 961,The Last To Know,76,1,1,"Mike Bordin, Billy Gould, Mike Patton",267833,8736776,0.99 962,Just A Man,76,1,1,"Mike Bordin, Billy Gould, Mike Patton",336666,11031254,0.99 963,Absolute Zero,76,1,1,"Mike Bordin, Billy Gould, Mike Patton",181995,5929427,0.99 964,From Out Of Nowhere,77,1,4,Faith No More,202527,6587802,0.99 965,Epic,77,1,4,Faith No More,294008,9631296,0.99 966,Falling To Pieces,77,1,4,Faith No More,316055,10333123,0.99 967,Surprise! You're Dead!,77,1,4,Faith No More,147226,4823036,0.99 968,Zombie Eaters,77,1,4,Faith No More,360881,11835367,0.99 969,The Real Thing,77,1,4,Faith No More,493635,16233080,0.99 970,Underwater Love,77,1,4,Faith No More,231993,7634387,0.99 971,The Morning After,77,1,4,Faith No More,223764,7355898,0.99 972,Woodpecker From Mars,77,1,4,Faith No More,340532,11174250,0.99 973,War Pigs,77,1,4,"Tony Iommi, Bill Ward, Geezer Butler, Ozzy Osbourne",464770,15267802,0.99 974,Edge Of The World,77,1,4,Faith No More,250357,8235607,0.99 975,Deixa Entrar,78,1,7,,33619,1095012,0.99 976,Falamansa Song,78,1,7,,237165,7921313,0.99 977,Xote Dos Milagres,78,1,7,,269557,8897778,0.99 978,Rindo À Toa,78,1,7,,222066,7365321,0.99 979,Confidência,78,1,7,,222197,7460829,0.99 980,Forró De Tóquio,78,1,7,,169273,5588756,0.99 981,Zeca Violeiro,78,1,7,,143673,4781949,0.99 982,Avisa,78,1,7,,355030,11844320,0.99 983,Principiando/Decolagem,78,1,7,,116767,3923789,0.99 984,Asas,78,1,7,,231915,7711669,0.99 985,Medo De Escuro,78,1,7,,213760,7056323,0.99 986,Oração,78,1,7,,271072,9003882,0.99 987,Minha Gata,78,1,7,,181838,6039502,0.99 988,Desaforo,78,1,7,,174524,5853561,0.99 989,In Your Honor,79,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett",230191,7468463,0.99 990,No Way Back,79,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett",196675,6421400,0.99 991,Best Of You,79,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett",255712,8363467,0.99 992,DOA,79,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett",252186,8232342,0.99 993,Hell,79,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett",117080,3819255,0.99 994,The Last Song,79,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett",199523,6496742,0.99 995,Free Me,79,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett",278700,9109340,0.99 996,Resolve,79,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett",288731,9416186,0.99 997,The Deepest Blues Are Black,79,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett",238419,7735473,0.99 998,End Over End,79,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett",352078,11395296,0.99 999,Still,80,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett/FOO FIGHTERS",313182,10323157,0.99 1000,What If I Do?,80,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett/FOO FIGHTERS",302994,9929799,0.99 1001,Miracle,80,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett/FOO FIGHTERS",209684,6877994,0.99 1002,Another Round,80,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett/FOO FIGHTERS",265848,8752670,0.99 1003,Friend Of A Friend,80,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett/FOO FIGHTERS",193280,6355088,0.99 1004,Over And Out,80,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett/FOO FIGHTERS",316264,10428382,0.99 1005,On The Mend,80,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett/FOO FIGHTERS",271908,9071997,0.99 1006,Virginia Moon,80,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett/FOO FIGHTERS",229198,7494639,0.99 1007,Cold Day In The Sun,80,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett/FOO FIGHTERS",200724,6596617,0.99 1008,Razor,80,1,1,"Dave Grohl, Taylor Hawkins, Nate Mendel, Chris Shiflett/FOO FIGHTERS",293276,9721373,0.99 1009,All My Life,81,1,4,Foo Fighters,263653,8665545,0.99 1010,Low,81,1,4,Foo Fighters,268120,8847196,0.99 1011,Have It All,81,1,4,Foo Fighters,298057,9729292,0.99 1012,Times Like These,81,1,4,Foo Fighters,266370,8624691,0.99 1013,Disenchanted Lullaby,81,1,4,Foo Fighters,273528,8919111,0.99 1014,Tired Of You,81,1,4,Foo Fighters,311353,10094743,0.99 1015,Halo,81,1,4,Foo Fighters,306442,10026371,0.99 1016,Lonely As You,81,1,4,Foo Fighters,277185,9022628,0.99 1017,Overdrive,81,1,4,Foo Fighters,270550,8793187,0.99 1018,Burn Away,81,1,4,Foo Fighters,298396,9678073,0.99 1019,Come Back,81,1,4,Foo Fighters,469968,15371980,0.99 1020,Doll,82,1,1,"Dave, Taylor, Nate, Chris",83487,2702572,0.99 1021,Monkey Wrench,82,1,1,"Dave, Taylor, Nate, Chris",231523,7527531,0.99 1022,"Hey, Johnny Park!",82,1,1,"Dave, Taylor, Nate, Chris",248528,8079480,0.99 1023,My Poor Brain,82,1,1,"Dave, Taylor, Nate, Chris",213446,6973746,0.99 1024,Wind Up,82,1,1,"Dave, Taylor, Nate, Chris",152163,4950667,0.99 1025,Up In Arms,82,1,1,"Dave, Taylor, Nate, Chris",135732,4406227,0.99 1026,My Hero,82,1,1,"Dave, Taylor, Nate, Chris",260101,8472365,0.99 1027,See You,82,1,1,"Dave, Taylor, Nate, Chris",146782,4888173,0.99 1028,Enough Space,82,1,1,Dave Grohl,157387,5169280,0.99 1029,February Stars,82,1,1,"Dave, Taylor, Nate, Chris",289306,9344875,0.99 1030,Everlong,82,1,1,Dave Grohl,250749,8270816,0.99 1031,Walking After You,82,1,1,Dave Grohl,303856,9898992,0.99 1032,New Way Home,82,1,1,"Dave, Taylor, Nate, Chris",342230,11205664,0.99 1033,My Way,83,1,12,claude françois/gilles thibault/jacques revaux/paul anka,275879,8928684,0.99 1034,Strangers In The Night,83,1,12,berthold kaempfert/charles singleton/eddie snyder,155794,5055295,0.99 1035,"New York, New York",83,1,12,fred ebb/john kander,206001,6707993,0.99 1036,I Get A Kick Out Of You,83,1,12,cole porter,194429,6332441,0.99 1037,Something Stupid,83,1,12,carson c. parks,158615,5210643,0.99 1038,Moon River,83,1,12,henry mancini/johnny mercer,198922,6395808,0.99 1039,What Now My Love,83,1,12,carl sigman/gilbert becaud/pierre leroyer,149995,4913383,0.99 1040,Summer Love,83,1,12,hans bradtke/heinz meier/johnny mercer,174994,5693242,0.99 1041,For Once In My Life,83,1,12,orlando murden/ronald miller,171154,5557537,0.99 1042,Love And Marriage,83,1,12,jimmy van heusen/sammy cahn,89730,2930596,0.99 1043,They Can't Take That Away From Me,83,1,12,george gershwin/ira gershwin,161227,5240043,0.99 1044,My Kind Of Town,83,1,12,jimmy van heusen/sammy cahn,188499,6119915,0.99 1045,Fly Me To The Moon,83,1,12,bart howard,149263,4856954,0.99 1046,I've Got You Under My Skin,83,1,12,cole porter,210808,6883787,0.99 1047,The Best Is Yet To Come,83,1,12,carolyn leigh/cy coleman,173583,5633730,0.99 1048,It Was A Very Good Year,83,1,12,ervin drake,266605,8554066,0.99 1049,Come Fly With Me,83,1,12,jimmy van heusen/sammy cahn,190458,6231029,0.99 1050,That's Life,83,1,12,dean kay thompson/kelly gordon,187010,6095727,0.99 1051,The Girl From Ipanema,83,1,12,antonio carlos jobim/norman gimbel/vinicius de moraes,193750,6410674,0.99 1052,The Lady Is A Tramp,83,1,12,lorenz hart/richard rodgers,184111,5987372,0.99 1053,"Bad, Bad Leroy Brown",83,1,12,jim croce,169900,5548581,0.99 1054,Mack The Knife,83,1,12,bert brecht/kurt weill/marc blitzstein,292075,9541052,0.99 1055,Loves Been Good To Me,83,1,12,rod mckuen,203964,6645365,0.99 1056,L.A. Is My Lady,83,1,12,alan bergman/marilyn bergman/peggy lipton jones/quincy jones,193175,6378511,0.99 1057,Entrando Na Sua (Intro),84,1,7,,179252,5840027,0.99 1058,Nervosa,84,1,7,,229537,7680421,0.99 1059,Funk De Bamba (Com Fernanda Abreu),84,1,7,,237191,7866165,0.99 1060,Call Me At Cleo´s,84,1,7,,236617,7920510,0.99 1061,Olhos Coloridos (Com Sandra De Sá),84,1,7,,321332,10567404,0.99 1062,Zambação,84,1,7,,301113,10030604,0.99 1063,Funk Hum,84,1,7,,244453,8084475,0.99 1064,Forty Days (Com DJ Hum),84,1,7,,221727,7347172,0.99 1065,Balada Da Paula,84,1,7,Emerson Villani,322821,10603717,0.99 1066,Dujji,84,1,7,,324597,10833935,0.99 1067,Meu Guarda-Chuva,84,1,7,,248528,8216625,0.99 1068,Motéis,84,1,7,,213498,7041077,0.99 1069,Whistle Stop,84,1,7,,526132,17533664,0.99 1070,16 Toneladas,84,1,7,,191634,6390885,0.99 1071,Divirta-Se (Saindo Da Sua),84,1,7,,74919,2439206,0.99 1072,Forty Days Instrumental,84,1,7,,292493,9584317,0.99 1073,Óia Eu Aqui De Novo,85,1,10,,219454,7469735,0.99 1074,Baião Da Penha,85,1,10,,247928,8393047,0.99 1075,Esperando Na Janela,85,1,10,Manuca/Raimundinho DoAcordion/Targino Godim,261041,8660617,0.99 1076,Juazeiro,85,1,10,Humberto Teixeira/Luiz Gonzaga,222275,7349779,0.99 1077,Último Pau-De-Arara,85,1,10,Corumbá/José Gumarães/Venancio,200437,6638563,0.99 1078,Asa Branca,85,1,10,Humberto Teixeira/Luiz Gonzaga,217051,7387183,0.99 1079,Qui Nem Jiló,85,1,10,Humberto Teixeira/Luiz Gonzaga,204695,6937472,0.99 1080,Assum Preto,85,1,10,Humberto Teixeira/Luiz Gonzaga,199653,6625000,0.99 1081,Pau-De-Arara,85,1,10,"Guio De Morais E Seus ""Parentes""/Luiz Gonzaga",191660,6340649,0.99 1082,A Volta Da Asa Branca,85,1,10,Luiz Gonzaga/Zé Dantas,271020,9098093,0.99 1083,O Amor Daqui De Casa,85,1,10,Gilberto Gil,148636,4888292,0.99 1084,As Pegadas Do Amor,85,1,10,Gilberto Gil,209136,6899062,0.99 1085,Lamento Sertanejo,85,1,10,Dominguinhos/Gilberto Gil,260963,8518290,0.99 1086,Casinha Feliz,85,1,10,Gilberto Gil,32287,1039615,0.99 1087,Introdução (Live),86,1,7,,154096,5227579,0.99 1088,Palco (Live),86,1,7,,238315,8026622,0.99 1089,Is This Love (Live),86,1,7,,295262,9819759,0.99 1090,Stir It Up (Live),86,1,7,,282409,9594738,0.99 1091,Refavela (Live),86,1,7,,236695,7985305,0.99 1092,Vendedor De Caranguejo (Live),86,1,7,,248842,8358128,0.99 1093,Quanta (Live),86,1,7,,357485,11774865,0.99 1094,Estrela (Live),86,1,7,,285309,9436411,0.99 1095,Pela Internet (Live),86,1,7,,263471,8804401,0.99 1096,Cérebro Eletrônico (Live),86,1,7,,231627,7805352,0.99 1097,Opachorô (Live),86,1,7,,259526,8596384,0.99 1098,Copacabana (Live),86,1,7,,289671,9673672,0.99 1099,A Novidade (Live),86,1,7,,316969,10508000,0.99 1100,Ghandi (Live),86,1,7,,222458,7481950,0.99 1101,De Ouro E Marfim (Live),86,1,7,,234971,7838453,0.99 1102,Doce De Carnaval (Candy All),87,1,2,,356101,11998470,0.99 1103,Lamento De Carnaval,87,1,2,,294530,9819276,0.99 1104,Pretinha,87,1,2,,265273,8914579,0.99 1105,A Novidade,73,1,7,Gilberto Gil,324780,10765600,0.99 1106,Tenho Sede,73,1,7,Gilberto Gil,261616,8708114,0.99 1107,Refazenda,73,1,7,Gilberto Gil,218305,7237784,0.99 1108,Realce,73,1,7,Gilberto Gil,264489,8847612,0.99 1109,Esotérico,73,1,7,Gilberto Gil,317779,10530533,0.99 1110,Drão,73,1,7,Gilberto Gil,301453,9931950,0.99 1111,A Paz,73,1,7,Gilberto Gil,293093,9593064,0.99 1112,Beira Mar,73,1,7,Gilberto Gil,295444,9597994,0.99 1113,Sampa,73,1,7,Gilberto Gil,225697,7469905,0.99 1114,Parabolicamará,73,1,7,Gilberto Gil,284943,9543435,0.99 1115,Tempo Rei,73,1,7,Gilberto Gil,302733,10019269,0.99 1116,Expresso 2222,73,1,7,Gilberto Gil,284760,9690577,0.99 1117,Aquele Abraço,73,1,7,Gilberto Gil,263993,8805003,0.99 1118,Palco,73,1,7,Gilberto Gil,270550,9049901,0.99 1119,Toda Menina Baiana,73,1,7,Gilberto Gil,278177,9351000,0.99 1120,Sítio Do Pica-Pau Amarelo,73,1,7,Gilberto Gil,218070,7217955,0.99 1121,Straight Out Of Line,88,1,3,Sully Erna,259213,8511877,0.99 1122,Faceless,88,1,3,Sully Erna,216006,6992417,0.99 1123,Changes,88,1,3,Sully Erna; Tony Rombola,260022,8455835,0.99 1124,Make Me Believe,88,1,3,Sully Erna,248607,8075050,0.99 1125,I Stand Alone,88,1,3,Sully Erna,246125,8017041,0.99 1126,Re-Align,88,1,3,Sully Erna,260884,8513891,0.99 1127,I Fucking Hate You,88,1,3,Sully Erna,247170,8059642,0.99 1128,Releasing The Demons,88,1,3,Sully Erna,252760,8276372,0.99 1129,Dead And Broken,88,1,3,Sully Erna,251454,8206611,0.99 1130,I Am,88,1,3,Sully Erna,239516,7803270,0.99 1131,The Awakening,88,1,3,Sully Erna,89547,3035251,0.99 1132,Serenity,88,1,3,Sully Erna; Tony Rombola,274834,9172976,0.99 1133,American Idiot,89,1,4,"Billie Joe Armstrong, Mike Dirnt, Tré Cool",174419,5705793,0.99 1134,Jesus Of Suburbia / City Of The Damned / I Don't Care / Dearly Beloved / Tales Of Another Broken Home,89,1,4,Billie Joe Armstrong/Green Day,548336,17875209,0.99 1135,Holiday,89,1,4,"Billie Joe Armstrong, Mike Dirnt, Tré Cool",232724,7599602,0.99 1136,Boulevard Of Broken Dreams,89,1,4,"Mike Dint, Billie Joe, Tré Cool",260858,8485122,0.99 1137,Are We The Waiting,89,1,4,Green Day,163004,5328329,0.99 1138,St. Jimmy,89,1,4,Green Day,175307,5716589,0.99 1139,Give Me Novacaine,89,1,4,Green Day,205871,6752485,0.99 1140,She's A Rebel,89,1,4,Green Day,120528,3901226,0.99 1141,Extraordinary Girl,89,1,4,Green Day,214021,6975177,0.99 1142,Letterbomb,89,1,4,Green Day,246151,7980902,0.99 1143,Wake Me Up When September Ends,89,1,4,"Mike Dint, Billie Joe, Tré Cool",285753,9325597,0.99 1144,Homecoming / The Death Of St. Jimmy / East 12th St. / Nobody Likes You / Rock And Roll Girlfriend / We're Coming Home Again,89,1,4,Mike Dirnt/Tré Cool,558602,18139840,0.99 1145,Whatsername,89,1,4,Green Day,252316,8244843,0.99 1146,Welcome to the Jungle,90,2,1,,273552,4538451,0.99 1147,It's So Easy,90,2,1,,202824,3394019,0.99 1148,Nightrain,90,2,1,,268537,4457283,0.99 1149,Out Ta Get Me,90,2,1,,263893,4382147,0.99 1150,Mr. Brownstone,90,2,1,,228924,3816323,0.99 1151,Paradise City,90,2,1,,406347,6687123,0.99 1152,My Michelle,90,2,1,,219961,3671299,0.99 1153,Think About You,90,2,1,,231640,3860275,0.99 1154,Sweet Child O' Mine,90,2,1,,356424,5879347,0.99 1155,You're Crazy,90,2,1,,197135,3301971,0.99 1156,Anything Goes,90,2,1,,206400,3451891,0.99 1157,Rocket Queen,90,2,1,,375349,6185539,0.99 1158,Right Next Door to Hell,91,2,1,,182321,3175950,0.99 1159,Dust N' Bones,91,2,1,,298374,5053742,0.99 1160,Live and Let Die,91,2,1,,184016,3203390,0.99 1161,Don't Cry (Original),91,2,1,,284744,4833259,0.99 1162,Perfect Crime,91,2,1,,143637,2550030,0.99 1163,You Ain't the First,91,2,1,,156268,2754414,0.99 1164,Bad Obsession,91,2,1,,328282,5537678,0.99 1165,Back off Bitch,91,2,1,,303436,5135662,0.99 1166,Double Talkin' Jive,91,2,1,,203637,3520862,0.99 1167,November Rain,91,2,1,,537540,8923566,0.99 1168,The Garden,91,2,1,,322175,5438862,0.99 1169,Garden of Eden,91,2,1,,161539,2839694,0.99 1170,Don't Damn Me,91,2,1,,318901,5385886,0.99 1171,Bad Apples,91,2,1,,268351,4567966,0.99 1172,Dead Horse,91,2,1,,257600,4394014,0.99 1173,Coma,91,2,1,,616511,10201342,0.99 1174,Civil War,92,1,3,Duff McKagan/Slash/W. Axl Rose,461165,15046579,0.99 1175,14 Years,92,1,3,Izzy Stradlin'/W. Axl Rose,261355,8543664,0.99 1176,Yesterdays,92,1,3,Billy/Del James/W. Axl Rose/West Arkeen,196205,6398489,0.99 1177,Knockin' On Heaven's Door,92,1,3,Bob Dylan,336457,10986716,0.99 1178,Get In The Ring,92,1,3,Duff McKagan/Slash/W. Axl Rose,341054,11134105,0.99 1179,Shotgun Blues,92,1,3,W. Axl Rose,203206,6623916,0.99 1180,Breakdown,92,1,3,W. Axl Rose,424960,13978284,0.99 1181,Pretty Tied Up,92,1,3,Izzy Stradlin',287477,9408754,0.99 1182,Locomotive,92,1,3,Slash/W. Axl Rose,522396,17236842,0.99 1183,So Fine,92,1,3,Duff McKagan,246491,8039484,0.99 1184,Estranged,92,1,3,W. Axl Rose,563800,18343787,0.99 1185,You Could Be Mine,92,1,3,Izzy Stradlin'/W. Axl Rose,343875,11207355,0.99 1186,Don't Cry,92,1,3,Izzy Stradlin'/W. Axl Rose,284238,9222458,0.99 1187,My World,92,1,3,W. Axl Rose,84532,2764045,0.99 1188,Colibri,93,1,2,Richard Bull,361012,12055329,0.99 1189,Love Is The Colour,93,1,2,R. Carless,251585,8419165,0.99 1190,Magnetic Ocean,93,1,2,Patrick Claher/Richard Bull,321123,10720741,0.99 1191,Deep Waters,93,1,2,Richard Bull,396460,13075359,0.99 1192,L'Arc En Ciel De Miles,93,1,2,Kevin Robinson/Richard Bull,242390,8053997,0.99 1193,Gypsy,93,1,2,Kevin Robinson,330997,11083374,0.99 1194,Journey Into Sunlight,93,1,2,Jean Paul Maunick,249756,8241177,0.99 1195,Sunchild,93,1,2,Graham Harvey,259970,8593143,0.99 1196,Millenium,93,1,2,Maxton Gig Beesley Jnr.,379167,12511939,0.99 1197,Thinking 'Bout Tomorrow,93,1,2,Fayyaz Virgi/Richard Bull,355395,11865384,0.99 1198,Jacob's Ladder,93,1,2,Julian Crampton,367647,12201595,0.99 1199,She Wears Black,93,1,2,G Harvey/R Hope-Taylor,528666,17617944,0.99 1200,Dark Side Of The Cog,93,1,2,Jean Paul Maunick,377155,12491122,0.99 1201,Different World,94,2,1,,258692,4383764,0.99 1202,These Colours Don't Run,94,2,1,,412152,6883500,0.99 1203,Brighter Than a Thousand Suns,94,2,1,,526255,8721490,0.99 1204,The Pilgrim,94,2,1,,307593,5172144,0.99 1205,The Longest Day,94,2,1,,467810,7785748,0.99 1206,Out of the Shadows,94,2,1,,336896,5647303,0.99 1207,The Reincarnation of Benjamin Breeg,94,2,1,,442106,7367736,0.99 1208,For the Greater Good of God,94,2,1,,564893,9367328,0.99 1209,Lord of Light,94,2,1,,444614,7393698,0.99 1210,The Legacy,94,2,1,,562966,9314287,0.99 1211,Hallowed Be Thy Name (Live) [Non Album Bonus Track],94,2,1,,431262,7205816,0.99 1212,The Number Of The Beast,95,1,3,Steve Harris,294635,4718897,0.99 1213,The Trooper,95,1,3,Steve Harris,235311,3766272,0.99 1214,Prowler,95,1,3,Steve Harris,255634,4091904,0.99 1215,Transylvania,95,1,3,Steve Harris,265874,4255744,0.99 1216,Remember Tomorrow,95,1,3,Paul Di'Anno/Steve Harris,352731,5648438,0.99 1217,Where Eagles Dare,95,1,3,Steve Harris,289358,4630528,0.99 1218,Sanctuary,95,1,3,David Murray/Paul Di'Anno/Steve Harris,293250,4694016,0.99 1219,Running Free,95,1,3,Paul Di'Anno/Steve Harris,228937,3663872,0.99 1220,Run To The Hilss,95,1,3,Steve Harris,237557,3803136,0.99 1221,2 Minutes To Midnight,95,1,3,Adrian Smith/Bruce Dickinson,337423,5400576,0.99 1222,Iron Maiden,95,1,3,Steve Harris,324623,5195776,0.99 1223,Hallowed Be Thy Name,95,1,3,Steve Harris,471849,7550976,0.99 1224,Be Quick Or Be Dead,96,1,3,Bruce Dickinson/Janick Gers,196911,3151872,0.99 1225,From Here To Eternity,96,1,3,Steve Harris,259866,4159488,0.99 1226,Can I Play With Madness,96,1,3,Adrian Smith/Bruce Dickinson/Steve Harris,282488,4521984,0.99 1227,Wasting Love,96,1,3,Bruce Dickinson/Janick Gers,347846,5566464,0.99 1228,Tailgunner,96,1,3,Bruce Dickinson/Steve Harris,249469,3993600,0.99 1229,The Evil That Men Do,96,1,3,Adrian Smith/Bruce Dickinson/Steve Harris,325929,5216256,0.99 1230,Afraid To Shoot Strangers,96,1,3,Steve Harris,407980,6529024,0.99 1231,Bring Your Daughter... To The Slaughter,96,1,3,Bruce Dickinson,317727,5085184,0.99 1232,Heaven Can Wait,96,1,3,Steve Harris,448574,7178240,0.99 1233,The Clairvoyant,96,1,3,Steve Harris,269871,4319232,0.99 1234,Fear Of The Dark,96,1,3,Steve Harris,431333,6906078,0.99 1235,The Wicker Man,97,1,1,Adrian Smith/Bruce Dickinson/Steve Harris,275539,11022464,0.99 1236,Ghost Of The Navigator,97,1,1,Bruce Dickinson/Janick Gers/Steve Harris,410070,16404608,0.99 1237,Brave New World,97,1,1,Bruce Dickinson/David Murray/Steve Harris,378984,15161472,0.99 1238,Blood Brothers,97,1,1,Steve Harris,434442,17379456,0.99 1239,The Mercenary,97,1,1,Janick Gers/Steve Harris,282488,11300992,0.99 1240,Dream Of Mirrors,97,1,1,Janick Gers/Steve Harris,561162,22448256,0.99 1241,The Fallen Angel,97,1,1,Adrian Smith/Steve Harris,240718,9629824,0.99 1242,The Nomad,97,1,1,David Murray/Steve Harris,546115,21846144,0.99 1243,Out Of The Silent Planet,97,1,1,Bruce Dickinson/Janick Gers/Steve Harris,385541,15423616,0.99 1244,The Thin Line Between Love & Hate,97,1,1,David Murray/Steve Harris,506801,20273280,0.99 1245,Wildest Dreams,98,1,13,Adrian Smith/Steve Harris,232777,9312384,0.99 1246,Rainmaker,98,1,13,Bruce Dickinson/David Murray/Steve Harris,228623,9146496,0.99 1247,No More Lies,98,1,13,Steve Harris,441782,17672320,0.99 1248,Montsegur,98,1,13,Bruce Dickinson/Janick Gers/Steve Harris,350484,14020736,0.99 1249,Dance Of Death,98,1,13,Janick Gers/Steve Harris,516649,20670727,0.99 1250,Gates Of Tomorrow,98,1,13,Bruce Dickinson/Janick Gers/Steve Harris,312032,12482688,0.99 1251,New Frontier,98,1,13,Adrian Smith/Bruce Dickinson/Nicko McBrain,304509,12181632,0.99 1252,Paschendale,98,1,13,Adrian Smith/Steve Harris,508107,20326528,0.99 1253,Face In The Sand,98,1,13,Adrian Smith/Bruce Dickinson/Steve Harris,391105,15648948,0.99 1254,Age Of Innocence,98,1,13,David Murray/Steve Harris,370468,14823478,0.99 1255,Journeyman,98,1,13,Bruce Dickinson/David Murray/Steve Harris,427023,17082496,0.99 1256,Be Quick Or Be Dead,99,1,1,Bruce Dickinson/Janick Gers,204512,8181888,0.99 1257,From Here To Eternity,99,1,1,Steve Harris,218357,8739038,0.99 1258,Afraid To Shoot Strangers,99,1,1,Steve Harris,416496,16664589,0.99 1259,Fear Is The Key,99,1,1,Bruce Dickinson/Janick Gers,335307,13414528,0.99 1260,Childhood's End,99,1,1,Steve Harris,280607,11225216,0.99 1261,Wasting Love,99,1,1,Bruce Dickinson/Janick Gers,350981,14041216,0.99 1262,The Fugitive,99,1,1,Steve Harris,294112,11765888,0.99 1263,Chains Of Misery,99,1,1,Bruce Dickinson/David Murray,217443,8700032,0.99 1264,The Apparition,99,1,1,Janick Gers/Steve Harris,234605,9386112,0.99 1265,Judas Be My Guide,99,1,1,Bruce Dickinson/David Murray,188786,7553152,0.99 1266,Weekend Warrior,99,1,1,Janick Gers/Steve Harris,339748,13594678,0.99 1267,Fear Of The Dark,99,1,1,Steve Harris,436976,17483789,0.99 1268,01 - Prowler,100,1,6,Steve Harris,236173,5668992,0.99 1269,02 - Sanctuary,100,1,6,David Murray/Paul Di'Anno/Steve Harris,196284,4712576,0.99 1270,03 - Remember Tomorrow,100,1,6,Harris/Paul Di´Anno,328620,7889024,0.99 1271,04 - Running Free,100,1,6,Harris/Paul Di´Anno,197276,4739122,0.99 1272,05 - Phantom of the Opera,100,1,6,Steve Harris,428016,10276872,0.99 1273,06 - Transylvania,100,1,6,Steve Harris,259343,6226048,0.99 1274,07 - Strange World,100,1,6,Steve Harris,332460,7981184,0.99 1275,08 - Charlotte the Harlot,100,1,6,Murray Dave,252708,6066304,0.99 1276,09 - Iron Maiden,100,1,6,Steve Harris,216058,5189891,0.99 1277,The Ides Of March,101,1,13,Steve Harris,105926,2543744,0.99 1278,Wrathchild,101,1,13,Steve Harris,174471,4188288,0.99 1279,Murders In The Rue Morgue,101,1,13,Steve Harris,258377,6205786,0.99 1280,Another Life,101,1,13,Steve Harris,203049,4874368,0.99 1281,Genghis Khan,101,1,13,Steve Harris,187141,4493440,0.99 1282,Innocent Exile,101,1,13,Di´Anno/Harris,232515,5584861,0.99 1283,Killers,101,1,13,Steve Harris,300956,7227440,0.99 1284,Prodigal Son,101,1,13,Steve Harris,372349,8937600,0.99 1285,Purgatory,101,1,13,Steve Harris,200150,4804736,0.99 1286,Drifter,101,1,13,Steve Harris,288757,6934660,0.99 1287,Intro- Churchill S Speech,102,1,13,,48013,1154488,0.99 1288,Aces High,102,1,13,,276375,6635187,0.99 1289,2 Minutes To Midnight,102,1,3,Smith/Dickinson,366550,8799380,0.99 1290,The Trooper,102,1,3,Harris,268878,6455255,0.99 1291,Revelations,102,1,3,Dickinson,371826,8926021,0.99 1292,Flight Of Icarus,102,1,3,Smith/Dickinson,229982,5521744,0.99 1293,Rime Of The Ancient Mariner,102,1,3,,789472,18949518,0.99 1294,Powerslave,102,1,3,,454974,10921567,0.99 1295,The Number Of The Beast,102,1,3,Harris,275121,6605094,0.99 1296,Hallowed Be Thy Name,102,1,3,Harris,451422,10836304,0.99 1297,Iron Maiden,102,1,3,Harris,261955,6289117,0.99 1298,Run To The Hills,102,1,3,Harris,231627,5561241,0.99 1299,Running Free,102,1,3,Harris/Di Anno,204617,4912986,0.99 1300,Wrathchild,102,1,13,Steve Harris,183666,4410181,0.99 1301,Acacia Avenue,102,1,13,,379872,9119118,0.99 1302,Children Of The Damned,102,1,13,Steve Harris,278177,6678446,0.99 1303,Die With Your Boots On,102,1,13,Adrian Smith/Bruce Dickinson/Steve Harris,314174,7542367,0.99 1304,Phantom Of The Opera,102,1,13,Steve Harris,441155,10589917,0.99 1305,Be Quick Or Be Dead,103,1,1,,233142,5599853,0.99 1306,The Number Of The Beast,103,1,1,,294008,7060625,0.99 1307,Wrathchild,103,1,1,,174106,4182963,0.99 1308,From Here To Eternity,103,1,1,,284447,6831163,0.99 1309,Can I Play With Madness,103,1,1,,213106,5118995,0.99 1310,Wasting Love,103,1,1,,336953,8091301,0.99 1311,Tailgunner,103,1,1,,247640,5947795,0.99 1312,The Evil That Men Do,103,1,1,,478145,11479913,0.99 1313,Afraid To Shoot Strangers,103,1,1,,412525,9905048,0.99 1314,Fear Of The Dark,103,1,1,,431542,10361452,0.99 1315,Bring Your Daughter... To The Slaughter...,104,1,1,,376711,9045532,0.99 1316,The Clairvoyant,104,1,1,,262426,6302648,0.99 1317,Heaven Can Wait,104,1,1,,440555,10577743,0.99 1318,Run To The Hills,104,1,1,,235859,5665052,0.99 1319,2 Minutes To Midnight,104,1,1,Adrian Smith/Bruce Dickinson,338233,8122030,0.99 1320,Iron Maiden,104,1,1,,494602,11874875,0.99 1321,Hallowed Be Thy Name,104,1,1,,447791,10751410,0.99 1322,The Trooper,104,1,1,,232672,5588560,0.99 1323,Sanctuary,104,1,1,,318511,7648679,0.99 1324,Running Free,104,1,1,,474017,11380851,0.99 1325,Tailgunner,105,1,3,Bruce Dickinson/Steve Harris,255582,4089856,0.99 1326,Holy Smoke,105,1,3,Bruce Dickinson/Steve Harris,229459,3672064,0.99 1327,No Prayer For The Dying,105,1,3,Steve Harris,263941,4225024,0.99 1328,Public Enema Number One,105,1,3,Bruce Dickinson/David Murray,254197,4071587,0.99 1329,Fates Warning,105,1,3,David Murray/Steve Harris,250853,4018088,0.99 1330,The Assassin,105,1,3,Steve Harris,258768,4141056,0.99 1331,Run Silent Run Deep,105,1,3,Bruce Dickinson/Steve Harris,275408,4407296,0.99 1332,Hooks In You,105,1,3,Adrian Smith/Bruce Dickinson,247510,3960832,0.99 1333,Bring Your Daughter... ...To The Slaughter,105,1,3,Bruce Dickinson,284238,4548608,0.99 1334,Mother Russia,105,1,3,Steve Harris,332617,5322752,0.99 1335,Where Eagles Dare,106,1,3,Steve Harris,369554,5914624,0.99 1336,Revelations,106,1,3,Bruce Dickinson,408607,6539264,0.99 1337,Flight Of The Icarus,106,1,3,Adrian Smith/Bruce Dickinson,230269,3686400,0.99 1338,Die With Your Boots On,106,1,3,Adrian Smith/Bruce Dickinson/Steve Harris,325694,5212160,0.99 1339,The Trooper,106,1,3,Steve Harris,251454,4024320,0.99 1340,Still Life,106,1,3,David Murray/Steve Harris,294347,4710400,0.99 1341,Quest For Fire,106,1,3,Steve Harris,221309,3543040,0.99 1342,Sun And Steel,106,1,3,Adrian Smith/Bruce Dickinson,206367,3306324,0.99 1343,To Tame A Land,106,1,3,Steve Harris,445283,7129264,0.99 1344,Aces High,107,1,3,Harris,269531,6472088,0.99 1345,2 Minutes To Midnight,107,1,3,Smith/Dickinson,359810,8638809,0.99 1346,Losfer Words,107,1,3,Steve Harris,252891,6074756,0.99 1347,Flash of The Blade,107,1,3,Dickinson,242729,5828861,0.99 1348,Duelists,107,1,3,Steve Harris,366471,8800686,0.99 1349,Back in the Village,107,1,3,Dickinson/Smith,320548,7696518,0.99 1350,Powerslave,107,1,3,Dickinson,407823,9791106,0.99 1351,Rime of the Ancient Mariner,107,1,3,Harris,816509,19599577,0.99 1352,Intro,108,1,3,,115931,4638848,0.99 1353,The Wicker Man,108,1,3,Adrian Smith/Bruce Dickinson/Steve Harris,281782,11272320,0.99 1354,Ghost Of The Navigator,108,1,3,Bruce Dickinson/Janick Gers/Steve Harris,408607,16345216,0.99 1355,Brave New World,108,1,3,Bruce Dickinson/David Murray/Steve Harris,366785,14676148,0.99 1356,Wrathchild,108,1,3,Steve Harris,185808,7434368,0.99 1357,2 Minutes To Midnight,108,1,3,Adrian Smith/Bruce Dickinson,386821,15474816,0.99 1358,Blood Brothers,108,1,3,Steve Harris,435513,17422464,0.99 1359,Sign Of The Cross,108,1,3,Steve Harris,649116,25966720,0.99 1360,The Mercenary,108,1,3,Janick Gers/Steve Harris,282697,11309184,0.99 1361,The Trooper,108,1,3,Steve Harris,273528,10942592,0.99 1362,Dream Of Mirrors,109,1,1,Janick Gers/Steve Harris,578324,23134336,0.99 1363,The Clansman,109,1,1,Steve Harris,559203,22370432,0.99 1364,The Evil That Men Do,109,1,3,Adrian Smith/Bruce Dickinson/Steve Harris,280737,11231360,0.99 1365,Fear Of The Dark,109,1,1,Steve Harris,460695,18430080,0.99 1366,Iron Maiden,109,1,1,Steve Harris,351869,14076032,0.99 1367,The Number Of The Beast,109,1,1,Steve Harris,300434,12022107,0.99 1368,Hallowed Be Thy Name,109,1,1,Steve Harris,443977,17760384,0.99 1369,Sanctuary,109,1,1,David Murray/Paul Di'Anno/Steve Harris,317335,12695680,0.99 1370,Run To The Hills,109,1,1,Steve Harris,292179,11688064,0.99 1371,Moonchild,110,1,3,Adrian Smith; Bruce Dickinson,340767,8179151,0.99 1372,Infinite Dreams,110,1,3,Steve Harris,369005,8858669,0.99 1373,Can I Play With Madness,110,1,3,Adrian Smith; Bruce Dickinson; Steve Harris,211043,5067867,0.99 1374,The Evil That Men Do,110,1,3,Adrian Smith; Bruce Dickinson; Steve Harris,273998,6578930,0.99 1375,Seventh Son of a Seventh Son,110,1,3,Steve Harris,593580,14249000,0.99 1376,The Prophecy,110,1,3,Dave Murray; Steve Harris,305475,7334450,0.99 1377,The Clairvoyant,110,1,3,Adrian Smith; Bruce Dickinson; Steve Harris,267023,6411510,0.99 1378,Only the Good Die Young,110,1,3,Bruce Dickinson; Harris,280894,6744431,0.99 1379,Caught Somewhere in Time,111,1,3,Steve Harris,445779,10701149,0.99 1380,Wasted Years,111,1,3,Adrian Smith,307565,7384358,0.99 1381,Sea of Madness,111,1,3,Adrian Smith,341995,8210695,0.99 1382,Heaven Can Wait,111,1,3,Steve Harris,441417,10596431,0.99 1383,Stranger in a Strange Land,111,1,3,Adrian Smith,344502,8270899,0.99 1384,Alexander the Great,111,1,3,Steve Harris,515631,12377742,0.99 1385,De Ja Vu,111,1,3,David Murray/Steve Harris,296176,7113035,0.99 1386,The Loneliness of the Long Dis,111,1,3,Steve Harris,391314,9393598,0.99 1387,22 Acacia Avenue,112,1,3,Adrian Smith/Steve Harris,395572,5542516,0.99 1388,Children of the Damned,112,1,3,Steve Harris,274364,3845631,0.99 1389,Gangland,112,1,3,Adrian Smith/Clive Burr/Steve Harris,228440,3202866,0.99 1390,Hallowed Be Thy Name,112,1,3,Steve Harris,428669,6006107,0.99 1391,Invaders,112,1,3,Steve Harris,203180,2849181,0.99 1392,Run to the Hills,112,1,3,Steve Harris,228884,3209124,0.99 1393,The Number Of The Beast,112,1,1,Steve Harris,293407,11737216,0.99 1394,The Prisoner,112,1,3,Adrian Smith/Steve Harris,361299,5062906,0.99 1395,Sign Of The Cross,113,1,1,Steve Harris,678008,27121792,0.99 1396,Lord Of The Flies,113,1,1,Janick Gers/Steve Harris,303699,12148864,0.99 1397,Man On The Edge,113,1,1,Blaze Bayley/Janick Gers,253413,10137728,0.99 1398,Fortunes Of War,113,1,1,Steve Harris,443977,17760384,0.99 1399,Look For The Truth,113,1,1,Blaze Bayley/Janick Gers/Steve Harris,310230,12411008,0.99 1400,The Aftermath,113,1,1,Blaze Bayley/Janick Gers/Steve Harris,380786,15233152,0.99 1401,Judgement Of Heaven,113,1,1,Steve Harris,312476,12501120,0.99 1402,Blood On The World's Hands,113,1,1,Steve Harris,357799,14313600,0.99 1403,The Edge Of Darkness,113,1,1,Blaze Bayley/Janick Gers/Steve Harris,399333,15974528,0.99 1404,2 A.M.,113,1,1,Blaze Bayley/Janick Gers/Steve Harris,337658,13511087,0.99 1405,The Unbeliever,113,1,1,Janick Gers/Steve Harris,490422,19617920,0.99 1406,Futureal,114,1,1,Blaze Bayley/Steve Harris,175777,7032960,0.99 1407,The Angel And The Gambler,114,1,1,Steve Harris,592744,23711872,0.99 1408,Lightning Strikes Twice,114,1,1,David Murray/Steve Harris,290377,11616384,0.99 1409,The Clansman,114,1,1,Steve Harris,539689,21592327,0.99 1410,When Two Worlds Collide,114,1,1,Blaze Bayley/David Murray/Steve Harris,377312,15093888,0.99 1411,The Educated Fool,114,1,1,Steve Harris,404767,16191616,0.99 1412,Don't Look To The Eyes Of A Stranger,114,1,1,Steve Harris,483657,19347584,0.99 1413,Como Estais Amigos,114,1,1,Blaze Bayley/Janick Gers,330292,13213824,0.99 1414,Please Please Please,115,1,14,James Brown/Johnny Terry,165067,5394585,0.99 1415,Think,115,1,14,Lowman Pauling,166739,5513208,0.99 1416,Night Train,115,1,14,Jimmy Forrest/Lewis C. Simpkins/Oscar Washington,212401,7027377,0.99 1417,Out Of Sight,115,1,14,Ted Wright,143725,4711323,0.99 1418,Papa's Got A Brand New Bag Pt.1,115,1,14,James Brown,127399,4174420,0.99 1419,I Got You (I Feel Good),115,1,14,James Brown,167392,5468472,0.99 1420,It's A Man's Man's Man's World,115,1,14,Betty Newsome/James Brown,168228,5541611,0.99 1421,Cold Sweat,115,1,14,Alfred Ellis/James Brown,172408,5643213,0.99 1422,"Say It Loud, I'm Black And I'm Proud Pt.1",115,1,14,Alfred Ellis/James Brown,167392,5478117,0.99 1423,Get Up (I Feel Like Being A) Sex Machine,115,1,14,Bobby Byrd/James Brown/Ron Lenhoff,316551,10498031,0.99 1424,Hey America,115,1,14,Addie William Jones/Nat Jones,218226,7187857,0.99 1425,Make It Funky Pt.1,115,1,14,Charles Bobbitt/James Brown,196231,6507782,0.99 1426,I'm A Greedy Man Pt.1,115,1,14,Charles Bobbitt/James Brown,217730,7251211,0.99 1427,Get On The Good Foot,115,1,14,Fred Wesley/James Brown/Joseph Mims,215902,7182736,0.99 1428,Get Up Offa That Thing,115,1,14,Deanna Brown/Deidra Jenkins/Yamma Brown,250723,8355989,0.99 1429,It's Too Funky In Here,115,1,14,Brad Shapiro/George Jackson/Robert Miller/Walter Shaw,239072,7973979,0.99 1430,Living In America,115,1,14,Charlie Midnight/Dan Hartman,282880,9432346,0.99 1431,I'm Real,115,1,14,Full Force/James Brown,334236,11183457,0.99 1432,Hot Pants Pt.1,115,1,14,Fred Wesley/James Brown,188212,6295110,0.99 1433,Soul Power (Live),115,1,14,James Brown,260728,8593206,0.99 1434,When You Gonna Learn (Digeridoo),116,1,1,"Jay Kay/Kay, Jay",230635,7655482,0.99 1435,Too Young To Die,116,1,1,"Smith, Toby",365818,12391660,0.99 1436,Hooked Up,116,1,1,"Smith, Toby",275879,9301687,0.99 1437,"If I Like It, I Do It",116,1,1,"Gelder, Nick van",293093,9848207,0.99 1438,Music Of The Wind,116,1,1,"Smith, Toby",383033,12870239,0.99 1439,Emergency On Planet Earth,116,1,1,"Smith, Toby",245263,8117218,0.99 1440,"Whatever It Is, I Just Can't Stop",116,1,1,"Jay Kay/Kay, Jay",247222,8249453,0.99 1441,Blow Your Mind,116,1,1,"Smith, Toby",512339,17089176,0.99 1442,Revolution 1993,116,1,1,"Smith, Toby",616829,20816872,0.99 1443,Didgin' Out,116,1,1,"Buchanan, Wallis",157100,5263555,0.99 1444,Canned Heat,117,1,14,Jay Kay,331964,11042037,0.99 1445,Planet Home,117,1,14,Jay Kay/Toby Smith,284447,9566237,0.99 1446,Black Capricorn Day,117,1,14,Jay Kay,341629,11477231,0.99 1447,Soul Education,117,1,14,Jay Kay/Toby Smith,255477,8575435,0.99 1448,Failling,117,1,14,Jay Kay/Toby Smith,225227,7503999,0.99 1449,Destitute Illusions,117,1,14,Derrick McKenzie/Jay Kay/Toby Smith,340218,11452651,0.99 1450,Supersonic,117,1,14,Jay Kay,315872,10699265,0.99 1451,Butterfly,117,1,14,Jay Kay/Toby Smith,268852,8947356,0.99 1452,Were Do We Go From Here,117,1,14,Jay Kay,313626,10504158,0.99 1453,King For A Day,117,1,14,Jay Kay/Toby Smith,221544,7335693,0.99 1454,Deeper Underground,117,1,14,Toby Smith,281808,9351277,0.99 1455,Just Another Story,118,1,15,Toby Smith,529684,17582818,0.99 1456,Stillness In Time,118,1,15,Toby Smith,257097,8644290,0.99 1457,Half The Man,118,1,15,Toby Smith,289854,9577679,0.99 1458,Light Years,118,1,15,Toby Smith,354560,11796244,0.99 1459,Manifest Destiny,118,1,15,Toby Smith,382197,12676962,0.99 1460,The Kids,118,1,15,Toby Smith,309995,10334529,0.99 1461,Mr. Moon,118,1,15,Stuard Zender/Toby Smith,329534,11043559,0.99 1462,Scam,118,1,15,Stuart Zender,422321,14019705,0.99 1463,Journey To Arnhemland,118,1,15,Toby Smith/Wallis Buchanan,322455,10843832,0.99 1464,Morning Glory,118,1,15,J. Kay/Jay Kay,384130,12777210,0.99 1465,Space Cowboy,118,1,15,J. Kay/Jay Kay,385697,12906520,0.99 1466,Last Chance,119,1,4,C. Cester/C. Muncey,112352,3683130,0.99 1467,Are You Gonna Be My Girl,119,1,4,C. Muncey/N. Cester,213890,6992324,0.99 1468,Rollover D.J.,119,1,4,C. Cester/N. Cester,196702,6406517,0.99 1469,Look What You've Done,119,1,4,N. Cester,230974,7517083,0.99 1470,Get What You Need,119,1,4,C. Cester/C. Muncey/N. Cester,247719,8043765,0.99 1471,Move On,119,1,4,C. Cester/N. Cester,260623,8519353,0.99 1472,Radio Song,119,1,4,C. Cester/C. Muncey/N. Cester,272117,8871509,0.99 1473,Get Me Outta Here,119,1,4,C. Cester/N. Cester,176274,5729098,0.99 1474,Cold Hard Bitch,119,1,4,C. Cester/C. Muncey/N. Cester,243278,7929610,0.99 1475,Come Around Again,119,1,4,C. Muncey/N. Cester,270497,8872405,0.99 1476,Take It Or Leave It,119,1,4,C. Muncey/N. Cester,142889,4643370,0.99 1477,Lazy Gun,119,1,4,C. Cester/N. Cester,282174,9186285,0.99 1478,Timothy,119,1,4,C. Cester,270341,8856507,0.99 1479,Foxy Lady,120,1,1,Jimi Hendrix,199340,6480896,0.99 1480,Manic Depression,120,1,1,Jimi Hendrix,222302,7289272,0.99 1481,Red House,120,1,1,Jimi Hendrix,224130,7285851,0.99 1482,Can You See Me,120,1,1,Jimi Hendrix,153077,4987068,0.99 1483,Love Or Confusion,120,1,1,Jimi Hendrix,193123,6329408,0.99 1484,I Don't Live Today,120,1,1,Jimi Hendrix,235311,7661214,0.99 1485,May This Be Love,120,1,1,Jimi Hendrix,191216,6240028,0.99 1486,Fire,120,1,1,Jimi Hendrix,164989,5383075,0.99 1487,Third Stone From The Sun,120,1,1,Jimi Hendrix,404453,13186975,0.99 1488,Remember,120,1,1,Jimi Hendrix,168150,5509613,0.99 1489,Are You Experienced?,120,1,1,Jimi Hendrix,254537,8292497,0.99 1490,Hey Joe,120,1,1,Billy Roberts,210259,6870054,0.99 1491,Stone Free,120,1,1,Jimi Hendrix,216293,7002331,0.99 1492,Purple Haze,120,1,1,Jimi Hendrix,171572,5597056,0.99 1493,51st Anniversary,120,1,1,Jimi Hendrix,196388,6398044,0.99 1494,The Wind Cries Mary,120,1,1,Jimi Hendrix,200463,6540638,0.99 1495,Highway Chile,120,1,1,Jimi Hendrix,212453,6887949,0.99 1496,Surfing with the Alien,121,2,1,,263707,4418504,0.99 1497,Ice 9,121,2,1,,239721,4036215,0.99 1498,Crushing Day,121,2,1,,314768,5232158,0.99 1499,"Always With Me, Always With You",121,2,1,,202035,3435777,0.99 1500,Satch Boogie,121,2,1,,193560,3300654,0.99 1501,Hill of the Skull,121,2,1,J. Satriani,108435,1944738,0.99 1502,Circles,121,2,1,,209071,3548553,0.99 1503,Lords of Karma,121,2,1,J. Satriani,288227,4809279,0.99 1504,Midnight,121,2,1,J. Satriani,102630,1851753,0.99 1505,Echo,121,2,1,J. Satriani,337570,5595557,0.99 1506,Engenho De Dentro,122,1,7,,310073,10211473,0.99 1507,Alcohol,122,1,7,,355239,12010478,0.99 1508,Mama Africa,122,1,7,,283062,9488316,0.99 1509,Salve Simpatia,122,1,7,,343484,11314756,0.99 1510,W/Brasil (Chama O Síndico),122,1,7,,317100,10599953,0.99 1511,País Tropical,122,1,7,,452519,14946972,0.99 1512,Os Alquimistas Estão Chegando,122,1,7,,367281,12304520,0.99 1513,Charles Anjo 45,122,1,7,,389276,13022833,0.99 1514,Selassiê,122,1,7,,326321,10724982,0.99 1515,Menina Sarará,122,1,7,,191477,6393818,0.99 1516,Que Maravilha,122,1,7,,338076,10996656,0.99 1517,Santa Clara Clareou,122,1,7,,380081,12524725,0.99 1518,Filho Maravilha,122,1,7,,227526,7498259,0.99 1519,Taj Mahal,122,1,7,,289750,9502898,0.99 1520,Rapidamente,123,1,7,,252238,8470107,0.99 1521,As Dores do Mundo,123,1,7,Hyldon,255477,8537092,0.99 1522,Vou Pra Ai,123,1,7,,300878,10053718,0.99 1523,My Brother,123,1,7,,253231,8431821,0.99 1524,Há Quanto Tempo,123,1,7,,270027,9004470,0.99 1525,Vício,123,1,7,,269897,8887216,0.99 1526,Encontrar Alguém,123,1,7,Marco Tulio Lara/Rogerio Flausino,224078,7437935,0.99 1527,Dance Enquanto é Tempo,123,1,7,,229093,7583799,0.99 1528,A Tarde,123,1,7,,266919,8836127,0.99 1529,Always Be All Right,123,1,7,,128078,4299676,0.99 1530,Sem Sentido,123,1,7,,250462,8292108,0.99 1531,Onibusfobia,123,1,7,,315977,10474904,0.99 1532,Pura Elegancia,124,1,16,João Suplicy,284107,9632269,0.99 1533,Choramingando,124,1,16,João Suplicy,190484,6400532,0.99 1534,Por Merecer,124,1,16,João Suplicy,230582,7764601,0.99 1535,No Futuro,124,1,16,João Suplicy,182308,6056200,0.99 1536,Voce Inteira,124,1,16,João Suplicy,241084,8077282,0.99 1537,Cuando A Noite Vai Chegando,124,1,16,João Suplicy,270628,9081874,0.99 1538,Naquele Dia,124,1,16,João Suplicy,251768,8452654,0.99 1539,Equinocio,124,1,16,João Suplicy,269008,8871455,0.99 1540,Papelão,124,1,16,João Suplicy,213263,7257390,0.99 1541,Cuando Eu For Pro Ceu,124,1,16,João Suplicy,118804,3948371,0.99 1542,Do Nosso Amor,124,1,16,João Suplicy,203415,6774566,0.99 1543,Borogodo,124,1,16,João Suplicy,208457,7104588,0.99 1544,Cafezinho,124,1,16,João Suplicy,180924,6031174,0.99 1545,Enquanto O Dia Não Vem,124,1,16,João Suplicy,220891,7248336,0.99 1546,The Green Manalishi,125,1,3,,205792,6720789,0.99 1547,Living After Midnight,125,1,3,,213289,7056785,0.99 1548,Breaking The Law (Live),125,1,3,,144195,4728246,0.99 1549,Hot Rockin',125,1,3,,197328,6509179,0.99 1550,Heading Out To The Highway (Live),125,1,3,,276427,9006022,0.99 1551,The Hellion,125,1,3,,41900,1351993,0.99 1552,Electric Eye,125,1,3,,222197,7231368,0.99 1553,You've Got Another Thing Comin',125,1,3,,305162,9962558,0.99 1554,Turbo Lover,125,1,3,,335542,11068866,0.99 1555,Freewheel Burning,125,1,3,,265952,8713599,0.99 1556,Some Heads Are Gonna Roll,125,1,3,,249939,8198617,0.99 1557,Metal Meltdown,125,1,3,,290664,9390646,0.99 1558,Ram It Down,125,1,3,,292179,9554023,0.99 1559,Diamonds And Rust (Live),125,1,3,,219350,7163147,0.99 1560,Victim Of Change (Live),125,1,3,,430942,14067512,0.99 1561,Tyrant (Live),125,1,3,,282253,9190536,0.99 1562,Comin' Home,126,1,1,"Paul Stanley, Ace Frehley",172068,5661120,0.99 1563,Plaster Caster,126,1,1,Gene Simmons,198060,6528719,0.99 1564,Goin' Blind,126,1,1,"Gene Simmons, Stephen Coronel",217652,7167523,0.99 1565,Do You Love Me,126,1,1,"Paul Stanley, Bob Ezrin, Kim Fowley",193619,6343111,0.99 1566,Domino,126,1,1,Gene Simmons,226377,7488191,0.99 1567,Sure Know Something,126,1,1,"Paul Stanley, Vincent Poncia",254354,8375190,0.99 1568,A World Without Heroes,126,1,1,"Paul Stanley, Gene Simmons, Bob Ezrin, Lewis Reed",177815,5832524,0.99 1569,Rock Bottom,126,1,1,"Paul Stanley, Ace Frehley",200594,6560818,0.99 1570,See You Tonight,126,1,1,Gene Simmons,146494,4817521,0.99 1571,I Still Love You,126,1,1,Paul Stanley,369815,12086145,0.99 1572,Every Time I Look At You,126,1,1,"Paul Stanley, Vincent Cusano",283898,9290948,0.99 1573,"2,000 Man",126,1,1,"Mick Jagger, Keith Richard",312450,10292829,0.99 1574,Beth,126,1,1,"Peter Criss, Stan Penridge, Bob Ezrin",170187,5577807,0.99 1575,Nothin' To Lose,126,1,1,Gene Simmons,222354,7351460,0.99 1576,Rock And Roll All Nite,126,1,1,"Paul Stanley, Gene Simmons",259631,8549296,0.99 1577,Immigrant Song,127,1,1,Robert Plant,201247,6457766,0.99 1578,Heartbreaker,127,1,1,John Bonham/John Paul Jones/Robert Plant,316081,10179657,0.99 1579,Since I've Been Loving You,127,1,1,John Paul Jones/Robert Plant,416365,13471959,0.99 1580,Black Dog,127,1,1,John Paul Jones/Robert Plant,317622,10267572,0.99 1581,Dazed And Confused,127,1,1,Jimmy Page/Led Zeppelin,1116734,36052247,0.99 1582,Stairway To Heaven,127,1,1,Robert Plant,529658,17050485,0.99 1583,Going To California,127,1,1,Robert Plant,234605,7646749,0.99 1584,That's The Way,127,1,1,Robert Plant,343431,11248455,0.99 1585,Whole Lotta Love (Medley),127,1,1,Arthur Crudup/Bernard Besman/Bukka White/Doc Pomus/John Bonham/John Lee Hooker/John Paul Jones/Mort Shuman/Robert Plant/Willie Dixon,825103,26742545,0.99 1586,Thank You,127,1,1,Robert Plant,398262,12831826,0.99 1587,We're Gonna Groove,128,1,1,Ben E.King/James Bethea,157570,5180975,0.99 1588,Poor Tom,128,1,1,Jimmy Page/Robert Plant,182491,6016220,0.99 1589,I Can't Quit You Baby,128,1,1,Willie Dixon,258168,8437098,0.99 1590,Walter's Walk,128,1,1,"Jimmy Page, Robert Plant",270785,8712499,0.99 1591,Ozone Baby,128,1,1,"Jimmy Page, Robert Plant",215954,7079588,0.99 1592,Darlene,128,1,1,"Jimmy Page, Robert Plant, John Bonham, John Paul Jones",307226,10078197,0.99 1593,Bonzo's Montreux,128,1,1,John Bonham,258925,8557447,0.99 1594,Wearing And Tearing,128,1,1,"Jimmy Page, Robert Plant",330004,10701590,0.99 1595,The Song Remains The Same,129,1,1,Jimmy Page/Jimmy Page & Robert Plant/Robert Plant,330004,10708950,0.99 1596,The Rain Song,129,1,1,Jimmy Page/Jimmy Page & Robert Plant/Robert Plant,459180,15029875,0.99 1597,Over The Hills And Far Away,129,1,1,Jimmy Page/Jimmy Page & Robert Plant/Robert Plant,290089,9552829,0.99 1598,The Crunge,129,1,1,John Bonham/John Paul Jones,197407,6460212,0.99 1599,Dancing Days,129,1,1,Jimmy Page/Jimmy Page & Robert Plant/Robert Plant,223216,7250104,0.99 1600,D'Yer Mak'er,129,1,1,John Bonham/John Paul Jones,262948,8645935,0.99 1601,No Quarter,129,1,1,John Paul Jones,420493,13656517,0.99 1602,The Ocean,129,1,1,John Bonham/John Paul Jones,271098,8846469,0.99 1603,In The Evening,130,1,1,"Jimmy Page, Robert Plant & John Paul Jones",410566,13399734,0.99 1604,South Bound Saurez,130,1,1,John Paul Jones & Robert Plant,254406,8420427,0.99 1605,Fool In The Rain,130,1,1,"Jimmy Page, Robert Plant & John Paul Jones",372950,12371433,0.99 1606,Hot Dog,130,1,1,Jimmy Page & Robert Plant,197198,6536167,0.99 1607,Carouselambra,130,1,1,"John Paul Jones, Jimmy Page & Robert Plant",634435,20858315,0.99 1608,All My Love,130,1,1,Robert Plant & John Paul Jones,356284,11684862,0.99 1609,I'm Gonna Crawl,130,1,1,"Jimmy Page, Robert Plant & John Paul Jones",329639,10737665,0.99 1610,Black Dog,131,1,1,"Jimmy Page, Robert Plant, John Paul Jones",296672,9660588,0.99 1611,Rock & Roll,131,1,1,"Jimmy Page, Robert Plant, John Paul Jones, John Bonham",220917,7142127,0.99 1612,The Battle Of Evermore,131,1,1,"Jimmy Page, Robert Plant",351555,11525689,0.99 1613,Stairway To Heaven,131,1,1,"Jimmy Page, Robert Plant",481619,15706767,0.99 1614,Misty Mountain Hop,131,1,1,"Jimmy Page, Robert Plant, John Paul Jones",278857,9092799,0.99 1615,Four Sticks,131,1,1,"Jimmy Page, Robert Plant",284447,9481301,0.99 1616,Going To California,131,1,1,"Jimmy Page, Robert Plant",215693,7068737,0.99 1617,When The Levee Breaks,131,1,1,"Jimmy Page, Robert Plant, John Paul Jones, John Bonham, Memphis Minnie",427702,13912107,0.99 1618,Good Times Bad Times,132,1,1,Jimmy Page/John Bonham/John Paul Jones,166164,5464077,0.99 1619,Babe I'm Gonna Leave You,132,1,1,Jimmy Page/Robert Plant,401475,13189312,0.99 1620,You Shook Me,132,1,1,J. B. Lenoir/Willie Dixon,388179,12643067,0.99 1621,Dazed and Confused,132,1,1,Jimmy Page,386063,12610326,0.99 1622,Your Time Is Gonna Come,132,1,1,Jimmy Page/John Paul Jones,274860,9011653,0.99 1623,Black Mountain Side,132,1,1,Jimmy Page,132702,4440602,0.99 1624,Communication Breakdown,132,1,1,Jimmy Page/John Bonham/John Paul Jones,150230,4899554,0.99 1625,I Can't Quit You Baby,132,1,1,Willie Dixon,282671,9252733,0.99 1626,How Many More Times,132,1,1,Jimmy Page/John Bonham/John Paul Jones,508055,16541364,0.99 1627,Whole Lotta Love,133,1,1,"Jimmy Page, Robert Plant, John Paul Jones, John Bonham",334471,11026243,0.99 1628,What Is And What Should Never Be,133,1,1,"Jimmy Page, Robert Plant",287973,9369385,0.99 1629,The Lemon Song,133,1,1,"Jimmy Page, Robert Plant, John Paul Jones, John Bonham",379141,12463496,0.99 1630,Thank You,133,1,1,"Jimmy Page, Robert Plant",287791,9337392,0.99 1631,Heartbreaker,133,1,1,"Jimmy Page, Robert Plant, John Paul Jones, John Bonham",253988,8387560,0.99 1632,Living Loving Maid (She's Just A Woman),133,1,1,"Jimmy Page, Robert Plant",159216,5219819,0.99 1633,Ramble On,133,1,1,"Jimmy Page, Robert Plant",275591,9199710,0.99 1634,Moby Dick,133,1,1,"John Bonham, John Paul Jones, Jimmy Page",260728,8664210,0.99 1635,Bring It On Home,133,1,1,"Jimmy Page, Robert Plant",259970,8494731,0.99 1636,Immigrant Song,134,1,1,"Jimmy Page, Robert Plant",144875,4786461,0.99 1637,Friends,134,1,1,"Jimmy Page, Robert Plant",233560,7694220,0.99 1638,Celebration Day,134,1,1,"Jimmy Page, Robert Plant, John Paul Jones",209528,6871078,0.99 1639,Since I've Been Loving You,134,1,1,"Jimmy Page, Robert Plant, John Paul Jones",444055,14482460,0.99 1640,Out On The Tiles,134,1,1,"Jimmy Page, Robert Plant, John Bonham",246047,8060350,0.99 1641,Gallows Pole,134,1,1,Traditional,296228,9757151,0.99 1642,Tangerine,134,1,1,Jimmy Page,189675,6200893,0.99 1643,That's The Way,134,1,1,"Jimmy Page, Robert Plant",337345,11202499,0.99 1644,Bron-Y-Aur Stomp,134,1,1,"Jimmy Page, Robert Plant, John Paul Jones",259500,8674508,0.99 1645,Hats Off To (Roy) Harper,134,1,1,Traditional,219376,7236640,0.99 1646,In The Light,135,1,1,John Paul Jones/Robert Plant,526785,17033046,0.99 1647,Bron-Yr-Aur,135,1,1,Jimmy Page,126641,4150746,0.99 1648,Down By The Seaside,135,1,1,Robert Plant,316186,10371282,0.99 1649,Ten Years Gone,135,1,1,Robert Plant,393116,12756366,0.99 1650,Night Flight,135,1,1,John Paul Jones/Robert Plant,217547,7160647,0.99 1651,The Wanton Song,135,1,1,Robert Plant,249887,8180988,0.99 1652,Boogie With Stu,135,1,1,Ian Stewart/John Bonham/John Paul Jones/Mrs. Valens/Robert Plant,233273,7657086,0.99 1653,Black Country Woman,135,1,1,Robert Plant,273084,8951732,0.99 1654,Sick Again,135,1,1,Robert Plant,283036,9279263,0.99 1655,Achilles Last Stand,136,1,1,Jimmy Page/Robert Plant,625502,20593955,0.99 1656,For Your Life,136,1,1,Jimmy Page/Robert Plant,384391,12633382,0.99 1657,Royal Orleans,136,1,1,John Bonham/John Paul Jones,179591,5930027,0.99 1658,Nobody's Fault But Mine,136,1,1,Jimmy Page/Robert Plant,376215,12237859,0.99 1659,Candy Store Rock,136,1,1,Jimmy Page/Robert Plant,252055,8397423,0.99 1660,Hots On For Nowhere,136,1,1,Jimmy Page/Robert Plant,284107,9342342,0.99 1661,Tea For One,136,1,1,Jimmy Page/Robert Plant,566752,18475264,0.99 1662,Rock & Roll,137,1,1,John Bonham/John Paul Jones/Robert Plant,242442,7897065,0.99 1663,Celebration Day,137,1,1,John Paul Jones/Robert Plant,230034,7478487,0.99 1664,The Song Remains The Same,137,1,1,Robert Plant,353358,11465033,0.99 1665,Rain Song,137,1,1,Robert Plant,505808,16273705,0.99 1666,Dazed And Confused,137,1,1,Jimmy Page,1612329,52490554,0.99 1667,No Quarter,138,1,1,John Paul Jones/Robert Plant,749897,24399285,0.99 1668,Stairway To Heaven,138,1,1,Robert Plant,657293,21354766,0.99 1669,Moby Dick,138,1,1,John Bonham/John Paul Jones,766354,25345841,0.99 1670,Whole Lotta Love,138,1,1,John Bonham/John Paul Jones/Robert Plant/Willie Dixon,863895,28191437,0.99 1671,Natália,139,1,7,Renato Russo,235728,7640230,0.99 1672,L'Avventura,139,1,7,Renato Russo,278256,9165769,0.99 1673,Música De Trabalho,139,1,7,Renato Russo,260231,8590671,0.99 1674,Longe Do Meu Lado,139,1,7,Renato Russo - Marcelo Bonfá,266161,8655249,0.99 1675,A Via Láctea,139,1,7,Renato Russo,280084,9234879,0.99 1676,Música Ambiente,139,1,7,Renato Russo,247614,8234388,0.99 1677,Aloha,139,1,7,Renato Russo,325955,10793301,0.99 1678,Soul Parsifal,139,1,7,Renato Russo - Marisa Monte,295053,9853589,0.99 1679,Dezesseis,139,1,7,Renato Russo,323918,10573515,0.99 1680,Mil Pedaços,139,1,7,Renato Russo,203337,6643291,0.99 1681,Leila,139,1,7,Renato Russo,323056,10608239,0.99 1682,1º De Julho,139,1,7,Renato Russo,290298,9619257,0.99 1683,Esperando Por Mim,139,1,7,Renato Russo,261668,8844133,0.99 1684,Quando Você Voltar,139,1,7,Renato Russo,173897,5781046,0.99 1685,O Livro Dos Dias,139,1,7,Renato Russo,257253,8570929,0.99 1686,Será,140,1,7,Dado Villa-Lobos/Marcelo Bonfá,148401,4826528,0.99 1687,Ainda É Cedo,140,1,7,Dado Villa-Lobos/Ico Ouro-Preto/Marcelo Bonfá,236826,7796400,0.99 1688,Geração Coca-Cola,140,1,7,Renato Russo,141453,4625731,0.99 1689,Eduardo E Mônica,140,1,7,Renato Russo,271229,9026691,0.99 1690,Tempo Perdido,140,1,7,Renato Russo,302158,9963914,0.99 1691,Indios,140,1,7,Renato Russo,258168,8610226,0.99 1692,Que País É Este,140,1,7,Renato Russo,177606,5822124,0.99 1693,Faroeste Caboclo,140,1,7,Renato Russo,543007,18092739,0.99 1694,Há Tempos,140,1,7,Dado Villa-Lobos/Marcelo Bonfá,197146,6432922,0.99 1695,Pais E Filhos,140,1,7,Dado Villa-Lobos/Marcelo Bonfá,308401,10130685,0.99 1696,Meninos E Meninas,140,1,7,Dado Villa-Lobos/Marcelo Bonfá,203781,6667802,0.99 1697,Vento No Litoral,140,1,7,Dado Villa-Lobos/Marcelo Bonfá,366445,12063806,0.99 1698,Perfeição,140,1,7,Dado Villa-Lobos/Marcelo Bonfá,276558,9258489,0.99 1699,Giz,140,1,7,Dado Villa-Lobos/Marcelo Bonfá,202213,6677671,0.99 1700,Dezesseis,140,1,7,Dado Villa-Lobos/Marcelo Bonfá,321724,10501773,0.99 1701,Antes Das Seis,140,1,7,Dado Villa-Lobos,189231,6296531,0.99 1702,Are You Gonna Go My Way,141,1,1,Craig Ross/Lenny Kravitz,211591,6905135,0.99 1703,Fly Away,141,1,1,Lenny Kravitz,221962,7322085,0.99 1704,Rock And Roll Is Dead,141,1,1,Lenny Kravitz,204199,6680312,0.99 1705,Again,141,1,1,Lenny Kravitz,228989,7490476,0.99 1706,It Ain't Over 'Til It's Over,141,1,1,Lenny Kravitz,242703,8078936,0.99 1707,Can't Get You Off My Mind,141,1,1,Lenny Kravitz,273815,8937150,0.99 1708,Mr. Cab Driver,141,1,1,Lenny Kravitz,230321,7668084,0.99 1709,American Woman,141,1,1,B. Cummings/G. Peterson/M.J. Kale/R. Bachman,261773,8538023,0.99 1710,Stand By My Woman,141,1,1,Henry Kirssch/Lenny Kravitz/S. Pasch A. Krizan,259683,8447611,0.99 1711,Always On The Run,141,1,1,Lenny Kravitz/Slash,232515,7593397,0.99 1712,Heaven Help,141,1,1,Gerry DeVeaux/Terry Britten,190354,6222092,0.99 1713,I Belong To You,141,1,1,Lenny Kravitz,257123,8477980,0.99 1714,Believe,141,1,1,Henry Hirsch/Lenny Kravitz,295131,9661978,0.99 1715,Let Love Rule,141,1,1,Lenny Kravitz,342648,11298085,0.99 1716,Black Velveteen,141,1,1,Lenny Kravitz,290899,9531301,0.99 1717,Assim Caminha A Humanidade,142,1,7,,210755,6993763,0.99 1718,Honolulu,143,1,7,,261433,8558481,0.99 1719,Dancin´Days,143,1,7,,237400,7875347,0.99 1720,Um Pro Outro,142,1,7,,236382,7825215,0.99 1721,Aviso Aos Navegantes,143,1,7,,242808,8058651,0.99 1722,Casa,142,1,7,,307591,10107269,0.99 1723,Condição,142,1,7,,263549,8778465,0.99 1724,Hyperconectividade,143,1,7,,180636,5948039,0.99 1725,O Descobridor Dos Sete Mares,143,1,7,,225854,7475780,0.99 1726,Satisfação,142,1,7,,208065,6901681,0.99 1727,Brumário,142,1,7,,216241,7243499,0.99 1728,Um Certo Alguém,143,1,7,,194063,6430939,0.99 1729,Fullgás,143,1,7,,346070,11505484,0.99 1730,Sábado À Noite,142,1,7,,193854,6435114,0.99 1731,A Cura,142,1,7,,280920,9260588,0.99 1732,Aquilo,143,1,7,,246073,8167819,0.99 1733,Atrás Do Trio Elétrico,142,1,7,,149080,4917615,0.99 1734,Senta A Pua,143,1,7,,217547,7205844,0.99 1735,Ro-Que-Se-Da-Ne,143,1,7,,146703,4805897,0.99 1736,Tudo Bem,142,1,7,,196101,6419139,0.99 1737,Toda Forma De Amor,142,1,7,,227813,7496584,0.99 1738,Tudo Igual,143,1,7,,276035,9201645,0.99 1739,Fogo De Palha,143,1,7,,246804,8133732,0.99 1740,Sereia,142,1,7,,278047,9121087,0.99 1741,Assaltaram A Gramática,143,1,7,,261041,8698959,0.99 1742,Se Você Pensa,142,1,7,,195996,6552490,0.99 1743,Lá Vem O Sol (Here Comes The Sun),142,1,7,,189492,6229645,0.99 1744,O Último Romântico (Ao Vivo),143,1,7,,231993,7692697,0.99 1745,Pseudo Silk Kimono,144,1,1,"Kelly, Mosley, Rothery, Trewaves",134739,4334038,0.99 1746,Kayleigh,144,1,1,"Kelly, Mosley, Rothery, Trewaves",234605,7716005,0.99 1747,Lavender,144,1,1,"Kelly, Mosley, Rothery, Trewaves",153417,4999814,0.99 1748,Bitter Suite: Brief Encounter / Lost Weekend / Blue Angel,144,1,1,"Kelly, Mosley, Rothery, Trewaves",356493,11791068,0.99 1749,Heart Of Lothian: Wide Boy / Curtain Call,144,1,1,"Kelly, Mosley, Rothery, Trewaves",366053,11893723,0.99 1750,Waterhole (Expresso Bongo),144,1,1,"Kelly, Mosley, Rothery, Trewaves",133093,4378835,0.99 1751,Lords Of The Backstage,144,1,1,"Kelly, Mosley, Rothery, Trewaves",112875,3741319,0.99 1752,Blind Curve: Vocal Under A Bloodlight / Passing Strangers / Mylo / Perimeter Walk / Threshold,144,1,1,"Kelly, Mosley, Rothery, Trewaves",569704,18578995,0.99 1753,Childhoods End?,144,1,1,"Kelly, Mosley, Rothery, Trewaves",272796,9015366,0.99 1754,White Feather,144,1,1,"Kelly, Mosley, Rothery, Trewaves",143595,4711776,0.99 1755,Arrepio,145,1,7,Carlinhos Brown,136254,4511390,0.99 1756,Magamalabares,145,1,7,Carlinhos Brown,215875,7183757,0.99 1757,Chuva No Brejo,145,1,7,Morais,145606,4857761,0.99 1758,Cérebro Eletrônico,145,1,7,Gilberto Gil,172800,5760864,0.99 1759,Tempos Modernos,145,1,7,Lulu Santos,183066,6066234,0.99 1760,Maraçá,145,1,7,Carlinhos Brown,230008,7621482,0.99 1761,Blanco,145,1,7,Marisa Monte/poema de Octavio Paz/versão: Haroldo de Campos,45191,1454532,0.99 1762,Panis Et Circenses,145,1,7,Caetano Veloso e Gilberto Gil,192339,6318373,0.99 1763,De Noite Na Cama,145,1,7,Caetano Veloso e Gilberto Gil,209005,7012658,0.99 1764,Beija Eu,145,1,7,Caetano Veloso e Gilberto Gil,197276,6512544,0.99 1765,Give Me Love,145,1,7,Caetano Veloso e Gilberto Gil,249808,8196331,0.99 1766,Ainda Lembro,145,1,7,Caetano Veloso e Gilberto Gil,218801,7211247,0.99 1767,A Menina Dança,145,1,7,Caetano Veloso e Gilberto Gil,129410,4326918,0.99 1768,Dança Da Solidão,145,1,7,Caetano Veloso e Gilberto Gil,203520,6699368,0.99 1769,Ao Meu Redor,145,1,7,Caetano Veloso e Gilberto Gil,275591,9158834,0.99 1770,Bem Leve,145,1,7,Caetano Veloso e Gilberto Gil,159190,5246835,0.99 1771,Segue O Seco,145,1,7,Caetano Veloso e Gilberto Gil,178207,5922018,0.99 1772,O Xote Das Meninas,145,1,7,Caetano Veloso e Gilberto Gil,291866,9553228,0.99 1773,Wherever I Lay My Hat,146,1,14,,136986,4477321,0.99 1774,Get My Hands On Some Lovin',146,1,14,,149054,4860380,0.99 1775,No Good Without You,146,1,14,"William ""Mickey"" Stevenson",161410,5259218,0.99 1776,You've Been A Long Time Coming,146,1,14,Brian Holland/Eddie Holland/Lamont Dozier,137221,4437949,0.99 1777,When I Had Your Love,146,1,14,"Robert Rogers/Warren ""Pete"" Moore/William ""Mickey"" Stevenson",152424,4972815,0.99 1778,You're What's Happening (In The World Today),146,1,14,Allen Story/George Gordy/Robert Gordy,142027,4631104,0.99 1779,Loving You Is Sweeter Than Ever,146,1,14,Ivy Hunter/Stevie Wonder,166295,5377546,0.99 1780,It's A Bitter Pill To Swallow,146,1,14,"Smokey Robinson/Warren ""Pete"" Moore",194821,6477882,0.99 1781,Seek And You Shall Find,146,1,14,"Ivy Hunter/William ""Mickey"" Stevenson",223451,7306719,0.99 1782,Gonna Keep On Tryin' Till I Win Your Love,146,1,14,Barrett Strong/Norman Whitfield,176404,5789945,0.99 1783,Gonna Give Her All The Love I've Got,146,1,14,Barrett Strong/Norman Whitfield,210886,6893603,0.99 1784,I Wish It Would Rain,146,1,14,Barrett Strong/Norman Whitfield/Roger Penzabene,172486,5647327,0.99 1785,"Abraham, Martin And John",146,1,14,Dick Holler,273057,8888206,0.99 1786,Save The Children,146,1,14,Al Cleveland/Marvin Gaye/Renaldo Benson,194821,6342021,0.99 1787,You Sure Love To Ball,146,1,14,Marvin Gaye,218540,7217872,0.99 1788,Ego Tripping Out,146,1,14,Marvin Gaye,314514,10383887,0.99 1789,Praise,146,1,14,Marvin Gaye,235833,7839179,0.99 1790,Heavy Love Affair,146,1,14,Marvin Gaye,227892,7522232,0.99 1791,Down Under,147,1,1,,222171,7366142,0.99 1792,Overkill,147,1,1,,225410,7408652,0.99 1793,Be Good Johnny,147,1,1,,216320,7139814,0.99 1794,Everything I Need,147,1,1,,216476,7107625,0.99 1795,Down by the Sea,147,1,1,,408163,13314900,0.99 1796,Who Can It Be Now?,147,1,1,,202396,6682850,0.99 1797,It's a Mistake,147,1,1,,273371,8979965,0.99 1798,Dr. Heckyll & Mr. Jive,147,1,1,,278465,9110403,0.99 1799,Shakes and Ladders,147,1,1,,198008,6560753,0.99 1800,No Sign of Yesterday,147,1,1,,362004,11829011,0.99 1801,Enter Sandman,148,1,3,"James Hetfield, Lars Ulrich and Kirk Hammett",332251,10852002,0.99 1802,Sad But True,148,1,3,Ulrich,324754,10541258,0.99 1803,Holier Than Thou,148,1,3,Ulrich,227892,7462011,0.99 1804,The Unforgiven,148,1,3,"James Hetfield, Lars Ulrich and Kirk Hammett",387082,12646886,0.99 1805,Wherever I May Roam,148,1,3,Ulrich,404323,13161169,0.99 1806,Don't Tread On Me,148,1,3,Ulrich,240483,7827907,0.99 1807,Through The Never,148,1,3,"James Hetfield, Lars Ulrich and Kirk Hammett",244375,8024047,0.99 1808,Nothing Else Matters,148,1,3,Ulrich,388832,12606241,0.99 1809,Of Wolf And Man,148,1,3,"James Hetfield, Lars Ulrich and Kirk Hammett",256835,8339785,0.99 1810,The God That Failed,148,1,3,Ulrich,308610,10055959,0.99 1811,My Friend Of Misery,148,1,3,"James Hetfield, Lars Ulrich and Jason Newsted",409547,13293515,0.99 1812,The Struggle Within,148,1,3,Ulrich,234240,7654052,0.99 1813,Helpless,149,1,3,Harris/Tatler,398315,12977902,0.99 1814,The Small Hours,149,1,3,Holocaust,403435,13215133,0.99 1815,The Wait,149,1,3,Killing Joke,295418,9688418,0.99 1816,Crash Course In Brain Surgery,149,1,3,Bourge/Phillips/Shelley,190406,6233729,0.99 1817,Last Caress/Green Hell,149,1,3,Danzig,209972,6854313,0.99 1818,Am I Evil?,149,1,3,Harris/Tatler,470256,15387219,0.99 1819,Blitzkrieg,149,1,3,Jones/Sirotto/Smith,216685,7090018,0.99 1820,Breadfan,149,1,3,Bourge/Phillips/Shelley,341551,11100130,0.99 1821,The Prince,149,1,3,Harris/Tatler,265769,8624492,0.99 1822,Stone Cold Crazy,149,1,3,Deacon/May/Mercury/Taylor,137717,4514830,0.99 1823,So What,149,1,3,Culmer/Exalt,189152,6162894,0.99 1824,Killing Time,149,1,3,Sweet Savage,183693,6021197,0.99 1825,Overkill,149,1,3,Clarke/Kilmister/Tayler,245133,7971330,0.99 1826,Damage Case,149,1,3,Clarke/Farren/Kilmister/Tayler,220212,7212997,0.99 1827,Stone Dead Forever,149,1,3,Clarke/Kilmister/Tayler,292127,9556060,0.99 1828,Too Late Too Late,149,1,3,Clarke/Kilmister/Tayler,192052,6276291,0.99 1829,Hit The Lights,150,1,3,"James Hetfield, Lars Ulrich",257541,8357088,0.99 1830,The Four Horsemen,150,1,3,"James Hetfield, Lars Ulrich, Dave Mustaine",433188,14178138,0.99 1831,Motorbreath,150,1,3,James Hetfield,188395,6153933,0.99 1832,Jump In The Fire,150,1,3,"James Hetfield, Lars Ulrich, Dave Mustaine",281573,9135755,0.99 1833,(Anesthesia) Pulling Teeth,150,1,3,Cliff Burton,254955,8234710,0.99 1834,Whiplash,150,1,3,"James Hetfield, Lars Ulrich",249208,8102839,0.99 1835,Phantom Lord,150,1,3,"James Hetfield, Lars Ulrich, Dave Mustaine",302053,9817143,0.99 1836,No Remorse,150,1,3,"James Hetfield, Lars Ulrich",386795,12672166,0.99 1837,Seek & Destroy,150,1,3,"James Hetfield, Lars Ulrich",415817,13452301,0.99 1838,Metal Militia,150,1,3,"James Hetfield, Lars Ulrich, Dave Mustaine",311327,10141785,0.99 1839,Ain't My Bitch,151,1,3,"James Hetfield, Lars Ulrich",304457,9931015,0.99 1840,2 X 4,151,1,3,"James Hetfield, Lars Ulrich, Kirk Hammett",328254,10732251,0.99 1841,The House Jack Built,151,1,3,"James Hetfield, Lars Ulrich, Kirk Hammett",398942,13005152,0.99 1842,Until It Sleeps,151,1,3,"James Hetfield, Lars Ulrich",269740,8837394,0.99 1843,King Nothing,151,1,3,"James Hetfield, Lars Ulrich, Kirk Hammett",328097,10681477,0.99 1844,Hero Of The Day,151,1,3,"James Hetfield, Lars Ulrich, Kirk Hammett",261982,8540298,0.99 1845,Bleeding Me,151,1,3,"James Hetfield, Lars Ulrich, Kirk Hammett",497998,16249420,0.99 1846,Cure,151,1,3,"James Hetfield, Lars Ulrich",294347,9648615,0.99 1847,Poor Twisted Me,151,1,3,"James Hetfield, Lars Ulrich",240065,7854349,0.99 1848,Wasted My Hate,151,1,3,"James Hetfield, Lars Ulrich, Kirk Hammett",237296,7762300,0.99 1849,Mama Said,151,1,3,"James Hetfield, Lars Ulrich",319764,10508310,0.99 1850,Thorn Within,151,1,3,"James Hetfield, Lars Ulrich, Kirk Hammett",351738,11486686,0.99 1851,Ronnie,151,1,3,"James Hetfield, Lars Ulrich",317204,10390947,0.99 1852,The Outlaw Torn,151,1,3,"James Hetfield, Lars Ulrich",588721,19286261,0.99 1853,Battery,152,1,3,J.Hetfield/L.Ulrich,312424,10229577,0.99 1854,Master Of Puppets,152,1,3,K.Hammett,515239,16893720,0.99 1855,The Thing That Should Not Be,152,1,3,K.Hammett,396199,12952368,0.99 1856,Welcome Home (Sanitarium),152,1,3,K.Hammett,387186,12679965,0.99 1857,Disposable Heroes,152,1,3,J.Hetfield/L.Ulrich,496718,16135560,0.99 1858,Leper Messiah,152,1,3,C.Burton,347428,11310434,0.99 1859,Orion,152,1,3,K.Hammett,500062,16378477,0.99 1860,Damage Inc.,152,1,3,K.Hammett,330919,10725029,0.99 1861,Fuel,153,1,3,"Hetfield, Ulrich, Hammett",269557,8876811,0.99 1862,The Memory Remains,153,1,3,"Hetfield, Ulrich",279353,9110730,0.99 1863,Devil's Dance,153,1,3,"Hetfield, Ulrich",318955,10414832,0.99 1864,The Unforgiven II,153,1,3,"Hetfield, Ulrich, Hammett",395520,12886474,0.99 1865,Better Than You,153,1,3,"Hetfield, Ulrich",322899,10549070,0.99 1866,Slither,153,1,3,"Hetfield, Ulrich, Hammett",313103,10199789,0.99 1867,Carpe Diem Baby,153,1,3,"Hetfield, Ulrich, Hammett",372480,12170693,0.99 1868,Bad Seed,153,1,3,"Hetfield, Ulrich, Hammett",245394,8019586,0.99 1869,Where The Wild Things Are,153,1,3,"Hetfield, Ulrich, Newsted",414380,13571280,0.99 1870,Prince Charming,153,1,3,"Hetfield, Ulrich",365061,12009412,0.99 1871,Low Man's Lyric,153,1,3,"Hetfield, Ulrich",457639,14855583,0.99 1872,Attitude,153,1,3,"Hetfield, Ulrich",315898,10335734,0.99 1873,Fixxxer,153,1,3,"Hetfield, Ulrich, Hammett",496065,16190041,0.99 1874,Fight Fire With Fire,154,1,3,Metallica,285753,9420856,0.99 1875,Ride The Lightning,154,1,3,Metallica,397740,13055884,0.99 1876,For Whom The Bell Tolls,154,1,3,Metallica,311719,10159725,0.99 1877,Fade To Black,154,1,3,Metallica,414824,13531954,0.99 1878,Trapped Under Ice,154,1,3,Metallica,244532,7975942,0.99 1879,Escape,154,1,3,Metallica,264359,8652332,0.99 1880,Creeping Death,154,1,3,Metallica,396878,12955593,0.99 1881,The Call Of Ktulu,154,1,3,Metallica,534883,17486240,0.99 1882,Frantic,155,1,3,Bob Rock/James Hetfield/Kirk Hammett/Lars Ulrich,350458,11510849,0.99 1883,St. Anger,155,1,3,Bob Rock/James Hetfield/Kirk Hammett/Lars Ulrich,441234,14363779,0.99 1884,Some Kind Of Monster,155,1,3,Bob Rock/James Hetfield/Kirk Hammett/Lars Ulrich,505626,16557497,0.99 1885,Dirty Window,155,1,3,Bob Rock/James Hetfield/Kirk Hammett/Lars Ulrich,324989,10670604,0.99 1886,Invisible Kid,155,1,3,Bob Rock/James Hetfield/Kirk Hammett/Lars Ulrich,510197,16591800,0.99 1887,My World,155,1,3,Bob Rock/James Hetfield/Kirk Hammett/Lars Ulrich,345626,11253756,0.99 1888,Shoot Me Again,155,1,3,Bob Rock/James Hetfield/Kirk Hammett/Lars Ulrich,430210,14093551,0.99 1889,Sweet Amber,155,1,3,Bob Rock/James Hetfield/Kirk Hammett/Lars Ulrich,327235,10616595,0.99 1890,The Unnamed Feeling,155,1,3,Bob Rock/James Hetfield/Kirk Hammett/Lars Ulrich,429479,14014582,0.99 1891,Purify,155,1,3,Bob Rock/James Hetfield/Kirk Hammett/Lars Ulrich,314017,10232537,0.99 1892,All Within My Hands,155,1,3,Bob Rock/James Hetfield/Kirk Hammett/Lars Ulrich,527986,17162741,0.99 1893,Blackened,156,1,3,"James Hetfield, Lars Ulrich & Jason Newsted",403382,13254874,0.99 1894,...And Justice For All,156,1,3,"James Hetfield, Lars Ulrich & Kirk Hammett",585769,19262088,0.99 1895,Eye Of The Beholder,156,1,3,"James Hetfield, Lars Ulrich & Kirk Hammett",385828,12747894,0.99 1896,One,156,1,3,James Hetfield & Lars Ulrich,446484,14695721,0.99 1897,The Shortest Straw,156,1,3,James Hetfield and Lars Ulrich,395389,13013990,0.99 1898,Harvester Of Sorrow,156,1,3,James Hetfield and Lars Ulrich,345547,11377339,0.99 1899,The Frayed Ends Of Sanity,156,1,3,"James Hetfield, Lars Ulrich and Kirk Hammett",464039,15198986,0.99 1900,To Live Is To Die,156,1,3,"James Hetfield, Lars Ulrich and Cliff Burton",588564,19243795,0.99 1901,Dyers Eve,156,1,3,"James Hetfield, Lars Ulrich and Kirk Hammett",313991,10302828,0.99 1902,Springsville,157,1,2,J. Carisi,207725,6776219,0.99 1903,The Maids Of Cadiz,157,1,2,L. Delibes,233534,7505275,0.99 1904,The Duke,157,1,2,Dave Brubeck,214961,6977626,0.99 1905,My Ship,157,1,2,"Ira Gershwin, Kurt Weill",268016,8581144,0.99 1906,Miles Ahead,157,1,2,"Miles Davis, Gil Evans",209893,6807707,0.99 1907,Blues For Pablo,157,1,2,Gil Evans,318328,10218398,0.99 1908,New Rhumba,157,1,2,A. Jamal,276871,8980400,0.99 1909,The Meaning Of The Blues,157,1,2,"R. Troup, L. Worth",168594,5395412,0.99 1910,Lament,157,1,2,J.J. Johnson,134191,4293394,0.99 1911,I Don't Wanna Be Kissed (By Anyone But You),157,1,2,"H. Spina, J. Elliott",191320,6219487,0.99 1912,Springsville (Alternate Take),157,1,2,J. Carisi,196388,6382079,0.99 1913,Blues For Pablo (Alternate Take),157,1,2,Gil Evans,212558,6900619,0.99 1914,The Meaning Of The Blues/Lament (Alternate Take),157,1,2,"J.J. Johnson/R. Troup, L. Worth",309786,9912387,0.99 1915,I Don't Wanna Be Kissed (By Anyone But You) (Alternate Take),157,1,2,"H. Spina, J. Elliott",192078,6254796,0.99 1916,Coração De Estudante,158,1,7,"Wagner Tiso, Milton Nascimento",238550,7797308,0.99 1917,A Noite Do Meu Bem,158,1,7,Dolores Duran,220081,7125225,0.99 1918,Paisagem Na Janela,158,1,7,"Lô Borges, Fernando Brant",197694,6523547,0.99 1919,Cuitelinho,158,1,7,Folclore,209397,6803970,0.99 1920,Caxangá,158,1,7,"Milton Nascimento, Fernando Brant",245551,8144179,0.99 1921,Nos Bailes Da Vida,158,1,7,"Milton Nascimento, Fernando Brant",275748,9126170,0.99 1922,Menestrel Das Alagoas,158,1,7,"Milton Nascimento, Fernando Brant",199758,6542289,0.99 1923,Brasil,158,1,7,"Milton Nascimento, Fernando Brant",155428,5252560,0.99 1924,Canção Do Novo Mundo,158,1,7,"Beto Guedes, Ronaldo Bastos",215353,7032626,0.99 1925,Um Gosto De Sol,158,1,7,"Milton Nascimento, Ronaldo Bastos",307200,9893875,0.99 1926,Solar,158,1,7,"Milton Nascimento, Fernando Brant",156212,5098288,0.99 1927,Para Lennon E McCartney,158,1,7,"Lô Borges, Márcio Borges, Fernando Brant",321828,10626920,0.99 1928,"Maria, Maria",158,1,7,"Milton Nascimento, Fernando Brant",72463,2371543,0.99 1929,Minas,159,1,7,"Milton Nascimento, Caetano Veloso",152293,4921056,0.99 1930,"Fé Cega, Faca Amolada",159,1,7,"Milton Nascimento, Ronaldo Bastos",278099,9258649,0.99 1931,Beijo Partido,159,1,7,Toninho Horta,229564,7506969,0.99 1932,Saudade Dos Aviões Da Panair (Conversando No Bar),159,1,7,"Milton Nascimento, Fernando Brant",268721,8805088,0.99 1933,Gran Circo,159,1,7,"Milton Nascimento, Márcio Borges",251297,8237026,0.99 1934,Ponta de Areia,159,1,7,"Milton Nascimento, Fernando Brant",272796,8874285,0.99 1935,Trastevere,159,1,7,"Milton Nascimento, Ronaldo Bastos",265665,8708399,0.99 1936,Idolatrada,159,1,7,"Milton Nascimento, Fernando Brant",286249,9426153,0.99 1937,Leila (Venha Ser Feliz),159,1,7,Milton Nascimento,209737,6898507,0.99 1938,Paula E Bebeto,159,1,7,"Milton Nascimento, Caetano Veloso",135732,4583956,0.99 1939,Simples,159,1,7,Nelson Angelo,133093,4326333,0.99 1940,Norwegian Wood,159,1,7,"John Lennon, Paul McCartney",413910,13520382,0.99 1941,Caso Você Queira Saber,159,1,7,"Beto Guedes, Márcio Borges",205688,6787901,0.99 1942,Ace Of Spades,160,1,3,Clarke/Kilmister/Taylor,169926,5523552,0.99 1943,Love Me Like A Reptile,160,1,3,Clarke/Kilmister/Taylor,203546,6616389,0.99 1944,Shoot You In The Back,160,1,3,Clarke/Kilmister/Taylor,160026,5175327,0.99 1945,Live To Win,160,1,3,Clarke/Kilmister/Taylor,217626,7102182,0.99 1946,Fast And Loose,160,1,3,Clarke/Kilmister/Taylor,203337,6643350,0.99 1947,(We Are) The Road Crew,160,1,3,Clarke/Kilmister/Taylor,192600,6283035,0.99 1948,Fire Fire,160,1,3,Clarke/Kilmister/Taylor,164675,5416114,0.99 1949,Jailbait,160,1,3,Clarke/Kilmister/Taylor,213916,6983609,0.99 1950,Dance,160,1,3,Clarke/Kilmister/Taylor,158432,5155099,0.99 1951,Bite The Bullet,160,1,3,Clarke/Kilmister/Taylor,98115,3195536,0.99 1952,The Chase Is Better Than The Catch,160,1,3,Clarke/Kilmister/Taylor,258403,8393310,0.99 1953,The Hammer,160,1,3,Clarke/Kilmister/Taylor,168071,5543267,0.99 1954,Dirty Love,160,1,3,Clarke/Kilmister/Taylor,176457,5805241,0.99 1955,Please Don't Touch,160,1,3,Heath/Robinson,169926,5557002,0.99 1956,Emergency,160,1,3,Dufort/Johnson/McAuliffe/Williams,180427,5828728,0.99 1957,Kir Royal,161,1,16,Mônica Marianno,234788,7706552,0.99 1958,O Que Vai Em Meu Coração,161,1,16,Mônica Marianno,255373,8366846,0.99 1959,Aos Leões,161,1,16,Mônica Marianno,234684,7790574,0.99 1960,Dois Índios,161,1,16,Mônica Marianno,219271,7213072,0.99 1961,Noite Negra,161,1,16,Mônica Marianno,206811,6819584,0.99 1962,Beijo do Olhar,161,1,16,Mônica Marianno,252682,8369029,0.99 1963,É Fogo,161,1,16,Mônica Marianno,194873,6501520,0.99 1964,Já Foi,161,1,16,Mônica Marianno,245681,8094872,0.99 1965,Só Se For Pelo Cabelo,161,1,16,Mônica Marianno,238288,8006345,0.99 1966,No Clima,161,1,16,Mônica Marianno,249495,8362040,0.99 1967,A Moça e a Chuva,161,1,16,Mônica Marianno,274625,8929357,0.99 1968,Demorou!,161,1,16,Mônica Marianno,39131,1287083,0.99 1969,Bitter Pill,162,1,3,Mick Mars/Nikki Sixx/Tommy Lee/Vince Neil,266814,8666786,0.99 1970,Enslaved,162,1,3,Mick Mars/Nikki Sixx/Tommy Lee,269844,8789966,0.99 1971,"Girls, Girls, Girls",162,1,3,Mick Mars/Nikki Sixx/Tommy Lee,270288,8874814,0.99 1972,Kickstart My Heart,162,1,3,Nikki Sixx,283559,9237736,0.99 1973,Wild Side,162,1,3,Nikki Sixx/Tommy Lee/Vince Neil,276767,9116997,0.99 1974,Glitter,162,1,3,Bryan Adams/Nikki Sixx/Scott Humphrey,340114,11184094,0.99 1975,Dr. Feelgood,162,1,3,Mick Mars/Nikki Sixx,282618,9281875,0.99 1976,Same Ol' Situation,162,1,3,Mick Mars/Nikki Sixx/Tommy Lee/Vince Neil,254511,8283958,0.99 1977,Home Sweet Home,162,1,3,Nikki Sixx/Tommy Lee/Vince Neil,236904,7697538,0.99 1978,Afraid,162,1,3,Nikki Sixx,248006,8077464,0.99 1979,Don't Go Away Mad (Just Go Away),162,1,3,Mick Mars/Nikki Sixx,279980,9188156,0.99 1980,Without You,162,1,3,Mick Mars/Nikki Sixx,268956,8738371,0.99 1981,Smokin' in The Boys Room,162,1,3,Cub Coda/Michael Lutz,206837,6735408,0.99 1982,Primal Scream,162,1,3,Mick Mars/Nikki Sixx/Tommy Lee/Vince Neil,286197,9421164,0.99 1983,Too Fast For Love,162,1,3,Nikki Sixx,200829,6580542,0.99 1984,Looks That Kill,162,1,3,Nikki Sixx,240979,7831122,0.99 1985,Shout At The Devil,162,1,3,Nikki Sixx,221962,7281974,0.99 1986,Intro,163,1,1,Kurt Cobain,52218,1688527,0.99 1987,School,163,1,1,Kurt Cobain,160235,5234885,0.99 1988,Drain You,163,1,1,Kurt Cobain,215196,7013175,0.99 1989,Aneurysm,163,1,1,Nirvana,271516,8862545,0.99 1990,Smells Like Teen Spirit,163,1,1,Nirvana,287190,9425215,0.99 1991,Been A Son,163,1,1,Kurt Cobain,127555,4170369,0.99 1992,Lithium,163,1,1,Kurt Cobain,250017,8148800,0.99 1993,Sliver,163,1,1,Kurt Cobain,116218,3784567,0.99 1994,Spank Thru,163,1,1,Kurt Cobain,190354,6186487,0.99 1995,Scentless Apprentice,163,1,1,Nirvana,211200,6898177,0.99 1996,Heart-Shaped Box,163,1,1,Kurt Cobain,281887,9210982,0.99 1997,Milk It,163,1,1,Kurt Cobain,225724,7406945,0.99 1998,Negative Creep,163,1,1,Kurt Cobain,163761,5354854,0.99 1999,Polly,163,1,1,Kurt Cobain,149995,4885331,0.99 2000,Breed,163,1,1,Kurt Cobain,208378,6759080,0.99 2001,Tourette's,163,1,1,Kurt Cobain,115591,3753246,0.99 2002,Blew,163,1,1,Kurt Cobain,216346,7096936,0.99 2003,Smells Like Teen Spirit,164,1,1,Kurt Cobain,301296,9823847,0.99 2004,In Bloom,164,1,1,Kurt Cobain,254928,8327077,0.99 2005,Come As You Are,164,1,1,Kurt Cobain,219219,7123357,0.99 2006,Breed,164,1,1,Kurt Cobain,183928,5984812,0.99 2007,Lithium,164,1,1,Kurt Cobain,256992,8404745,0.99 2008,Polly,164,1,1,Kurt Cobain,177031,5788407,0.99 2009,Territorial Pissings,164,1,1,Kurt Cobain,143281,4613880,0.99 2010,Drain You,164,1,1,Kurt Cobain,223973,7273440,0.99 2011,Lounge Act,164,1,1,Kurt Cobain,156786,5093635,0.99 2012,Stay Away,164,1,1,Kurt Cobain,212636,6956404,0.99 2013,On A Plain,164,1,1,Kurt Cobain,196440,6390635,0.99 2014,Something In The Way,164,1,1,Kurt Cobain,230556,7472168,0.99 2015,Time,165,1,1,,96888,3124455,0.99 2016,P.S.Apareça,165,1,1,,209188,6842244,0.99 2017,Sangue Latino,165,1,1,,223033,7354184,0.99 2018,Folhas Secas,165,1,1,,161253,5284522,0.99 2019,Poeira,165,1,1,,267075,8784141,0.99 2020,Mágica,165,1,1,,233743,7627348,0.99 2021,Quem Mata A Mulher Mata O Melhor,165,1,1,,262791,8640121,0.99 2022,Mundaréu,165,1,1,,217521,7158975,0.99 2023,O Braço Da Minha Guitarra,165,1,1,,258351,8469531,0.99 2024,Deus,165,1,1,,284160,9188110,0.99 2025,Mãe Terra,165,1,1,,306625,9949269,0.99 2026,Às Vezes,165,1,1,,330292,10706614,0.99 2027,Menino De Rua,165,1,1,,329795,10784595,0.99 2028,Prazer E Fé,165,1,1,,214831,7031383,0.99 2029,Elza,165,1,1,,199105,6517629,0.99 2030,Requebra,166,1,7,,240744,8010811,0.99 2031,Nossa Gente (Avisa Là),166,1,7,,188212,6233201,0.99 2032,Olodum - Alegria Geral,166,1,7,,233404,7754245,0.99 2033,Madagáscar Olodum,166,1,7,,252264,8270584,0.99 2034,Faraó Divindade Do Egito,166,1,7,,228571,7523278,0.99 2035,Todo Amor (Asas Da Liberdade),166,1,7,,245133,8121434,0.99 2036,Denúncia,166,1,7,,159555,5327433,0.99 2037,"Olodum, A Banda Do Pelô",166,1,7,,146599,4900121,0.99 2038,Cartao Postal,166,1,7,,211565,7082301,0.99 2039,Jeito Faceiro,166,1,7,,217286,7233608,0.99 2040,Revolta Olodum,166,1,7,,230191,7557065,0.99 2041,Reggae Odoyá,166,1,7,,224470,7499807,0.99 2042,Protesto Do Olodum (Ao Vivo),166,1,7,,206001,6766104,0.99 2043,Olodum - Smile (Instrumental),166,1,7,,235833,7871409,0.99 2044,Vulcão Dub - Fui Eu,167,1,7,Bi Ribeira/Herbert Vianna/João Barone,287059,9495202,0.99 2045,O Trem Da Juventude,167,1,7,Herbert Vianna,225880,7507655,0.99 2046,Manguetown,167,1,7,Chico Science/Dengue/Lúcio Maia,162925,5382018,0.99 2047,"Um Amor, Um Lugar",167,1,7,Herbert Vianna,184555,6090334,0.99 2048,Bora-Bora,167,1,7,Herbert Vianna,182987,6036046,0.99 2049,Vai Valer,167,1,7,Herbert Vianna,206524,6899778,0.99 2050,I Feel Good (I Got You) - Sossego,167,1,7,James Brown/Tim Maia,244976,8091302,0.99 2051,Uns Dias,167,1,7,Herbert Vianna,240796,7931552,0.99 2052,Sincero Breu,167,1,7,C. A./C.A./Celso Alvim/Herbert Vianna/Mário Moura/Pedro Luís/Sidon Silva,208013,6921669,0.99 2053,Meu Erro,167,1,7,Herbert Vianna,188577,6192791,0.99 2054,Selvagem,167,1,7,Bi Ribeiro/Herbert Vianna/João Barone,148558,4942831,0.99 2055,Brasília 5:31,167,1,7,Herbert Vianna,178337,5857116,0.99 2056,Tendo A Lua,167,1,7,Herbert Vianna/Tet Tillett,198922,6568180,0.99 2057,Que País É Este,167,1,7,Renato Russo,216685,7137865,0.99 2058,Navegar Impreciso,167,1,7,Herbert Vianna,262870,8761283,0.99 2059,Feira Moderna,167,1,7,Beto Guedes/Fernando Brant/L Borges,182517,6001793,0.99 2060,Tequila - Lourinha Bombril (Parate Y Mira),167,1,7,Bahiano/Chuck Rio/Diego Blanco/Herbert Vianna,255738,8514961,0.99 2061,Vamo Batê Lata,167,1,7,Herbert Vianna,228754,7585707,0.99 2062,Life During Wartime,167,1,7,Chris Frantz/David Byrne/Jerry Harrison/Tina Weymouth,259186,8543439,0.99 2063,Nebulosa Do Amor,167,1,7,Herbert Vianna,203415,6732496,0.99 2064,Caleidoscópio,167,1,7,Herbert Vianna,256522,8484597,0.99 2065,Trac Trac,168,1,7,Fito Paez/Herbert Vianna,231653,7638256,0.99 2066,Tendo A Lua,168,1,7,Herbert Vianna/Tetê Tillet,219585,7342776,0.99 2067,Mensagen De Amor (2000),168,1,7,Herbert Vianna,183588,6061324,0.99 2068,Lourinha Bombril,168,1,7,Bahiano/Diego Blanco/Herbert Vianna,159895,5301882,0.99 2069,La Bella Luna,168,1,7,Herbert Vianna,192653,6428598,0.99 2070,Busca Vida,168,1,7,Herbert Vianna,176431,5798663,0.99 2071,Uma Brasileira,168,1,7,Carlinhos Brown/Herbert Vianna,217573,7280574,0.99 2072,Luis Inacio (300 Picaretas),168,1,7,Herbert Vianna,198191,6576790,0.99 2073,Saber Amar,168,1,7,Herbert Vianna,202788,6723733,0.99 2074,Ela Disse Adeus,168,1,7,Herbert Vianna,226298,7608999,0.99 2075,O Amor Nao Sabe Esperar,168,1,7,Herbert Vianna,241084,8042534,0.99 2076,Aonde Quer Que Eu Va,168,1,7,Herbert Vianna/Paulo Sérgio Valle,258089,8470121,0.99 2077,Caleidoscópio,169,1,7,,211330,7000017,0.99 2078,Óculos,169,1,7,,219271,7262419,0.99 2079,Cinema Mudo,169,1,7,,227918,7612168,0.99 2080,Alagados,169,1,7,,302393,10255463,0.99 2081,Lanterna Dos Afogados,169,1,7,,190197,6264318,0.99 2082,Melô Do Marinheiro,169,1,7,,208352,6905668,0.99 2083,Vital E Sua Moto,169,1,7,,210207,6902878,0.99 2084,O Beco,169,1,7,,189178,6293184,0.99 2085,Meu Erro,169,1,7,,208431,6893533,0.99 2086,Perplexo,169,1,7,,161175,5355013,0.99 2087,Me Liga,169,1,7,,229590,7565912,0.99 2088,Quase Um Segundo,169,1,7,,275644,8971355,0.99 2089,Selvagem,169,1,7,,245890,8141084,0.99 2090,Romance Ideal,169,1,7,,250070,8260477,0.99 2091,Será Que Vai Chover?,169,1,7,,337057,11133830,0.99 2092,SKA,169,1,7,,148871,4943540,0.99 2093,Bark at the Moon,170,2,1,O. Osbourne,257252,4601224,0.99 2094,I Don't Know,171,2,1,"B. Daisley, O. Osbourne & R. Rhoads",312980,5525339,0.99 2095,Crazy Train,171,2,1,"B. Daisley, O. Osbourne & R. Rhoads",295960,5255083,0.99 2096,Flying High Again,172,2,1,"L. Kerslake, O. Osbourne, R. Daisley & R. Rhoads",290851,5179599,0.99 2097,"Mama, I'm Coming Home",173,2,1,"L. Kilmister, O. Osbourne & Z. Wylde",251586,4302390,0.99 2098,No More Tears,173,2,1,"J. Purdell, M. Inez, O. Osbourne, R. Castillo & Z. Wylde",444358,7362964,0.99 2099,I Don't Know,174,1,3,"O. Osbourne, R. Daisley, R. Rhoads",283088,9207869,0.99 2100,Crazy Train,174,1,3,"O. Osbourne, R. Daisley, R. Rhoads",322716,10517408,0.99 2101,Believer,174,1,3,"O. Osbourne, R. Daisley, R. Rhoads",308897,10003794,0.99 2102,Mr. Crowley,174,1,3,"O. Osbourne, R. Daisley, R. Rhoads",344241,11184130,0.99 2103,Flying High Again,174,1,3,"O. Osbourne, R. Daisley, R. Rhoads, L. Kerslake",261224,8481822,0.99 2104,Relvelation (Mother Earth),174,1,3,"O. Osbourne, R. Daisley, R. Rhoads",349440,11367866,0.99 2105,Steal Away (The Night),174,1,3,"O. Osbourne, R. Daisley, R. Rhoads",485720,15945806,0.99 2106,Suicide Solution (With Guitar Solo),174,1,3,"O. Osbourne, R. Daisley, R. Rhoads",467069,15119938,0.99 2107,Iron Man,174,1,3,"A. F. Iommi, W. Ward, T. Butler, J. Osbourne",172120,5609799,0.99 2108,Children Of The Grave,174,1,3,"A. F. Iommi, W. Ward, T. Butler, J. Osbourne",357067,11626740,0.99 2109,Paranoid,174,1,3,"A. F. Iommi, W. Ward, T. Butler, J. Osbourne",176352,5729813,0.99 2110,Goodbye To Romance,174,1,3,"O. Osbourne, R. Daisley, R. Rhoads",334393,10841337,0.99 2111,No Bone Movies,174,1,3,"O. Osbourne, R. Daisley, R. Rhoads",249208,8095199,0.99 2112,Dee,174,1,3,R. Rhoads,261302,8555963,0.99 2113,Shining In The Light,175,1,1,"Jimmy Page, Robert Plant, Charlie Jones, Michael Lee",240796,7951688,0.99 2114,When The World Was Young,175,1,1,"Jimmy Page, Robert Plant, Charlie Jones, Michael Lee",373394,12198930,0.99 2115,Upon A Golden Horse,175,1,1,"Jimmy Page, Robert Plant, Charlie Jones, Michael Lee",232359,7594829,0.99 2116,Blue Train,175,1,1,"Jimmy Page, Robert Plant, Charlie Jones, Michael Lee",405028,13170391,0.99 2117,Please Read The Letter,175,1,1,"Jimmy Page, Robert Plant, Charlie Jones, Michael Lee",262112,8603372,0.99 2118,Most High,175,1,1,"Jimmy Page, Robert Plant, Charlie Jones, Michael Lee",336535,10999203,0.99 2119,Heart In Your Hand,175,1,1,"Jimmy Page, Robert Plant, Charlie Jones, Michael Lee",230896,7598019,0.99 2120,Walking Into Clarksdale,175,1,1,"Jimmy Page, Robert Plant, Charlie Jones, Michael Lee",318511,10396315,0.99 2121,Burning Up,175,1,1,"Jimmy Page, Robert Plant, Charlie Jones, Michael Lee",321619,10525136,0.99 2122,When I Was A Child,175,1,1,"Jimmy Page, Robert Plant, Charlie Jones, Michael Lee",345626,11249456,0.99 2123,House Of Love,175,1,1,"Jimmy Page, Robert Plant, Charlie Jones, Michael Lee",335699,10990880,0.99 2124,Sons Of Freedom,175,1,1,"Jimmy Page, Robert Plant, Charlie Jones, Michael Lee",246465,8087944,0.99 2125,United Colours,176,1,10,"Brian Eno, Bono, Adam Clayton, The Edge & Larry Mullen Jnr.",330266,10939131,0.99 2126,Slug,176,1,10,"Brian Eno, Bono, Adam Clayton, The Edge & Larry Mullen Jnr.",281469,9295950,0.99 2127,Your Blue Room,176,1,10,"Brian Eno, Bono, Adam Clayton, The Edge & Larry Mullen Jnr.",328228,10867860,0.99 2128,Always Forever Now,176,1,10,"Brian Eno, Bono, Adam Clayton, The Edge & Larry Mullen Jnr.",383764,12727928,0.99 2129,A Different Kind Of Blue,176,1,10,"Brian Eno, Bono, Adam Clayton, The Edge & Larry Mullen Jnr.",120816,3884133,0.99 2130,Beach Sequence,176,1,10,"Brian Eno, Bono, Adam Clayton, The Edge & Larry Mullen Jnr.",212297,6928259,0.99 2131,Miss Sarajevo,176,1,10,"Brian Eno, Bono, Adam Clayton, The Edge & Larry Mullen Jnr.",340767,11064884,0.99 2132,Ito Okashi,176,1,10,"Brian Eno, Bono, Adam Clayton, The Edge & Larry Mullen Jnr.",205087,6572813,0.99 2133,One Minute Warning,176,1,10,"Brian Eno, Bono, Adam Clayton, The Edge & Larry Mullen Jnr.",279693,9335453,0.99 2134,Corpse (These Chains Are Way Too Long),176,1,10,"Brian Eno, Bono, Adam Clayton, The Edge & Larry Mullen Jnr.",214909,6920451,0.99 2135,Elvis Ate America,176,1,10,"Brian Eno, Bono, Adam Clayton, The Edge & Larry Mullen Jnr.",180166,5851053,0.99 2136,Plot 180,176,1,10,"Brian Eno, Bono, Adam Clayton, The Edge & Larry Mullen Jnr.",221596,7253729,0.99 2137,Theme From The Swan,176,1,10,"Brian Eno, Bono, Adam Clayton, The Edge & Larry Mullen Jnr.",203911,6638076,0.99 2138,Theme From Let's Go Native,176,1,10,"Brian Eno, Bono, Adam Clayton, The Edge & Larry Mullen Jnr.",186723,6179777,0.99 2139,Wrathchild,177,1,1,Steve Harris,170396,5499390,0.99 2140,Killers,177,1,1,Paul Di'Anno/Steve Harris,309995,10009697,0.99 2141,Prowler,177,1,1,Steve Harris,240274,7782963,0.99 2142,Murders In The Rue Morgue,177,1,1,Steve Harris,258638,8360999,0.99 2143,Women In Uniform,177,1,1,Greg Macainsh,189936,6139651,0.99 2144,Remember Tomorrow,177,1,1,Paul Di'Anno/Steve Harris,326426,10577976,0.99 2145,Sanctuary,177,1,1,David Murray/Paul Di'Anno/Steve Harris,198844,6423543,0.99 2146,Running Free,177,1,1,Paul Di'Anno/Steve Harris,199706,6483496,0.99 2147,Phantom Of The Opera,177,1,1,Steve Harris,418168,13585530,0.99 2148,Iron Maiden,177,1,1,Steve Harris,235232,7600077,0.99 2149,Corduroy,178,1,1,Pearl Jam & Eddie Vedder,305293,9991106,0.99 2150,Given To Fly,178,1,1,Eddie Vedder & Mike McCready,233613,7678347,0.99 2151,"Hail, Hail",178,1,1,Stone Gossard & Eddie Vedder & Jeff Ament & Mike McCready,223764,7364206,0.99 2152,Daughter,178,1,1,Dave Abbruzzese & Jeff Ament & Stone Gossard & Mike McCready & Eddie Vedder,407484,13420697,0.99 2153,Elderly Woman Behind The Counter In A Small Town,178,1,1,Dave Abbruzzese & Jeff Ament & Stone Gossard & Mike McCready & Eddie Vedder,229328,7509304,0.99 2154,Untitled,178,1,1,Pearl Jam,122801,3957141,0.99 2155,MFC,178,1,1,Eddie Vedder,148192,4817665,0.99 2156,Go,178,1,1,Dave Abbruzzese & Jeff Ament & Stone Gossard & Mike McCready & Eddie Vedder,161541,5290810,0.99 2157,Red Mosquito,178,1,1,Jeff Ament & Stone Gossard & Jack Irons & Mike McCready & Eddie Vedder,242991,7944923,0.99 2158,Even Flow,178,1,1,Stone Gossard & Eddie Vedder,317100,10394239,0.99 2159,Off He Goes,178,1,1,Eddie Vedder,343222,11245109,0.99 2160,Nothingman,178,1,1,Jeff Ament & Eddie Vedder,278595,9107017,0.99 2161,Do The Evolution,178,1,1,Eddie Vedder & Stone Gossard,225462,7377286,0.99 2162,Better Man,178,1,1,Eddie Vedder,246204,8019563,0.99 2163,Black,178,1,1,Stone Gossard & Eddie Vedder,415712,13580009,0.99 2164,F*Ckin' Up,178,1,1,Neil Young,377652,12360893,0.99 2165,Life Wasted,179,1,4,Stone Gossard,234344,7610169,0.99 2166,World Wide Suicide,179,1,4,Eddie Vedder,209188,6885908,0.99 2167,Comatose,179,1,4,Mike McCready & Stone Gossard,139990,4574516,0.99 2168,Severed Hand,179,1,4,Eddie Vedder,270341,8817438,0.99 2169,Marker In The Sand,179,1,4,Mike McCready,263235,8656578,0.99 2170,Parachutes,179,1,4,Stone Gossard,216555,7074973,0.99 2171,Unemployable,179,1,4,Matt Cameron & Mike McCready,184398,6066542,0.99 2172,Big Wave,179,1,4,Jeff Ament,178573,5858788,0.99 2173,Gone,179,1,4,Eddie Vedder,249547,8158204,0.99 2174,Wasted Reprise,179,1,4,Stone Gossard,53733,1731020,0.99 2175,Army Reserve,179,1,4,Jeff Ament,225567,7393771,0.99 2176,Come Back,179,1,4,Eddie Vedder & Mike McCready,329743,10768701,0.99 2177,Inside Job,179,1,4,Eddie Vedder & Mike McCready,428643,14006924,0.99 2178,Can't Keep,180,1,1,Eddie Vedder,219428,7215713,0.99 2179,Save You,180,1,1,Eddie Vedder/Jeff Ament/Matt Cameron/Mike McCready/Stone Gossard,230112,7609110,0.99 2180,Love Boat Captain,180,1,1,Eddie Vedder,276453,9016789,0.99 2181,Cropduster,180,1,1,Matt Cameron,231888,7588928,0.99 2182,Ghost,180,1,1,Jeff Ament,195108,6383772,0.99 2183,I Am Mine,180,1,1,Eddie Vedder,215719,7086901,0.99 2184,Thumbing My Way,180,1,1,Eddie Vedder,250226,8201437,0.99 2185,You Are,180,1,1,Matt Cameron,270863,8938409,0.99 2186,Get Right,180,1,1,Matt Cameron,158589,5223345,0.99 2187,Green Disease,180,1,1,Eddie Vedder,161253,5375818,0.99 2188,Help Help,180,1,1,Jeff Ament,215092,7033002,0.99 2189,Bushleager,180,1,1,Stone Gossard,237479,7849757,0.99 2190,1/2 Full,180,1,1,Jeff Ament,251010,8197219,0.99 2191,Arc,180,1,1,Pearl Jam,65593,2099421,0.99 2192,All or None,180,1,1,Stone Gossard,277655,9104728,0.99 2193,Once,181,1,1,Stone Gossard,231758,7561555,0.99 2194,Evenflow,181,1,1,Stone Gossard,293720,9622017,0.99 2195,Alive,181,1,1,Stone Gossard,341080,11176623,0.99 2196,Why Go,181,1,1,Jeff Ament,200254,6539287,0.99 2197,Black,181,1,1,Dave Krusen/Stone Gossard,343823,11213314,0.99 2198,Jeremy,181,1,1,Jeff Ament,318981,10447222,0.99 2199,Oceans,181,1,1,Jeff Ament/Stone Gossard,162194,5282368,0.99 2200,Porch,181,1,1,Eddie Vedder,210520,6877475,0.99 2201,Garden,181,1,1,Jeff Ament/Stone Gossard,299154,9740738,0.99 2202,Deep,181,1,1,Jeff Ament/Stone Gossard,258324,8432497,0.99 2203,Release,181,1,1,Jeff Ament/Mike McCready/Stone Gossard,546063,17802673,0.99 2204,Go,182,1,1,Dave Abbruzzese/Eddie Vedder/Jeff Ament/Mike McCready/Stone Gossard,193123,6351920,0.99 2205,Animal,182,1,1,Dave Abbruzzese/Eddie Vedder/Jeff Ament/Mike McCready/Stone Gossard,169325,5503459,0.99 2206,Daughter,182,1,1,Dave Abbruzzese/Eddie Vedder/Jeff Ament/Mike McCready/Stone Gossard,235598,7824586,0.99 2207,Glorified G,182,1,1,Dave Abbruzzese/Eddie Vedder/Jeff Ament/Mike McCready/Stone Gossard,206968,6772116,0.99 2208,Dissident,182,1,1,Dave Abbruzzese/Eddie Vedder/Jeff Ament/Mike McCready/Stone Gossard,215510,7034500,0.99 2209,W.M.A.,182,1,1,Dave Abbruzzese/Eddie Vedder/Jeff Ament/Mike McCready/Stone Gossard,359262,12037261,0.99 2210,Blood,182,1,1,Dave Abbruzzese/Eddie Vedder/Jeff Ament/Mike McCready/Stone Gossard,170631,5551478,0.99 2211,Rearviewmirror,182,1,1,Dave Abbruzzese/Eddie Vedder/Jeff Ament/Mike McCready/Stone Gossard,284186,9321053,0.99 2212,Rats,182,1,1,Dave Abbruzzese/Eddie Vedder/Jeff Ament/Mike McCready/Stone Gossard,255425,8341934,0.99 2213,Elderly Woman Behind The Counter In A Small Town,182,1,1,Dave Abbruzzese/Eddie Vedder/Jeff Ament/Mike McCready/Stone Gossard,196336,6499398,0.99 2214,Leash,182,1,1,Dave Abbruzzese/Eddie Vedder/Jeff Ament/Mike McCready/Stone Gossard,189257,6191560,0.99 2215,Indifference,182,1,1,Dave Abbruzzese/Eddie Vedder/Jeff Ament/Mike McCready/Stone Gossard,302053,9756133,0.99 2216,Johnny B. Goode,141,1,8,,243200,8092024,0.99 2217,Don't Look Back,141,1,8,,221100,7344023,0.99 2218,Jah Seh No,141,1,8,,276871,9134476,0.99 2219,I'm The Toughest,141,1,8,,230191,7657594,0.99 2220,Nothing But Love,141,1,8,,221570,7335228,0.99 2221,Buk-In-Hamm Palace,141,1,8,,265665,8964369,0.99 2222,Bush Doctor,141,1,8,,239751,7942299,0.99 2223,Wanted Dread And Alive,141,1,8,,260310,8670933,0.99 2224,Mystic Man,141,1,8,,353671,11812170,0.99 2225,Coming In Hot,141,1,8,,213054,7109414,0.99 2226,Pick Myself Up,141,1,8,,234684,7788255,0.99 2227,Crystal Ball,141,1,8,,309733,10319296,0.99 2228,Equal Rights Downpresser Man,141,1,8,,366733,12086524,0.99 2229,Speak To Me/Breathe,183,1,1,"Mason/Waters, Gilmour, Wright",234213,7631305,0.99 2230,On The Run,183,1,1,"Gilmour, Waters",214595,7206300,0.99 2231,Time,183,1,1,"Mason, Waters, Wright, Gilmour",425195,13955426,0.99 2232,The Great Gig In The Sky,183,1,1,"Wright, Waters",284055,9147563,0.99 2233,Money,183,1,1,Waters,391888,12930070,0.99 2234,Us And Them,183,1,1,"Waters, Wright",461035,15000299,0.99 2235,Any Colour You Like,183,1,1,"Gilmour, Mason, Wright, Waters",205740,6707989,0.99 2236,Brain Damage,183,1,1,Waters,230556,7497655,0.99 2237,Eclipse,183,1,1,Waters,125361,4065299,0.99 2238,ZeroVinteUm,184,1,17,,315637,10426550,0.99 2239,Queimando Tudo,184,1,17,,172591,5723677,0.99 2240,Hip Hop Rio,184,1,17,,151536,4991935,0.99 2241,Bossa,184,1,17,,29048,967098,0.99 2242,100% HardCore,184,1,17,,165146,5407744,0.99 2243,Biruta,184,1,17,,213263,7108200,0.99 2244,Mão Na Cabeça,184,1,17,,202631,6642753,0.99 2245,O Bicho Tá Pregando,184,1,17,,171964,5683369,0.99 2246,Adoled (Ocean),184,1,17,,185103,6009946,0.99 2247,Seus Amigos,184,1,17,,100858,3304738,0.99 2248,Paga Pau,184,1,17,,197485,6529041,0.99 2249,Rappers Reais,184,1,17,,202004,6684160,0.99 2250,Nega Do Cabelo Duro,184,1,17,,121808,4116536,0.99 2251,Hemp Family,184,1,17,,205923,6806900,0.99 2252,Quem Me Cobrou?,184,1,17,,121704,3947664,0.99 2253,Se Liga,184,1,17,,410409,13559173,0.99 2254,Bohemian Rhapsody,185,1,1,"Mercury, Freddie",358948,11619868,0.99 2255,Another One Bites The Dust,185,1,1,"Deacon, John",216946,7172355,0.99 2256,Killer Queen,185,1,1,"Mercury, Freddie",182099,5967749,0.99 2257,Fat Bottomed Girls,185,1,1,"May, Brian",204695,6630041,0.99 2258,Bicycle Race,185,1,1,"Mercury, Freddie",183823,6012409,0.99 2259,You're My Best Friend,185,1,1,"Deacon, John",172225,5602173,0.99 2260,Don't Stop Me Now,185,1,1,"Mercury, Freddie",211826,6896666,0.99 2261,Save Me,185,1,1,"May, Brian",228832,7444624,0.99 2262,Crazy Little Thing Called Love,185,1,1,"Mercury, Freddie",164231,5435501,0.99 2263,Somebody To Love,185,1,1,"Mercury, Freddie",297351,9650520,0.99 2264,Now I'm Here,185,1,1,"May, Brian",255346,8328312,0.99 2265,Good Old-Fashioned Lover Boy,185,1,1,"Mercury, Freddie",175960,5747506,0.99 2266,Play The Game,185,1,1,"Mercury, Freddie",213368,6915832,0.99 2267,Flash,185,1,1,"May, Brian",168489,5464986,0.99 2268,Seven Seas Of Rhye,185,1,1,"Mercury, Freddie",170553,5539957,0.99 2269,We Will Rock You,185,1,1,"Deacon, John/May, Brian",122880,4026955,0.99 2270,We Are The Champions,185,1,1,"Mercury, Freddie",180950,5880231,0.99 2271,We Will Rock You,186,1,1,May,122671,4026815,0.99 2272,We Are The Champions,186,1,1,Mercury,182883,5939794,0.99 2273,Sheer Heart Attack,186,1,1,Taylor,207386,6642685,0.99 2274,"All Dead, All Dead",186,1,1,May,190119,6144878,0.99 2275,Spread Your Wings,186,1,1,Deacon,275356,8936992,0.99 2276,Fight From The Inside,186,1,1,Taylor,184737,6078001,0.99 2277,"Get Down, Make Love",186,1,1,Mercury,231235,7509333,0.99 2278,Sleep On The Sidewalk,186,1,1,May,187428,6099840,0.99 2279,Who Needs You,186,1,1,Deacon,186958,6292969,0.99 2280,It's Late,186,1,1,May,386194,12519388,0.99 2281,My Melancholy Blues,186,1,1,Mercury,206471,6691838,0.99 2282,Shiny Happy People,187,1,4,Bill Berry/Michael Stipe/Mike Mills/Peter Buck,226298,7475323,0.99 2283,Me In Honey,187,1,4,Bill Berry/Michael Stipe/Mike Mills/Peter Buck,246674,8194751,0.99 2284,Radio Song,187,1,4,Bill Berry/Michael Stipe/Mike Mills/Peter Buck,255477,8421172,0.99 2285,Pop Song 89,188,1,4,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,185730,6132218,0.99 2286,Get Up,188,1,4,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,160235,5264376,0.99 2287,You Are The Everything,188,1,4,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,226298,7373181,0.99 2288,Stand,188,1,4,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,192862,6349090,0.99 2289,World Leader Pretend,188,1,4,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,259761,8537282,0.99 2290,The Wrong Child,188,1,4,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,216633,7065060,0.99 2291,Orange Crush,188,1,4,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,231706,7742894,0.99 2292,Turn You Inside-Out,188,1,4,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,257358,8395671,0.99 2293,Hairshirt,188,1,4,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,235911,7753807,0.99 2294,I Remember California,188,1,4,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,304013,9950311,0.99 2295,Untitled,188,1,4,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,191503,6332426,0.99 2296,How The West Was Won And Where It Got Us,189,1,1,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,271151,8994291,0.99 2297,The Wake-Up Bomb,189,1,1,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,308532,10077337,0.99 2298,New Test Leper,189,1,1,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,326791,10866447,0.99 2299,Undertow,189,1,1,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,309498,10131005,0.99 2300,E-Bow The Letter,189,1,1,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,324963,10714576,0.99 2301,Leave,189,1,1,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,437968,14433365,0.99 2302,Departure,189,1,1,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,209423,6818425,0.99 2303,Bittersweet Me,189,1,1,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,245812,8114718,0.99 2304,Be Mine,189,1,1,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,333087,10790541,0.99 2305,Binky The Doormat,189,1,1,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,301688,9950320,0.99 2306,Zither,189,1,1,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,154148,5032962,0.99 2307,"So Fast, So Numb",189,1,1,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,252682,8341223,0.99 2308,Low Desert,189,1,1,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,212062,6989288,0.99 2309,Electrolite,189,1,1,Bill Berry-Peter Buck-Mike Mills-Michael Stipe,245315,8051199,0.99 2310,Losing My Religion,187,1,4,Bill Berry/Michael Stipe/Mike Mills/Peter Buck,269035,8885672,0.99 2311,Low,187,1,4,Bill Berry/Michael Stipe/Mike Mills/Peter Buck,296777,9633860,0.99 2312,Near Wild Heaven,187,1,4,Bill Berry/Michael Stipe/Mike Mills/Peter Buck,199862,6610009,0.99 2313,Endgame,187,1,4,Bill Berry/Michael Stipe/Mike Mills/Peter Buck,230687,7664479,0.99 2314,Belong,187,1,4,Bill Berry/Michael Stipe/Mike Mills/Peter Buck,247013,8219375,0.99 2315,Half A World Away,187,1,4,Bill Berry/Michael Stipe/Mike Mills/Peter Buck,208431,6837283,0.99 2316,Texarkana,187,1,4,Bill Berry/Michael Stipe/Mike Mills/Peter Buck,220081,7260681,0.99 2317,Country Feedback,187,1,4,Bill Berry/Michael Stipe/Mike Mills/Peter Buck,249782,8178943,0.99 2318,Carnival Of Sorts,190,1,4,R.E.M.,233482,7669658,0.99 2319,Radio Free Aurope,190,1,4,R.E.M.,245315,8163490,0.99 2320,Perfect Circle,190,1,4,R.E.M.,208509,6898067,0.99 2321,Talk About The Passion,190,1,4,R.E.M.,203206,6725435,0.99 2322,So Central Rain,190,1,4,R.E.M.,194768,6414550,0.99 2323,Don't Go Back To Rockville,190,1,4,R.E.M.,272352,9010715,0.99 2324,Pretty Persuasion,190,1,4,R.E.M.,229929,7577754,0.99 2325,Green Grow The Rushes,190,1,4,R.E.M.,225671,7422425,0.99 2326,Can't Get There From Here,190,1,4,R.E.M.,220630,7285936,0.99 2327,Driver 8,190,1,4,R.E.M.,204747,6779076,0.99 2328,Fall On Me,190,1,4,R.E.M.,172016,5676811,0.99 2329,I Believe,190,1,4,R.E.M.,227709,7542929,0.99 2330,Cuyahoga,190,1,4,R.E.M.,260623,8591057,0.99 2331,The One I Love,190,1,4,R.E.M.,197355,6495125,0.99 2332,The Finest Worksong,190,1,4,R.E.M.,229276,7574856,0.99 2333,It's The End Of The World As We Know It (And I Feel Fine),190,1,4,R.E.M.,244819,7998987,0.99 2334,Infeliz Natal,191,1,4,Rodolfo,138266,4503299,0.99 2335,A Sua,191,1,4,Rodolfo,142132,4622064,0.99 2336,Papeau Nuky Doe,191,1,4,Rodolfo,121652,3995022,0.99 2337,Merry Christmas,191,1,4,Rodolfo,126040,4166652,0.99 2338,Bodies,191,1,4,Rodolfo,180035,5873778,0.99 2339,Puteiro Em João Pessoa,191,1,4,Rodolfo,195578,6395490,0.99 2340,Esporrei Na Manivela,191,1,4,Rodolfo,293276,9618499,0.99 2341,Bê-a-Bá,191,1,4,Rodolfo,249051,8130636,0.99 2342,Cajueiro,191,1,4,Rodolfo,158589,5164837,0.99 2343,Palhas Do Coqueiro,191,1,4,Rodolfo,133851,4396466,0.99 2344,Maluco Beleza,192,1,1,,203206,6628067,0.99 2345,O Dia Em Que A Terra Parou,192,1,1,,261720,8586678,0.99 2346,No Fundo Do Quintal Da Escola,192,1,1,,177606,5836953,0.99 2347,O Segredo Do Universo,192,1,1,,192679,6315187,0.99 2348,As Profecias,192,1,1,,232515,7657732,0.99 2349,Mata Virgem,192,1,1,,142602,4690029,0.99 2350,Sapato 36,192,1,1,,196702,6507301,0.99 2351,Todo Mundo Explica,192,1,1,,134896,4449772,0.99 2352,Que Luz É Essa,192,1,1,,165067,5620058,0.99 2353,Diamante De Mendigo,192,1,1,,206053,6775101,0.99 2354,Negócio É,192,1,1,,175464,5826775,0.99 2355,"Muita Estrela, Pouca Constelação",192,1,1,,268068,8781021,0.99 2356,Século XXI,192,1,1,,244897,8040563,0.99 2357,Rock Das Aranhas (Ao Vivo) (Live),192,1,1,,231836,7591945,0.99 2358,The Power Of Equality,193,1,4,Anthony Kiedis/Chad Smith/Flea/John Frusciante,243591,8148266,0.99 2359,If You Have To Ask,193,1,4,Anthony Kiedis/Chad Smith/Flea/John Frusciante,216790,7199175,0.99 2360,Breaking The Girl,193,1,4,Anthony Kiedis/Chad Smith/Flea/John Frusciante,295497,9805526,0.99 2361,Funky Monks,193,1,4,Anthony Kiedis/Chad Smith/Flea/John Frusciante,323395,10708168,0.99 2362,Suck My Kiss,193,1,4,Anthony Kiedis/Chad Smith/Flea/John Frusciante,217234,7129137,0.99 2363,I Could Have Lied,193,1,4,Anthony Kiedis/Chad Smith/Flea/John Frusciante,244506,8088244,0.99 2364,Mellowship Slinky In B Major,193,1,4,Anthony Kiedis/Chad Smith/Flea/John Frusciante,240091,7971384,0.99 2365,The Righteous & The Wicked,193,1,4,Anthony Kiedis/Chad Smith/Flea/John Frusciante,248084,8134096,0.99 2366,Give It Away,193,1,4,Anthony Kiedis/Chad Smith/Flea/John Frusciante,283010,9308997,0.99 2367,Blood Sugar Sex Magik,193,1,4,Anthony Kiedis/Chad Smith/Flea/John Frusciante,271229,8940573,0.99 2368,Under The Bridge,193,1,4,Anthony Kiedis/Chad Smith/Flea/John Frusciante,264359,8682716,0.99 2369,Naked In The Rain,193,1,4,Anthony Kiedis/Chad Smith/Flea/John Frusciante,265717,8724674,0.99 2370,Apache Rose Peacock,193,1,4,Anthony Kiedis/Chad Smith/Flea/John Frusciante,282226,9312588,0.99 2371,The Greeting Song,193,1,4,Anthony Kiedis/Chad Smith/Flea/John Frusciante,193593,6346507,0.99 2372,My Lovely Man,193,1,4,Anthony Kiedis/Chad Smith/Flea/John Frusciante,279118,9220114,0.99 2373,Sir Psycho Sexy,193,1,4,Anthony Kiedis/Chad Smith/Flea/John Frusciante,496692,16354362,0.99 2374,They're Red Hot,193,1,4,Robert Johnson,71941,2382220,0.99 2375,By The Way,194,1,1,"Anthony Kiedis, Flea, John Frusciante, and Chad Smith",218017,7197430,0.99 2376,Universally Speaking,194,1,1,"Anthony Kiedis, Flea, John Frusciante, and Chad Smith",259213,8501904,0.99 2377,This Is The Place,194,1,1,"Anthony Kiedis, Flea, John Frusciante, and Chad Smith",257906,8469765,0.99 2378,Dosed,194,1,1,"Anthony Kiedis, Flea, John Frusciante, and Chad Smith",312058,10235611,0.99 2379,Don't Forget Me,194,1,1,"Anthony Kiedis, Flea, John Frusciante, and Chad Smith",277995,9107071,0.99 2380,The Zephyr Song,194,1,1,"Anthony Kiedis, Flea, John Frusciante, and Chad Smith",232960,7690312,0.99 2381,Can't Stop,194,1,1,"Anthony Kiedis, Flea, John Frusciante, and Chad Smith",269400,8872479,0.99 2382,I Could Die For You,194,1,1,"Anthony Kiedis, Flea, John Frusciante, and Chad Smith",193906,6333311,0.99 2383,Midnight,194,1,1,"Anthony Kiedis, Flea, John Frusciante, and Chad Smith",295810,9702450,0.99 2384,Throw Away Your Television,194,1,1,"Anthony Kiedis, Flea, John Frusciante, and Chad Smith",224574,7483526,0.99 2385,Cabron,194,1,1,"Anthony Kiedis, Flea, John Frusciante, and Chad Smith",218592,7458864,0.99 2386,Tear,194,1,1,"Anthony Kiedis, Flea, John Frusciante, and Chad Smith",317413,10395500,0.99 2387,On Mercury,194,1,1,"Anthony Kiedis, Flea, John Frusciante, and Chad Smith",208509,6834762,0.99 2388,Minor Thing,194,1,1,"Anthony Kiedis, Flea, John Frusciante, and Chad Smith",217835,7148115,0.99 2389,Warm Tape,194,1,1,"Anthony Kiedis, Flea, John Frusciante, and Chad Smith",256653,8358200,0.99 2390,Venice Queen,194,1,1,"Anthony Kiedis, Flea, John Frusciante, and Chad Smith",369110,12280381,0.99 2391,Around The World,195,1,1,Anthony Kiedis/Chad Smith/Flea/John Frusciante,238837,7859167,0.99 2392,Parallel Universe,195,1,1,Red Hot Chili Peppers,270654,8958519,0.99 2393,Scar Tissue,195,1,1,Red Hot Chili Peppers,217469,7153744,0.99 2394,Otherside,195,1,1,Red Hot Chili Peppers,255973,8357989,0.99 2395,Get On Top,195,1,1,Red Hot Chili Peppers,198164,6587883,0.99 2396,Californication,195,1,1,Red Hot Chili Peppers,321671,10568999,0.99 2397,Easily,195,1,1,Red Hot Chili Peppers,231418,7504534,0.99 2398,Porcelain,195,1,1,Anthony Kiedis/Chad Smith/Flea/John Frusciante,163787,5278793,0.99 2399,Emit Remmus,195,1,1,Red Hot Chili Peppers,240300,7901717,0.99 2400,I Like Dirt,195,1,1,Red Hot Chili Peppers,157727,5225917,0.99 2401,This Velvet Glove,195,1,1,Red Hot Chili Peppers,225280,7480537,0.99 2402,Savior,195,1,1,Anthony Kiedis/Chad Smith/Flea/John Frusciante,292493,9551885,0.99 2403,Purple Stain,195,1,1,Red Hot Chili Peppers,253440,8359971,0.99 2404,Right On Time,195,1,1,Red Hot Chili Peppers,112613,3722219,0.99 2405,Road Trippin',195,1,1,Red Hot Chili Peppers,205635,6685831,0.99 2406,The Spirit Of Radio,196,1,1,Geddy Lee And Alex Lifeson/Geddy Lee And Neil Peart/Rush,299154,9862012,0.99 2407,The Trees,196,1,1,Geddy Lee And Alex Lifeson/Geddy Lee And Neil Peart/Rush,285126,9345473,0.99 2408,Something For Nothing,196,1,1,Geddy Lee And Alex Lifeson/Geddy Lee And Neil Peart/Rush,240770,7898395,0.99 2409,Freewill,196,1,1,Geddy Lee And Alex Lifeson/Geddy Lee And Neil Peart/Rush,324362,10694110,0.99 2410,Xanadu,196,1,1,Geddy Lee And Alex Lifeson/Geddy Lee And Neil Peart/Rush,667428,21753168,0.99 2411,Bastille Day,196,1,1,Geddy Lee And Alex Lifeson/Geddy Lee And Neil Peart/Rush,280528,9264769,0.99 2412,By-Tor And The Snow Dog,196,1,1,Geddy Lee And Alex Lifeson/Geddy Lee And Neil Peart/Rush,519888,17076397,0.99 2413,Anthem,196,1,1,Geddy Lee And Alex Lifeson/Geddy Lee And Neil Peart/Rush,264515,8693343,0.99 2414,Closer To The Heart,196,1,1,Geddy Lee And Alex Lifeson/Geddy Lee And Neil Peart/Rush,175412,5767005,0.99 2415,2112 Overture,196,1,1,Geddy Lee And Alex Lifeson/Geddy Lee And Neil Peart/Rush,272718,8898066,0.99 2416,The Temples Of Syrinx,196,1,1,Geddy Lee And Alex Lifeson/Geddy Lee And Neil Peart/Rush,133459,4360163,0.99 2417,La Villa Strangiato,196,1,1,Geddy Lee And Alex Lifeson/Geddy Lee And Neil Peart/Rush,577488,19137855,0.99 2418,Fly By Night,196,1,1,Geddy Lee And Alex Lifeson/Geddy Lee And Neil Peart/Rush,202318,6683061,0.99 2419,Finding My Way,196,1,1,Geddy Lee And Alex Lifeson/Geddy Lee And Neil Peart/Rush,305528,9985701,0.99 2420,Jingo,197,1,1,M.Babatunde Olantunji,592953,19736495,0.99 2421,El Corazon Manda,197,1,1,E.Weiss,713534,23519583,0.99 2422,La Puesta Del Sol,197,1,1,E.Weiss,628062,20614621,0.99 2423,Persuasion,197,1,1,Carlos Santana,318432,10354751,0.99 2424,As The Years Go by,197,1,1,Albert King,233064,7566829,0.99 2425,Soul Sacrifice,197,1,1,Carlos Santana,296437,9801120,0.99 2426,Fried Neckbones And Home Fries,197,1,1,W.Correa,638563,20939646,0.99 2427,Santana Jam,197,1,1,Carlos Santana,882834,29207100,0.99 2428,Evil Ways,198,1,1,,475402,15289235,0.99 2429,We've Got To Get Together/Jingo,198,1,1,,1070027,34618222,0.99 2430,Rock Me,198,1,1,,94720,3037596,0.99 2431,Just Ain't Good Enough,198,1,1,,850259,27489067,0.99 2432,Funky Piano,198,1,1,,934791,30200730,0.99 2433,The Way You Do To Mer,198,1,1,,618344,20028702,0.99 2434,Holding Back The Years,141,1,1,Mick Hucknall and Neil Moss,270053,8833220,0.99 2435,Money's Too Tight To Mention,141,1,1,John and William Valentine,268408,8861921,0.99 2436,The Right Thing,141,1,1,Mick Hucknall,262687,8624063,0.99 2437,It's Only Love,141,1,1,Jimmy and Vella Cameron,232594,7659017,0.99 2438,A New Flame,141,1,1,Mick Hucknall,237662,7822875,0.99 2439,You've Got It,141,1,1,Mick Hucknall and Lamont Dozier,235232,7712845,0.99 2440,If You Don't Know Me By Now,141,1,1,Kenny Gamble and Leon Huff,206524,6712634,0.99 2441,Stars,141,1,1,Mick Hucknall,248137,8194906,0.99 2442,Something Got Me Started,141,1,1,Mick Hucknall and Fritz McIntyre,239595,7997139,0.99 2443,Thrill Me,141,1,1,Mick Hucknall and Fritz McIntyre,303934,10034711,0.99 2444,Your Mirror,141,1,1,Mick Hucknall,240666,7893821,0.99 2445,For Your Babies,141,1,1,Mick Hucknall,256992,8408803,0.99 2446,So Beautiful,141,1,1,Mick Hucknall,298083,9837832,0.99 2447,Angel,141,1,1,Carolyn Franklin and Sonny Saunders,240561,7880256,0.99 2448,Fairground,141,1,1,Mick Hucknall,263888,8793094,0.99 2449,Água E Fogo,199,1,1,Chico Amaral/Edgard Scandurra/Samuel Rosa,278987,9272272,0.99 2450,Três Lados,199,1,1,Chico Amaral/Samuel Rosa,233665,7699609,0.99 2451,Ela Desapareceu,199,1,1,Chico Amaral/Samuel Rosa,250122,8289200,0.99 2452,Balada Do Amor Inabalável,199,1,1,Fausto Fawcett/Samuel Rosa,240613,8025816,0.99 2453,Canção Noturna,199,1,1,Chico Amaral/Lelo Zanettik,238628,7874774,0.99 2454,Muçulmano,199,1,1,"Leão, Rodrigo F./Samuel Rosa",249600,8270613,0.99 2455,Maquinarama,199,1,1,Chico Amaral/Samuel Rosa,245629,8213710,0.99 2456,Rebelião,199,1,1,Chico Amaral/Samuel Rosa,298527,9817847,0.99 2457,A Última Guerra,199,1,1,"Leão, Rodrigo F./Lô Borges/Samuel Rosa",314723,10480391,0.99 2458,Fica,199,1,1,Chico Amaral/Samuel Rosa,272169,8980972,0.99 2459,Ali,199,1,1,Nando Reis/Samuel Rosa,306390,10110351,0.99 2460,Preto Damião,199,1,1,Chico Amaral/Samuel Rosa,264568,8697658,0.99 2461,É Uma Partida De Futebol,200,1,1,Samuel Rosa,1071,38747,0.99 2462,Eu Disse A Ela,200,1,1,Samuel Rosa,254223,8479463,0.99 2463,Zé Trindade,200,1,1,Samuel Rosa,247954,8331310,0.99 2464,Garota Nacional,200,1,1,Samuel Rosa,317492,10511239,0.99 2465,Tão Seu,200,1,1,Samuel Rosa,243748,8133126,0.99 2466,Sem Terra,200,1,1,Samuel Rosa,279353,9196411,0.99 2467,Os Exilados,200,1,1,Samuel Rosa,245551,8222095,0.99 2468,Um Dia Qualquer,200,1,1,Samuel Rosa,292414,9805570,0.99 2469,Los Pretos,200,1,1,Samuel Rosa,239229,8025667,0.99 2470,Sul Da América,200,1,1,Samuel Rosa,254928,8484871,0.99 2471,Poconé,200,1,1,Samuel Rosa,318406,10771610,0.99 2472,Lucky 13,201,1,4,Billy Corgan,189387,6200617,0.99 2473,Aeroplane Flies High,201,1,4,Billy Corgan,473391,15408329,0.99 2474,Because You Are,201,1,4,Billy Corgan,226403,7405137,0.99 2475,Slow Dawn,201,1,4,Billy Corgan,192339,6269057,0.99 2476,Believe,201,1,4,James Iha,192940,6320652,0.99 2477,My Mistake,201,1,4,Billy Corgan,240901,7843477,0.99 2478,Marquis In Spades,201,1,4,Billy Corgan,192731,6304789,0.99 2479,Here's To The Atom Bomb,201,1,4,Billy Corgan,266893,8763140,0.99 2480,Sparrow,201,1,4,Billy Corgan,176822,5696989,0.99 2481,Waiting,201,1,4,Billy Corgan,228336,7627641,0.99 2482,Saturnine,201,1,4,Billy Corgan,229877,7523502,0.99 2483,Rock On,201,1,4,David Cook,366471,12133825,0.99 2484,Set The Ray To Jerry,201,1,4,Billy Corgan,249364,8215184,0.99 2485,Winterlong,201,1,4,Billy Corgan,299389,9670616,0.99 2486,Soot & Stars,201,1,4,Billy Corgan,399986,12866557,0.99 2487,Blissed & Gone,201,1,4,Billy Corgan,286302,9305998,0.99 2488,Siva,202,1,4,Billy Corgan,261172,8576622,0.99 2489,Rhinocerous,202,1,4,Billy Corgan,353462,11526684,0.99 2490,Drown,202,1,4,Billy Corgan,270497,8883496,0.99 2491,Cherub Rock,202,1,4,Billy Corgan,299389,9786739,0.99 2492,Today,202,1,4,Billy Corgan,202213,6596933,0.99 2493,Disarm,202,1,4,Billy Corgan,198556,6508249,0.99 2494,Landslide,202,1,4,Stevie Nicks,190275,6187754,0.99 2495,Bullet With Butterfly Wings,202,1,4,Billy Corgan,257306,8431747,0.99 2496,1979,202,1,4,Billy Corgan,263653,8728470,0.99 2497,Zero,202,1,4,Billy Corgan,161123,5267176,0.99 2498,"Tonight, Tonight",202,1,4,Billy Corgan,255686,8351543,0.99 2499,Eye,202,1,4,Billy Corgan,294530,9784201,0.99 2500,Ava Adore,202,1,4,Billy Corgan,261433,8590412,0.99 2501,Perfect,202,1,4,Billy Corgan,203023,6734636,0.99 2502,The Everlasting Gaze,202,1,4,Billy Corgan,242155,7844404,0.99 2503,Stand Inside Your Love,202,1,4,Billy Corgan,253753,8270113,0.99 2504,Real Love,202,1,4,Billy Corgan,250697,8025896,0.99 2505,[Untitled],202,1,4,Billy Corgan,231784,7689713,0.99 2506,Nothing To Say,203,1,1,Chris Cornell/Kim Thayil,238027,7744833,0.99 2507,Flower,203,1,1,Chris Cornell/Kim Thayil,208822,6830732,0.99 2508,Loud Love,203,1,1,Chris Cornell,297456,9660953,0.99 2509,Hands All Over,203,1,1,Chris Cornell/Kim Thayil,362475,11893108,0.99 2510,Get On The Snake,203,1,1,Chris Cornell/Kim Thayil,225123,7313744,0.99 2511,Jesus Christ Pose,203,1,1,Ben Shepherd/Chris Cornell/Kim Thayil/Matt Cameron,352966,11739886,0.99 2512,Outshined,203,1,1,Chris Cornell,312476,10274629,0.99 2513,Rusty Cage,203,1,1,Chris Cornell,267728,8779485,0.99 2514,Spoonman,203,1,1,Chris Cornell,248476,8289906,0.99 2515,The Day I Tried To Live,203,1,1,Chris Cornell,321175,10507137,0.99 2516,Black Hole Sun,203,1,1,Soundgarden,320365,10425229,0.99 2517,Fell On Black Days,203,1,1,Chris Cornell,282331,9256082,0.99 2518,Pretty Noose,203,1,1,Chris Cornell,253570,8317931,0.99 2519,Burden In My Hand,203,1,1,Chris Cornell,292153,9659911,0.99 2520,Blow Up The Outside World,203,1,1,Chris Cornell,347898,11379527,0.99 2521,Ty Cobb,203,1,1,Ben Shepherd/Chris Cornell,188786,6233136,0.99 2522,Bleed Together,203,1,1,Chris Cornell,232202,7597074,0.99 2523,Morning Dance,204,1,2,Jay Beckenstein,238759,8101979,0.99 2524,Jubilee,204,1,2,Jeremy Wall,275147,9151846,0.99 2525,Rasul,204,1,2,Jeremy Wall,238315,7854737,0.99 2526,Song For Lorraine,204,1,2,Jay Beckenstein,240091,8101723,0.99 2527,Starburst,204,1,2,Jeremy Wall,291500,9768399,0.99 2528,Heliopolis,204,1,2,Jay Beckenstein,338729,11365655,0.99 2529,It Doesn't Matter,204,1,2,Chet Catallo,270027,9034177,0.99 2530,Little Linda,204,1,2,Jeremy Wall,264019,8958743,0.99 2531,End Of Romanticism,204,1,2,Rick Strauss,320078,10553155,0.99 2532,The House Is Rockin',205,1,6,Doyle Bramhall/Stevie Ray Vaughan,144352,4706253,0.99 2533,Crossfire,205,1,6,B. Carter/C. Layton/R. Ellsworth/R. Wynans/T. Shannon,251219,8238033,0.99 2534,Tightrope,205,1,6,Doyle Bramhall/Stevie Ray Vaughan,281155,9254906,0.99 2535,Let Me Love You Baby,205,1,6,Willie Dixon,164127,5378455,0.99 2536,Leave My Girl Alone,205,1,6,B. Guy,256365,8438021,0.99 2537,Travis Walk,205,1,6,Stevie Ray Vaughan,140826,4650979,0.99 2538,Wall Of Denial,205,1,6,Doyle Bramhall/Stevie Ray Vaughan,336927,11085915,0.99 2539,Scratch-N-Sniff,205,1,6,Doyle Bramhall/Stevie Ray Vaughan,163422,5353627,0.99 2540,Love Me Darlin',205,1,6,C. Burnett,201586,6650869,0.99 2541,Riviera Paradise,205,1,6,Stevie Ray Vaughan,528692,17232776,0.99 2542,Dead And Bloated,206,1,1,R. DeLeo/Weiland,310386,10170433,0.99 2543,Sex Type Thing,206,1,1,D. DeLeo/Kretz/Weiland,218723,7102064,0.99 2544,Wicked Garden,206,1,1,D. DeLeo/R. DeLeo/Weiland,245368,7989505,0.99 2545,No Memory,206,1,1,Dean Deleo,80613,2660859,0.99 2546,Sin,206,1,1,R. DeLeo/Weiland,364800,12018823,0.99 2547,Naked Sunday,206,1,1,D. DeLeo/Kretz/R. DeLeo/Weiland,229720,7444201,0.99 2548,Creep,206,1,1,R. DeLeo/Weiland,333191,10894988,0.99 2549,Piece Of Pie,206,1,1,R. DeLeo/Weiland,324623,10605231,0.99 2550,Plush,206,1,1,R. DeLeo/Weiland,314017,10229848,0.99 2551,Wet My Bed,206,1,1,R. DeLeo/Weiland,96914,3198627,0.99 2552,Crackerman,206,1,1,Kretz/R. DeLeo/Weiland,194403,6317361,0.99 2553,Where The River Goes,206,1,1,D. DeLeo/Kretz/Weiland,505991,16468904,0.99 2554,Soldier Side - Intro,207,1,3,"Dolmayan, John/Malakian, Daron/Odadjian, Shavo",63764,2056079,0.99 2555,B.Y.O.B.,207,1,3,"Tankian, Serj",255555,8407935,0.99 2556,Revenga,207,1,3,"Tankian, Serj",228127,7503805,0.99 2557,Cigaro,207,1,3,"Tankian, Serj",131787,4321705,0.99 2558,Radio/Video,207,1,3,"Dolmayan, John/Malakian, Daron/Odadjian, Shavo",249312,8224917,0.99 2559,This Cocaine Makes Me Feel Like I'm On This Song,207,1,3,"Tankian, Serj",128339,4185193,0.99 2560,Violent Pornography,207,1,3,"Dolmayan, John/Malakian, Daron/Odadjian, Shavo",211435,6985960,0.99 2561,Question!,207,1,3,"Tankian, Serj",200698,6616398,0.99 2562,Sad Statue,207,1,3,"Tankian, Serj",205897,6733449,0.99 2563,Old School Hollywood,207,1,3,"Dolmayan, John/Malakian, Daron/Odadjian, Shavo",176953,5830258,0.99 2564,Lost in Hollywood,207,1,3,"Tankian, Serj",320783,10535158,0.99 2565,The Sun Road,208,1,1,"Terry Bozzio, Steve Stevens, Tony Levin",880640,29008407,0.99 2566,Dark Corners,208,1,1,"Terry Bozzio, Steve Stevens, Tony Levin",513541,16839223,0.99 2567,Duende,208,1,1,"Terry Bozzio, Steve Stevens, Tony Levin",447582,14956771,0.99 2568,Black Light Syndrome,208,1,1,"Terry Bozzio, Steve Stevens, Tony Levin",526471,17300835,0.99 2569,Falling in Circles,208,1,1,"Terry Bozzio, Steve Stevens, Tony Levin",549093,18263248,0.99 2570,Book of Hours,208,1,1,"Terry Bozzio, Steve Stevens, Tony Levin",583366,19464726,0.99 2571,Chaos-Control,208,1,1,"Terry Bozzio, Steve Stevens, Tony Levin",529841,17455568,0.99 2572,Midnight From The Inside Out,209,1,6,Chris Robinson/Rich Robinson,286981,9442157,0.99 2573,Sting Me,209,1,6,Chris Robinson/Rich Robinson,268094,8813561,0.99 2574,Thick & Thin,209,1,6,Chris Robinson/Rich Robinson,222720,7284377,0.99 2575,Greasy Grass River,209,1,6,Chris Robinson/Rich Robinson,218749,7157045,0.99 2576,Sometimes Salvation,209,1,6,Chris Robinson/Rich Robinson,389146,12749424,0.99 2577,Cursed Diamonds,209,1,6,Chris Robinson/Rich Robinson,368300,12047978,0.99 2578,Miracle To Me,209,1,6,Chris Robinson/Rich Robinson,372636,12222116,0.99 2579,Wiser Time,209,1,6,Chris Robinson/Rich Robinson,459990,15161907,0.99 2580,Girl From A Pawnshop,209,1,6,Chris Robinson/Rich Robinson,404688,13250848,0.99 2581,Cosmic Fiend,209,1,6,Chris Robinson/Rich Robinson,308401,10115556,0.99 2582,Black Moon Creeping,210,1,6,Chris Robinson/Rich Robinson,359314,11740886,0.99 2583,High Head Blues,210,1,6,Chris Robinson/Rich Robinson,371879,12227998,0.99 2584,Title Song,210,1,6,Chris Robinson/Rich Robinson,505521,16501316,0.99 2585,She Talks To Angels,210,1,6,Chris Robinson/Rich Robinson,361978,11837342,0.99 2586,Twice As Hard,210,1,6,Chris Robinson/Rich Robinson,275565,9008067,0.99 2587,Lickin',210,1,6,Chris Robinson/Rich Robinson,314409,10331216,0.99 2588,Soul Singing,210,1,6,Chris Robinson/Rich Robinson,233639,7672489,0.99 2589,Hard To Handle,210,1,6,A.Isbell/A.Jones/O.Redding,206994,6786304,0.99 2590,Remedy,210,1,6,Chris Robinson/Rich Robinson,337084,11049098,0.99 2591,White Riot,211,1,4,Joe Strummer/Mick Jones,118726,3922819,0.99 2592,Remote Control,211,1,4,Joe Strummer/Mick Jones,180297,5949647,0.99 2593,Complete Control,211,1,4,Joe Strummer/Mick Jones,192653,6272081,0.99 2594,Clash City Rockers,211,1,4,Joe Strummer/Mick Jones,227500,7555054,0.99 2595,(White Man) In Hammersmith Palais,211,1,4,Joe Strummer/Mick Jones,240640,7883532,0.99 2596,Tommy Gun,211,1,4,Joe Strummer/Mick Jones,195526,6399872,0.99 2597,English Civil War,211,1,4,Mick Jones/Traditional arr. Joe Strummer,156708,5111226,0.99 2598,I Fought The Law,211,1,4,Sonny Curtis,159764,5245258,0.99 2599,London Calling,211,1,4,Joe Strummer/Mick Jones,199706,6569007,0.99 2600,Train In Vain,211,1,4,Joe Strummer/Mick Jones,189675,6329877,0.99 2601,Bankrobber,211,1,4,Joe Strummer/Mick Jones,272431,9067323,0.99 2602,The Call Up,211,1,4,The Clash,324336,10746937,0.99 2603,Hitsville UK,211,1,4,The Clash,261433,8606887,0.99 2604,The Magnificent Seven,211,1,4,The Clash,268486,8889821,0.99 2605,This Is Radio Clash,211,1,4,The Clash,249756,8366573,0.99 2606,Know Your Rights,211,1,4,The Clash,217678,7195726,0.99 2607,Rock The Casbah,211,1,4,The Clash,222145,7361500,0.99 2608,Should I Stay Or Should I Go,211,1,4,The Clash,187219,6188688,0.99 2609,War (The Process),212,1,1,Billy Duffy/Ian Astbury,252630,8254842,0.99 2610,The Saint,212,1,1,Billy Duffy/Ian Astbury,216215,7061584,0.99 2611,Rise,212,1,1,Billy Duffy/Ian Astbury,219088,7106195,0.99 2612,Take The Power,212,1,1,Billy Duffy/Ian Astbury,235755,7650012,0.99 2613,Breathe,212,1,1,Billy Duffy/Ian Astbury/Marti Frederiksen/Mick Jones,299781,9742361,0.99 2614,Nico,212,1,1,Billy Duffy/Ian Astbury,289488,9412323,0.99 2615,American Gothic,212,1,1,Billy Duffy/Ian Astbury,236878,7739840,0.99 2616,Ashes And Ghosts,212,1,1,Billy Duffy/Bob Rock/Ian Astbury,300591,9787692,0.99 2617,Shape The Sky,212,1,1,Billy Duffy/Ian Astbury,209789,6885647,0.99 2618,Speed Of Light,212,1,1,Billy Duffy/Bob Rock/Ian Astbury,262817,8563352,0.99 2619,True Believers,212,1,1,Billy Duffy/Ian Astbury,308009,9981359,0.99 2620,My Bridges Burn,212,1,1,Billy Duffy/Ian Astbury,231862,7571370,0.99 2621,She Sells Sanctuary,213,1,1,,253727,8368634,0.99 2622,Fire Woman,213,1,1,,312790,10196995,0.99 2623,Lil' Evil,213,1,1,,165825,5419655,0.99 2624,Spirit Walker,213,1,1,,230060,7555897,0.99 2625,The Witch,213,1,1,,258768,8725403,0.99 2626,Revolution,213,1,1,,256026,8371254,0.99 2627,Wild Hearted Son,213,1,1,,266893,8670550,0.99 2628,Love Removal Machine,213,1,1,,257619,8412167,0.99 2629,Rain,213,1,1,,236669,7788461,0.99 2630,Edie (Ciao Baby),213,1,1,,241632,7846177,0.99 2631,Heart Of Soul,213,1,1,,274207,8967257,0.99 2632,Love,213,1,1,,326739,10729824,0.99 2633,Wild Flower,213,1,1,,215536,7084321,0.99 2634,Go West,213,1,1,,238158,7777749,0.99 2635,Resurrection Joe,213,1,1,,255451,8532840,0.99 2636,Sun King,213,1,1,,368431,12010865,0.99 2637,Sweet Soul Sister,213,1,1,,212009,6889883,0.99 2638,Earth Mofo,213,1,1,,282200,9204581,0.99 2639,Break on Through,214,1,1,"Robby Krieger, Ray Manzarek, John Densmore, Jim Morrison",149342,4943144,0.99 2640,Soul Kitchen,214,1,1,"Robby Krieger, Ray Manzarek, John Densmore, Jim Morrison",215066,7040865,0.99 2641,The Crystal Ship,214,1,1,"Robby Krieger, Ray Manzarek, John Densmore, Jim Morrison",154853,5052658,0.99 2642,Twentienth Century Fox,214,1,1,"Robby Krieger, Ray Manzarek, John Densmore, Jim Morrison",153913,5069211,0.99 2643,Alabama Song,214,1,1,Weill-Brecht,200097,6563411,0.99 2644,Light My Fire,214,1,1,"Robby Krieger, Ray Manzarek, John Densmore, Jim Morrison",428329,13963351,0.99 2645,Back Door Man,214,1,1,"Willie Dixon, C. Burnett",214360,7035636,0.99 2646,I Looked At You,214,1,1,"Robby Krieger, Ray Manzarek, John Densmore, Jim Morrison",142080,4663988,0.99 2647,End Of The Night,214,1,1,"Robby Krieger, Ray Manzarek, John Densmore, Jim Morrison",172695,5589732,0.99 2648,Take It As It Comes,214,1,1,"Robby Krieger, Ray Manzarek, John Densmore, Jim Morrison",137168,4512656,0.99 2649,The End,214,1,1,"Robby Krieger, Ray Manzarek, John Densmore, Jim Morrison",701831,22927336,0.99 2650,Roxanne,215,1,1,G M Sumner,192992,6330159,0.99 2651,Can't Stand Losing You,215,1,1,G M Sumner,181159,5971983,0.99 2652,Message in a Bottle,215,1,1,G M Sumner,291474,9647829,0.99 2653,Walking on the Moon,215,1,1,G M Sumner,302080,10019861,0.99 2654,Don't Stand so Close to Me,215,1,1,G M Sumner,241031,7956658,0.99 2655,"De Do Do Do, De Da Da Da",215,1,1,G M Sumner,247196,8227075,0.99 2656,Every Little Thing She Does is Magic,215,1,1,G M Sumner,261120,8646853,0.99 2657,Invisible Sun,215,1,1,G M Sumner,225593,7304320,0.99 2658,Spirit's in the Material World,215,1,1,G M Sumner,181133,5986622,0.99 2659,Every Breath You Take,215,1,1,G M Sumner,254615,8364520,0.99 2660,King Of Pain,215,1,1,G M Sumner,300512,9880303,0.99 2661,Wrapped Around Your Finger,215,1,1,G M Sumner,315454,10361490,0.99 2662,Don't Stand So Close to Me '86,215,1,1,G M Sumner,293590,9636683,0.99 2663,Message in a Bottle (new classic rock mix),215,1,1,G M Sumner,290951,9640349,0.99 2664,Time Is On My Side,216,1,1,Jerry Ragavoy,179983,5855836,0.99 2665,Heart Of Stone,216,1,1,Jagger/Richards,164493,5329538,0.99 2666,Play With Fire,216,1,1,Nanker Phelge,132022,4265297,0.99 2667,Satisfaction,216,1,1,Jagger/Richards,226612,7398766,0.99 2668,As Tears Go By,216,1,1,Jagger/Richards/Oldham,164284,5357350,0.99 2669,Get Off Of My Cloud,216,1,1,Jagger/Richards,176013,5719514,0.99 2670,Mother's Little Helper,216,1,1,Jagger/Richards,167549,5422434,0.99 2671,19th Nervous Breakdown,216,1,1,Jagger/Richards,237923,7742984,0.99 2672,Paint It Black,216,1,1,Jagger/Richards,226063,7442888,0.99 2673,Under My Thumb,216,1,1,Jagger/Richards,221387,7371799,0.99 2674,Ruby Tuesday,216,1,1,Jagger/Richards,197459,6433467,0.99 2675,Let's Spend The Night Together,216,1,1,Jagger/Richards,217495,7137048,0.99 2676,Intro,217,1,1,Jagger/Richards,49737,1618591,0.99 2677,You Got Me Rocking,217,1,1,Jagger/Richards,205766,6734385,0.99 2678,Gimmie Shelters,217,1,1,Jagger/Richards,382119,12528764,0.99 2679,Flip The Case,217,1,1,Jagger/Richards,252421,8336591,0.99 2680,Memory Motel,217,1,1,Jagger/Richards,365844,11982431,0.99 2681,Corinna,217,1,1,Jesse Ed Davis III/Taj Mahal,257488,8449471,0.99 2682,Saint Of Me,217,1,1,Jagger/Richards,325694,10725160,0.99 2683,Wainting On A Friend,217,1,1,Jagger/Richards,302497,9978046,0.99 2684,Sister Morphine,217,1,1,Faithfull/Jagger/Richards,376215,12345289,0.99 2685,Live With Me,217,1,1,Jagger/Richards,234893,7709006,0.99 2686,Respectable,217,1,1,Jagger/Richards,215693,7099669,0.99 2687,Thief In The Night,217,1,1,De Beauport/Jagger/Richards,337266,10952756,0.99 2688,The Last Time,217,1,1,Jagger/Richards,287294,9498758,0.99 2689,Out Of Control,217,1,1,Jagger/Richards,479242,15749289,0.99 2690,Love Is Strong,218,1,1,Jagger/Richards,230896,7639774,0.99 2691,You Got Me Rocking,218,1,1,Jagger/Richards,215928,7162159,0.99 2692,Sparks Will Fly,218,1,1,Jagger/Richards,196466,6492847,0.99 2693,The Worst,218,1,1,Jagger/Richards,144613,4750094,0.99 2694,New Faces,218,1,1,Jagger/Richards,172146,5689122,0.99 2695,Moon Is Up,218,1,1,Jagger/Richards,222119,7366316,0.99 2696,Out Of Tears,218,1,1,Jagger/Richards,327418,10677236,0.99 2697,I Go Wild,218,1,1,Jagger/Richards,264019,8630833,0.99 2698,Brand New Car,218,1,1,Jagger/Richards,256052,8459344,0.99 2699,Sweethearts Together,218,1,1,Jagger/Richards,285492,9550459,0.99 2700,Suck On The Jugular,218,1,1,Jagger/Richards,268225,8920566,0.99 2701,Blinded By Rainbows,218,1,1,Jagger/Richards,273946,8971343,0.99 2702,Baby Break It Down,218,1,1,Jagger/Richards,249417,8197309,0.99 2703,Thru And Thru,218,1,1,Jagger/Richards,375092,12175406,0.99 2704,Mean Disposition,218,1,1,Jagger/Richards,249155,8273602,0.99 2705,Walking Wounded,219,1,4,The Tea Party,277968,9184345,0.99 2706,Temptation,219,1,4,The Tea Party,205087,6711943,0.99 2707,The Messenger,219,1,4,Daniel Lanois,212062,6975437,0.99 2708,Psychopomp,219,1,4,The Tea Party,315559,10295199,0.99 2709,Sister Awake,219,1,4,The Tea Party,343875,11299407,0.99 2710,The Bazaar,219,1,4,The Tea Party,222458,7245691,0.99 2711,Save Me (Remix),219,1,4,The Tea Party,396303,13053839,0.99 2712,Fire In The Head,219,1,4,The Tea Party,306337,10005675,0.99 2713,Release,219,1,4,The Tea Party,244114,8014606,0.99 2714,Heaven Coming Down,219,1,4,The Tea Party,241867,7846459,0.99 2715,The River (Remix),219,1,4,The Tea Party,343170,11193268,0.99 2716,Babylon,219,1,4,The Tea Party,169795,5568808,0.99 2717,Waiting On A Sign,219,1,4,The Tea Party,261903,8558590,0.99 2718,Life Line,219,1,4,The Tea Party,277786,9082773,0.99 2719,Paint It Black,219,1,4,Keith Richards/Mick Jagger,214752,7101572,0.99 2720,Temptation,220,1,4,The Tea Party,205244,6719465,0.99 2721,Army Ants,220,1,4,The Tea Party,215405,7075838,0.99 2722,Psychopomp,220,1,4,The Tea Party,317231,10351778,0.99 2723,Gyroscope,220,1,4,The Tea Party,177711,5810323,0.99 2724,Alarum,220,1,4,The Tea Party,298187,9712545,0.99 2725,Release,220,1,4,The Tea Party,266292,8725824,0.99 2726,Transmission,220,1,4,The Tea Party,317257,10351152,0.99 2727,Babylon,220,1,4,The Tea Party,292466,9601786,0.99 2728,Pulse,220,1,4,The Tea Party,250253,8183872,0.99 2729,Emerald,220,1,4,The Tea Party,289750,9543789,0.99 2730,Aftermath,220,1,4,The Tea Party,343745,11085607,0.99 2731,I Can't Explain,221,1,1,Pete Townshend,125152,4082896,0.99 2732,"Anyway, Anyhow, Anywhere",221,1,1,"Pete Townshend, Roger Daltrey",161253,5234173,0.99 2733,My Generation,221,1,1,John Entwistle/Pete Townshend,197825,6446634,0.99 2734,Substitute,221,1,1,Pete Townshend,228022,7409995,0.99 2735,I'm A Boy,221,1,1,Pete Townshend,157126,5120605,0.99 2736,Boris The Spider,221,1,1,John Entwistle,149472,4835202,0.99 2737,Happy Jack,221,1,1,Pete Townshend,132310,4353063,0.99 2738,Pictures Of Lily,221,1,1,Pete Townshend,164414,5329751,0.99 2739,I Can See For Miles,221,1,1,Pete Townshend,262791,8604989,0.99 2740,Magic Bus,221,1,1,Pete Townshend,197224,6452700,0.99 2741,Pinball Wizard,221,1,1,John Entwistle/Pete Townshend,181890,6055580,0.99 2742,The Seeker,221,1,1,Pete Townshend,204643,6736866,0.99 2743,Baba O'Riley,221,1,1,John Entwistle/Pete Townshend,309472,10141660,0.99 2744,Won't Get Fooled Again (Full Length Version),221,1,1,John Entwistle/Pete Townshend,513750,16855521,0.99 2745,Let's See Action,221,1,1,Pete Townshend,243513,8078418,0.99 2746,5.15,221,1,1,Pete Townshend,289619,9458549,0.99 2747,Join Together,221,1,1,Pete Townshend,262556,8602485,0.99 2748,Squeeze Box,221,1,1,Pete Townshend,161280,5256508,0.99 2749,Who Are You (Single Edit Version),221,1,1,John Entwistle/Pete Townshend,299232,9900469,0.99 2750,You Better You Bet,221,1,1,Pete Townshend,338520,11160877,0.99 2751,Primavera,222,1,7,Genival Cassiano/Silvio Rochael,126615,4152604,0.99 2752,Chocolate,222,1,7,Tim Maia,194690,6411587,0.99 2753,Azul Da Cor Do Mar,222,1,7,Tim Maia,197955,6475007,0.99 2754,O Descobridor Dos Sete Mares,222,1,7,Gilson Mendonça/Michel,262974,8749583,0.99 2755,Até Que Enfim Encontrei Você,222,1,7,Tim Maia,105064,3477751,0.99 2756,Coroné Antonio Bento,222,1,7,"Do Vale, João/Luiz Wanderley",131317,4340326,0.99 2757,New Love,222,1,7,Tim Maia,237897,7786824,0.99 2758,Não Vou Ficar,222,1,7,Tim Maia,172068,5642919,0.99 2759,Música No Ar,222,1,7,Tim Maia,158511,5184891,0.99 2760,Salve Nossa Senhora,222,1,7,Carlos Imperial/Edardo Araújo,115461,3827629,0.99 2761,Você Fugiu,222,1,7,Genival Cassiano,238367,7971147,0.99 2762,Cristina Nº 2,222,1,7,Carlos Imperial/Tim Maia,90148,2978589,0.99 2763,Compadre,222,1,7,Tim Maia,171389,5631446,0.99 2764,Over Again,222,1,7,Tim Maia,200489,6612634,0.99 2765,Réu Confesso,222,1,7,Tim Maia,217391,7189874,0.99 2766,O Que Me Importa,223,1,7,,153155,4977852,0.99 2767,Gostava Tanto De Você,223,1,7,,253805,8380077,0.99 2768,Você,223,1,7,,242599,7911702,0.99 2769,Não Quero Dinheiro,223,1,7,,152607,5031797,0.99 2770,Eu Amo Você,223,1,7,,242782,7914628,0.99 2771,A Festa Do Santo Reis,223,1,7,,159791,5204995,0.99 2772,I Don't Know What To Do With Myself,223,1,7,,221387,7251478,0.99 2773,Padre Cícero,223,1,7,,139598,4581685,0.99 2774,Nosso Adeus,223,1,7,,206471,6793270,0.99 2775,Canário Do Reino,223,1,7,,139337,4552858,0.99 2776,Preciso Ser Amado,223,1,7,,174001,5618895,0.99 2777,Balanço,223,1,7,,209737,6890327,0.99 2778,Preciso Aprender A Ser Só,223,1,7,,162220,5213894,0.99 2779,Esta É A Canção,223,1,7,,184450,6069933,0.99 2780,Formigueiro,223,1,7,,252943,8455132,0.99 2781,Comida,224,1,4,Titãs,322612,10786578,0.99 2782,Go Back,224,1,4,Titãs,230504,7668899,0.99 2783,Prá Dizer Adeus,224,1,4,Titãs,222484,7382048,0.99 2784,Família,224,1,4,Titãs,218331,7267458,0.99 2785,Os Cegos Do Castelo,224,1,4,Titãs,296829,9868187,0.99 2786,O Pulso,224,1,4,Titãs,199131,6566998,0.99 2787,Marvin,224,1,4,Titãs,264359,8741444,0.99 2788,Nem 5 Minutos Guardados,224,1,4,Titãs,245995,8143797,0.99 2789,Flores,224,1,4,Titãs,215510,7148017,0.99 2790,Palavras,224,1,4,Titãs,158458,5285715,0.99 2791,Hereditário,224,1,4,Titãs,151693,5020547,0.99 2792,A Melhor Forma,224,1,4,Titãs,191503,6349938,0.99 2793,Cabeça Dinossauro,224,1,4,Titãs,37120,1220930,0.99 2794,32 Dentes,224,1,4,Titãs,184946,6157904,0.99 2795,Bichos Escrotos (Vinheta),224,1,4,Titãs,104986,3503755,0.99 2796,Não Vou Lutar,224,1,4,Titãs,189988,6308613,0.99 2797,Homem Primata (Vinheta),224,1,4,Titãs,34168,1124909,0.99 2798,Homem Primata,224,1,4,Titãs,195500,6486470,0.99 2799,Polícia (Vinheta),224,1,4,Titãs,56111,1824213,0.99 2800,Querem Meu Sangue,224,1,4,Titãs,212401,7069773,0.99 2801,Diversão,224,1,4,Titãs,285936,9531268,0.99 2802,Televisão,224,1,4,Titãs,293668,9776548,0.99 2803,Sonifera Ilha,225,1,4,Branco Mello/Carlos Barmack/Ciro Pessoa/Marcelo Fromer/Toni Belloto,170684,5678290,0.99 2804,Lugar Nenhum,225,1,4,Arnaldo Antunes/Charles Gavin/Marcelo Fromer/Sérgio Britto/Toni Bellotto,195840,6472780,0.99 2805,Sua Impossivel Chance,225,1,4,Nando Reis,246622,8073248,0.99 2806,Desordem,225,1,4,Charles Gavin/Marcelo Fromer/Sérgio Britto,213289,7067340,0.99 2807,Não Vou Me Adaptar,225,1,4,Arnaldo Antunes,221831,7304656,0.99 2808,Domingo,225,1,4,Sérgio Britto/Toni Bellotto,208613,6883180,0.99 2809,Amanhã Não Se Sabe,225,1,4,Sérgio Britto,189440,6243967,0.99 2810,Caras Como Eu,225,1,4,Toni Bellotto,183092,5999048,0.99 2811,Senhora E Senhor,225,1,4,Arnaldo Anutnes/Marcelo Fromer/Paulo Miklos,203702,6733733,0.99 2812,Era Uma Vez,225,1,4,Arnaldo Anutnes/Branco Mello/Marcelo Fromer/Sergio Brotto/Toni Bellotto,224261,7453156,0.99 2813,Miséria,225,1,4,"Arnaldo Antunes/Britto, SergioMiklos, Paulo",262191,8727645,0.99 2814,Insensível,225,1,4,Sérgio Britto,207830,6893664,0.99 2815,Eu E Ela,225,1,4,Nando Reis,276035,9138846,0.99 2816,Toda Cor,225,1,4,Ciro Pressoa/Marcelo Fromer,209084,6939176,0.99 2817,É Preciso Saber Viver,225,1,4,Erasmo Carlos/Roberto Carlos,251115,8271418,0.99 2818,Senhor Delegado/Eu Não Aguento,225,1,4,Antonio Lopes,156656,5277983,0.99 2819,Battlestar Galactica: The Story So Far,226,3,18,,2622250,490750393,1.99 2820,Occupation / Precipice,227,3,19,,5286953,1054423946,1.99 2821,"Exodus, Pt. 1",227,3,19,,2621708,475079441,1.99 2822,"Exodus, Pt. 2",227,3,19,,2618000,466820021,1.99 2823,Collaborators,227,3,19,,2626626,483484911,1.99 2824,Torn,227,3,19,,2631291,495262585,1.99 2825,A Measure of Salvation,227,3,18,,2563938,489715554,1.99 2826,Hero,227,3,18,,2713755,506896959,1.99 2827,Unfinished Business,227,3,18,,2622038,528499160,1.99 2828,The Passage,227,3,18,,2623875,490375760,1.99 2829,The Eye of Jupiter,227,3,18,,2618750,517909587,1.99 2830,Rapture,227,3,18,,2624541,508406153,1.99 2831,Taking a Break from All Your Worries,227,3,18,,2624207,492700163,1.99 2832,The Woman King,227,3,18,,2626376,552893447,1.99 2833,A Day In the Life,227,3,18,,2620245,462818231,1.99 2834,Dirty Hands,227,3,18,,2627961,537648614,1.99 2835,Maelstrom,227,3,18,,2622372,514154275,1.99 2836,The Son Also Rises,227,3,18,,2621830,499258498,1.99 2837,"Crossroads, Pt. 1",227,3,20,,2622622,486233524,1.99 2838,"Crossroads, Pt. 2",227,3,20,,2869953,497335706,1.99 2839,Genesis,228,3,19,,2611986,515671080,1.99 2840,Don't Look Back,228,3,21,,2571154,493628775,1.99 2841,One Giant Leap,228,3,21,,2607649,521616246,1.99 2842,Collision,228,3,21,,2605480,526182322,1.99 2843,Hiros,228,3,21,,2533575,488835454,1.99 2844,Better Halves,228,3,21,,2573031,549353481,1.99 2845,Nothing to Hide,228,3,19,,2605647,510058181,1.99 2846,Seven Minutes to Midnight,228,3,21,,2613988,515590682,1.99 2847,Homecoming,228,3,21,,2601351,516015339,1.99 2848,Six Months Ago,228,3,19,,2602852,505133869,1.99 2849,Fallout,228,3,21,,2594761,501145440,1.99 2850,The Fix,228,3,21,,2600266,507026323,1.99 2851,Distractions,228,3,21,,2590382,537111289,1.99 2852,Run!,228,3,21,,2602602,542936677,1.99 2853,Unexpected,228,3,21,,2598139,511777758,1.99 2854,Company Man,228,3,21,,2601226,493168135,1.99 2855,Company Man,228,3,21,,2601101,503786316,1.99 2856,Parasite,228,3,21,,2602727,487461520,1.99 2857,A Tale of Two Cities,229,3,19,,2636970,513691652,1.99 2858,"Lost (Pilot, Part 1) [Premiere]",230,3,19,,2548875,217124866,1.99 2859,"Man of Science, Man of Faith (Premiere)",231,3,19,,2612250,543342028,1.99 2860,Adrift,231,3,19,,2564958,502663995,1.99 2861,"Lost (Pilot, Part 2)",230,3,19,,2436583,204995876,1.99 2862,The Glass Ballerina,229,3,21,,2637458,535729216,1.99 2863,Further Instructions,229,3,19,,2563980,502041019,1.99 2864,Orientation,231,3,19,,2609083,500600434,1.99 2865,Tabula Rasa,230,3,19,,2627105,210526410,1.99 2866,Every Man for Himself,229,3,21,,2637387,513803546,1.99 2867,Everybody Hates Hugo,231,3,19,,2609192,498163145,1.99 2868,Walkabout,230,3,19,,2587370,207748198,1.99 2869,...And Found,231,3,19,,2563833,500330548,1.99 2870,The Cost of Living,229,3,19,,2637500,505647192,1.99 2871,White Rabbit,230,3,19,,2571965,201654606,1.99 2872,Abandoned,231,3,19,,2587041,537348711,1.99 2873,House of the Rising Sun,230,3,19,,2590032,210379525,1.99 2874,I Do,229,3,19,,2627791,504676825,1.99 2875,Not In Portland,229,3,21,,2637303,499061234,1.99 2876,Not In Portland,229,3,21,,2637345,510546847,1.99 2877,The Moth,230,3,19,,2631327,228896396,1.99 2878,The Other 48 Days,231,3,19,,2610625,535256753,1.99 2879,Collision,231,3,19,,2564916,475656544,1.99 2880,Confidence Man,230,3,19,,2615244,223756475,1.99 2881,Flashes Before Your Eyes,229,3,21,,2636636,537760755,1.99 2882,Lost Survival Guide,229,3,21,,2632590,486675063,1.99 2883,Solitary,230,3,19,,2612894,207045178,1.99 2884,What Kate Did,231,3,19,,2610250,484583988,1.99 2885,Raised By Another,230,3,19,,2590459,223623810,1.99 2886,Stranger In a Strange Land,229,3,21,,2636428,505056021,1.99 2887,The 23rd Psalm,231,3,19,,2610416,487401604,1.99 2888,All the Best Cowboys Have Daddy Issues,230,3,19,,2555492,211743651,1.99 2889,The Hunting Party,231,3,21,,2611333,520350364,1.99 2890,Tricia Tanaka Is Dead,229,3,21,,2635010,548197162,1.99 2891,Enter 77,229,3,21,,2629796,517521422,1.99 2892,Fire + Water,231,3,21,,2600333,488458695,1.99 2893,Whatever the Case May Be,230,3,19,,2616410,183867185,1.99 2894,Hearts and Minds,230,3,19,,2619462,207607466,1.99 2895,Par Avion,229,3,21,,2629879,517079642,1.99 2896,The Long Con,231,3,19,,2679583,518376636,1.99 2897,One of Them,231,3,21,,2698791,542332389,1.99 2898,Special,230,3,19,,2618530,219961967,1.99 2899,The Man from Tallahassee,229,3,21,,2637637,550893556,1.99 2900,Exposé,229,3,21,,2593760,511338017,1.99 2901,Homecoming,230,3,19,,2515882,210675221,1.99 2902,Maternity Leave,231,3,21,,2780416,555244214,1.99 2903,Left Behind,229,3,21,,2635343,538491964,1.99 2904,Outlaws,230,3,19,,2619887,206500939,1.99 2905,The Whole Truth,231,3,21,,2610125,495487014,1.99 2906,...In Translation,230,3,19,,2604575,215441983,1.99 2907,Lockdown,231,3,21,,2610250,543886056,1.99 2908,One of Us,229,3,21,,2638096,502387276,1.99 2909,Catch-22,229,3,21,,2561394,489773399,1.99 2910,Dave,231,3,19,,2825166,574325829,1.99 2911,Numbers,230,3,19,,2609772,214709143,1.99 2912,D.O.C.,229,3,21,,2616032,518556641,1.99 2913,Deus Ex Machina,230,3,19,,2582009,214996732,1.99 2914,S.O.S.,231,3,19,,2639541,517979269,1.99 2915,Do No Harm,230,3,19,,2618487,212039309,1.99 2916,Two for the Road,231,3,21,,2610958,502404558,1.99 2917,The Greater Good,230,3,19,,2617784,214130273,1.99 2918,"""?""",231,3,19,,2782333,528227089,1.99 2919,Born to Run,230,3,19,,2618619,213772057,1.99 2920,Three Minutes,231,3,19,,2763666,531556853,1.99 2921,Exodus (Part 1),230,3,19,,2620747,213107744,1.99 2922,"Live Together, Die Alone, Pt. 1",231,3,21,,2478041,457364940,1.99 2923,Exodus (Part 2) [Season Finale],230,3,19,,2605557,208667059,1.99 2924,"Live Together, Die Alone, Pt. 2",231,3,19,,2656531,503619265,1.99 2925,Exodus (Part 3) [Season Finale],230,3,19,,2619869,197937785,1.99 2926,Zoo Station,232,1,1,U2,276349,9056902,0.99 2927,Even Better Than The Real Thing,232,1,1,U2,221361,7279392,0.99 2928,One,232,1,1,U2,276192,9158892,0.99 2929,Until The End Of The World,232,1,1,U2,278700,9132485,0.99 2930,Who's Gonna Ride Your Wild Horses,232,1,1,U2,316551,10304369,0.99 2931,So Cruel,232,1,1,U2,349492,11527614,0.99 2932,The Fly,232,1,1,U2,268982,8825399,0.99 2933,Mysterious Ways,232,1,1,U2,243826,8062057,0.99 2934,Tryin' To Throw Your Arms Around The World,232,1,1,U2,232463,7612124,0.99 2935,Ultraviolet (Light My Way),232,1,1,U2,330788,10754631,0.99 2936,Acrobat,232,1,1,U2,270288,8824723,0.99 2937,Love Is Blindness,232,1,1,U2,263497,8531766,0.99 2938,Beautiful Day,233,1,1,"Adam Clayton, Bono, Larry Mullen, The Edge",248163,8056723,0.99 2939,Stuck In A Moment You Can't Get Out Of,233,1,1,"Adam Clayton, Bono, Larry Mullen, The Edge",272378,8997366,0.99 2940,Elevation,233,1,1,"Adam Clayton, Bono, Larry Mullen, The Edge",227552,7479414,0.99 2941,Walk On,233,1,1,"Adam Clayton, Bono, Larry Mullen, The Edge",296280,9800861,0.99 2942,Kite,233,1,1,"Adam Clayton, Bono, Larry Mullen, The Edge",266893,8765761,0.99 2943,In A Little While,233,1,1,"Adam Clayton, Bono, Larry Mullen, The Edge",219271,7189647,0.99 2944,Wild Honey,233,1,1,"Adam Clayton, Bono, Larry Mullen, The Edge",226768,7466069,0.99 2945,Peace On Earth,233,1,1,"Adam Clayton, Bono, Larry Mullen, The Edge",288496,9476171,0.99 2946,When I Look At The World,233,1,1,"Adam Clayton, Bono, Larry Mullen, The Edge",257776,8500491,0.99 2947,New York,233,1,1,"Adam Clayton, Bono, Larry Mullen, The Edge",330370,10862323,0.99 2948,Grace,233,1,1,"Adam Clayton, Bono, Larry Mullen, The Edge",330657,10877148,0.99 2949,The Three Sunrises,234,1,1,U2,234788,7717990,0.99 2950,Spanish Eyes,234,1,1,U2,196702,6392710,0.99 2951,Sweetest Thing,234,1,1,U2,185103,6154896,0.99 2952,Love Comes Tumbling,234,1,1,U2,282671,9328802,0.99 2953,Bass Trap,234,1,1,U2,213289,6834107,0.99 2954,Dancing Barefoot,234,1,1,Ivan Kral/Patti Smith,287895,9488294,0.99 2955,Everlasting Love,234,1,1,Buzz Cason/Mac Gayden,202631,6708932,0.99 2956,Unchained Melody,234,1,1,Alex North/Hy Zaret,294164,9597568,0.99 2957,Walk To The Water,234,1,1,U2,289253,9523336,0.99 2958,Luminous Times (Hold On To Love),234,1,1,Brian Eno/U2,277760,9015513,0.99 2959,Hallelujah Here She Comes,234,1,1,U2,242364,8027028,0.99 2960,Silver And Gold,234,1,1,Bono,279875,9199746,0.99 2961,Endless Deep,234,1,1,U2,179879,5899070,0.99 2962,A Room At The Heartbreak Hotel,234,1,1,U2,274546,9015416,0.99 2963,"Trash, Trampoline And The Party Girl",234,1,1,U2,153965,5083523,0.99 2964,Vertigo,235,1,1,"Adam Clayton, Bono, Larry Mullen & The Edge",194612,6329502,0.99 2965,Miracle Drug,235,1,1,"Adam Clayton, Bono, Larry Mullen & The Edge",239124,7760916,0.99 2966,Sometimes You Can't Make It On Your Own,235,1,1,"Adam Clayton, Bono, Larry Mullen & The Edge",308976,10112863,0.99 2967,Love And Peace Or Else,235,1,1,"Adam Clayton, Bono, Larry Mullen & The Edge",290690,9476723,0.99 2968,City Of Blinding Lights,235,1,1,"Adam Clayton, Bono, Larry Mullen & The Edge",347951,11432026,0.99 2969,All Because Of You,235,1,1,"Adam Clayton, Bono, Larry Mullen & The Edge",219141,7198014,0.99 2970,A Man And A Woman,235,1,1,"Adam Clayton, Bono, Larry Mullen & The Edge",270132,8938285,0.99 2971,Crumbs From Your Table,235,1,1,"Adam Clayton, Bono, Larry Mullen & The Edge",303568,9892349,0.99 2972,One Step Closer,235,1,1,"Adam Clayton, Bono, Larry Mullen & The Edge",231680,7512912,0.99 2973,Original Of The Species,235,1,1,"Adam Clayton, Bono, Larry Mullen & The Edge",281443,9230041,0.99 2974,Yahweh,235,1,1,"Adam Clayton, Bono, Larry Mullen & The Edge",262034,8636998,0.99 2975,Discotheque,236,1,1,"Bono, The Edge, Adam Clayton, and Larry Mullen",319582,10442206,0.99 2976,Do You Feel Loved,236,1,1,"Bono, The Edge, Adam Clayton, and Larry Mullen",307539,10122694,0.99 2977,Mofo,236,1,1,"Bono, The Edge, Adam Clayton, and Larry Mullen",349178,11583042,0.99 2978,If God Will Send His Angels,236,1,1,"Bono, The Edge, Adam Clayton, and Larry Mullen",322533,10563329,0.99 2979,Staring At The Sun,236,1,1,"Bono, The Edge, Adam Clayton, and Larry Mullen",276924,9082838,0.99 2980,Last Night On Earth,236,1,1,"Bono, The Edge, Adam Clayton, and Larry Mullen",285753,9401017,0.99 2981,Gone,236,1,1,"Bono, The Edge, Adam Clayton, and Larry Mullen",266866,8746301,0.99 2982,Miami,236,1,1,"Bono, The Edge, Adam Clayton, and Larry Mullen",293041,9741603,0.99 2983,The Playboy Mansion,236,1,1,"Bono, The Edge, Adam Clayton, and Larry Mullen",280555,9274144,0.99 2984,If You Wear That Velvet Dress,236,1,1,"Bono, The Edge, Adam Clayton, and Larry Mullen",315167,10227333,0.99 2985,Please,236,1,1,"Bono, The Edge, Adam Clayton, and Larry Mullen",302602,9909484,0.99 2986,Wake Up Dead Man,236,1,1,"Bono, The Edge, Adam Clayton, and Larry Mullen",292832,9515903,0.99 2987,Helter Skelter,237,1,1,"Lennon, John/McCartney, Paul",187350,6097636,0.99 2988,Van Diemen's Land,237,1,1,"Bono/Clayton, Adam/Mullen Jr., Larry/The Edge",186044,5990280,0.99 2989,Desire,237,1,1,"Bono/Clayton, Adam/Mullen Jr., Larry/The Edge",179226,5874535,0.99 2990,Hawkmoon 269,237,1,1,"Bono/Clayton, Adam/Mullen Jr., Larry/The Edge",382458,12494987,0.99 2991,All Along The Watchtower,237,1,1,"Dylan, Bob",264568,8623572,0.99 2992,I Still Haven't Found What I'm Looking for,237,1,1,"Bono/Clayton, Adam/Mullen Jr., Larry/The Edge",353567,11542247,0.99 2993,Freedom For My People,237,1,1,"Mabins, Macie/Magee, Sterling/Robinson, Bobby",38164,1249764,0.99 2994,Silver And Gold,237,1,1,"Bono/Clayton, Adam/Mullen Jr., Larry/The Edge",349831,11450194,0.99 2995,Pride (In The Name Of Love),237,1,1,"Bono/Clayton, Adam/Mullen Jr., Larry/The Edge",267807,8806361,0.99 2996,Angel Of Harlem,237,1,1,"Bono/Clayton, Adam/Mullen Jr., Larry/The Edge",229276,7498022,0.99 2997,Love Rescue Me,237,1,1,"Bono/Clayton, Adam/Dylan, Bob/Mullen Jr., Larry/The Edge",384522,12508716,0.99 2998,When Love Comes To Town,237,1,1,"Bono/Clayton, Adam/Mullen Jr., Larry/The Edge",255869,8340954,0.99 2999,Heartland,237,1,1,"Bono/Clayton, Adam/Mullen Jr., Larry/The Edge",303360,9867748,0.99 3000,God Part II,237,1,1,"Bono/Clayton, Adam/Mullen Jr., Larry/The Edge",195604,6497570,0.99 3001,The Star Spangled Banner,237,1,1,"Hendrix, Jimi",43232,1385810,0.99 3002,Bullet The Blue Sky,237,1,1,"Bono/Clayton, Adam/Mullen Jr., Larry/The Edge",337005,10993607,0.99 3003,All I Want Is You,237,1,1,"Bono/Clayton, Adam/Mullen Jr., Larry/The Edge",390243,12729820,0.99 3004,Pride (In The Name Of Love),238,1,1,U2,230243,7549085,0.99 3005,New Year's Day,238,1,1,U2,258925,8491818,0.99 3006,With Or Without You,238,1,1,U2,299023,9765188,0.99 3007,I Still Haven't Found What I'm Looking For,238,1,1,U2,280764,9306737,0.99 3008,Sunday Bloody Sunday,238,1,1,U2,282174,9269668,0.99 3009,Bad,238,1,1,U2,351817,11628058,0.99 3010,Where The Streets Have No Name,238,1,1,U2,276218,9042305,0.99 3011,I Will Follow,238,1,1,U2,218253,7184825,0.99 3012,The Unforgettable Fire,238,1,1,U2,295183,9684664,0.99 3013,Sweetest Thing,238,1,1,U2 & Daragh O'Toole,183066,6071385,0.99 3014,Desire,238,1,1,U2,179853,5893206,0.99 3015,When Love Comes To Town,238,1,1,U2,258194,8479525,0.99 3016,Angel Of Harlem,238,1,1,U2,230217,7527339,0.99 3017,All I Want Is You,238,1,1,U2 & Van Dyke Parks,591986,19202252,0.99 3018,Sunday Bloody Sunday,239,1,1,U2,278204,9140849,0.99 3019,Seconds,239,1,1,U2,191582,6352121,0.99 3020,New Year's Day,239,1,1,U2,336274,11054732,0.99 3021,Like A Song...,239,1,1,U2,287294,9365379,0.99 3022,Drowning Man,239,1,1,U2,254458,8457066,0.99 3023,The Refugee,239,1,1,U2,221283,7374043,0.99 3024,Two Hearts Beat As One,239,1,1,U2,243487,7998323,0.99 3025,Red Light,239,1,1,U2,225854,7453704,0.99 3026,Surrender,239,1,1,U2,333505,11221406,0.99 3027,"""40""",239,1,1,U2,157962,5251767,0.99 3028,Zooropa,240,1,1,U2; Bono,392359,12807979,0.99 3029,Babyface,240,1,1,U2; Bono,241998,7942573,0.99 3030,Numb,240,1,1,"U2; Edge, The",260284,8577861,0.99 3031,Lemon,240,1,1,U2; Bono,418324,13988878,0.99 3032,"Stay (Faraway, So Close!)",240,1,1,U2; Bono,298475,9785480,0.99 3033,Daddy's Gonna Pay For Your Crashed Car,240,1,1,U2; Bono,320287,10609581,0.99 3034,Some Days Are Better Than Others,240,1,1,U2; Bono,257436,8417690,0.99 3035,The First Time,240,1,1,U2; Bono,225697,7247651,0.99 3036,Dirty Day,240,1,1,"U2; Bono & Edge, The",324440,10652877,0.99 3037,The Wanderer,240,1,1,U2; Bono,283951,9258717,0.99 3038,Breakfast In Bed,241,1,8,,196179,6513325,0.99 3039,Where Did I Go Wrong,241,1,8,,226742,7485054,0.99 3040,I Would Do For You,241,1,8,,334524,11193602,0.99 3041,Homely Girl,241,1,8,,203833,6790788,0.99 3042,Here I Am (Come And Take Me),241,1,8,,242102,8106249,0.99 3043,Kingston Town,241,1,8,,226951,7638236,0.99 3044,Wear You To The Ball,241,1,8,,213342,7159527,0.99 3045,(I Can't Help) Falling In Love With You,241,1,8,,207568,6905623,0.99 3046,Higher Ground,241,1,8,,260179,8665244,0.99 3047,Bring Me Your Cup,241,1,8,,341498,11346114,0.99 3048,C'est La Vie,241,1,8,,270053,9031661,0.99 3049,Reggae Music,241,1,8,,245106,8203931,0.99 3050,Superstition,241,1,8,,319582,10728099,0.99 3051,Until My Dying Day,241,1,8,,235807,7886195,0.99 3052,Where Have All The Good Times Gone?,242,1,1,Ray Davies,186723,6063937,0.99 3053,Hang 'Em High,242,1,1,Alex Van Halen/David Lee Roth/Edward Van Halen/Michael Anthony,210259,6872314,0.99 3054,Cathedral,242,1,1,Alex Van Halen/David Lee Roth/Edward Van Halen/Michael Anthony,82860,2650998,0.99 3055,Secrets,242,1,1,Alex Van Halen/David Lee Roth/Edward Van Halen/Michael Anthony,206968,6803255,0.99 3056,Intruder,242,1,1,Alex Van Halen/David Lee Roth/Edward Van Halen/Michael Anthony,100153,3282142,0.99 3057,(Oh) Pretty Woman,242,1,1,Bill Dees/Roy Orbison,174680,5665828,0.99 3058,Dancing In The Street,242,1,1,Ivy Jo Hunter/Marvin Gaye/William Stevenson,225985,7461499,0.99 3059,Little Guitars (Intro),242,1,1,Alex Van Halen/David Lee Roth/Edward Van Halen/Michael Anthony,42240,1439530,0.99 3060,Little Guitars,242,1,1,Alex Van Halen/David Lee Roth/Edward Van Halen/Michael Anthony,228806,7453043,0.99 3061,Big Bad Bill (Is Sweet William Now),242,1,1,Jack Yellen/Milton Ager,165146,5489609,0.99 3062,The Full Bug,242,1,1,Alex Van Halen/David Lee Roth/Edward Van Halen/Michael Anthony,201116,6551013,0.99 3063,Happy Trails,242,1,1,Dale Evans,65488,2111141,0.99 3064,Eruption,243,1,1,"Edward Van Halen, Alex Van Halen, David Lee Roth, Michael Anthony",102164,3272891,0.99 3065,Ain't Talkin' 'bout Love,243,1,1,"Edward Van Halen, Alex Van Halen, David Lee Roth, Michael Anthony",228336,7569506,0.99 3066,Runnin' With The Devil,243,1,1,"Edward Van Halen, Alex Van Halen, David Lee Roth, Michael Anthony",215902,7061901,0.99 3067,Dance the Night Away,243,1,1,"Edward Van Halen, Alex Van Halen, David Lee Roth, Michael Anthony",185965,6087433,0.99 3068,And the Cradle Will Rock...,243,1,1,"Edward Van Halen, Alex Van Halen, David Lee Roth, Michael Anthony",213968,7011402,0.99 3069,Unchained,243,1,1,"Edward Van Halen, Alex Van Halen, David Lee Roth, Michael Anthony",208953,6777078,0.99 3070,Jump,243,1,1,"Edward Van Halen, Alex Van Halen, David Lee Roth",241711,7911090,0.99 3071,Panama,243,1,1,"Edward Van Halen, Alex Van Halen, David Lee Roth",211853,6921784,0.99 3072,Why Can't This Be Love,243,1,1,Van Halen,227761,7457655,0.99 3073,Dreams,243,1,1,"Edward Van Halen, Alex Van Halen, Michael Anthony,/Edward Van Halen, Alex Van Halen, Michael Anthony, Sammy Hagar",291813,9504119,0.99 3074,When It's Love,243,1,1,"Edward Van Halen, Alex Van Halen, Michael Anthony,/Edward Van Halen, Alex Van Halen, Michael Anthony, Sammy Hagar",338991,11049966,0.99 3075,Poundcake,243,1,1,"Edward Van Halen, Alex Van Halen, Michael Anthony,/Edward Van Halen, Alex Van Halen, Michael Anthony, Sammy Hagar",321854,10366978,0.99 3076,Right Now,243,1,1,Van Halen,321828,10503352,0.99 3077,Can't Stop Loving You,243,1,1,Van Halen,248502,8107896,0.99 3078,Humans Being,243,1,1,"Edward Van Halen, Alex Van Halen, Michael Anthony,/Edward Van Halen, Alex Van Halen, Michael Anthony, Sammy Hagar",308950,10014683,0.99 3079,Can't Get This Stuff No More,243,1,1,"Edward Van Halen, Alex Van Halen, Michael Anthony,/Edward Van Halen, Alex Van Halen, Michael Anthony, David Lee Roth",315376,10355753,0.99 3080,Me Wise Magic,243,1,1,"Edward Van Halen, Alex Van Halen, Michael Anthony,/Edward Van Halen, Alex Van Halen, Michael Anthony, David Lee Roth",366053,12013467,0.99 3081,Runnin' With The Devil,244,1,1,"Edward Van Halen, Alex Van Halen, Michael Anthony and David Lee Roth",216032,7056863,0.99 3082,Eruption,244,1,1,"Edward Van Halen, Alex Van Halen, Michael Anthony and David Lee Roth",102556,3286026,0.99 3083,You Really Got Me,244,1,1,Ray Davies,158589,5194092,0.99 3084,Ain't Talkin' 'Bout Love,244,1,1,"Edward Van Halen, Alex Van Halen, Michael Anthony and David Lee Roth",230060,7617284,0.99 3085,I'm The One,244,1,1,"Edward Van Halen, Alex Van Halen, Michael Anthony and David Lee Roth",226507,7373922,0.99 3086,Jamie's Cryin',244,1,1,"Edward Van Halen, Alex Van Halen, Michael Anthony and David Lee Roth",210546,6946086,0.99 3087,Atomic Punk,244,1,1,"Edward Van Halen, Alex Van Halen, Michael Anthony and David Lee Roth",182073,5908861,0.99 3088,Feel Your Love Tonight,244,1,1,"Edward Van Halen, Alex Van Halen, Michael Anthony and David Lee Roth",222850,7293608,0.99 3089,Little Dreamer,244,1,1,"Edward Van Halen, Alex Van Halen, Michael Anthony and David Lee Roth",203258,6648122,0.99 3090,Ice Cream Man,244,1,1,John Brim,200306,6573145,0.99 3091,On Fire,244,1,1,"Edward Van Halen, Alex Van Halen, Michael Anthony and David Lee Roth",180636,5879235,0.99 3092,Neworld,245,1,1,Van Halen,105639,3495897,0.99 3093,Without You,245,1,1,Van Halen,390295,12619558,0.99 3094,One I Want,245,1,1,Van Halen,330788,10743970,0.99 3095,From Afar,245,1,1,Van Halen,324414,10524554,0.99 3096,Dirty Water Dog,245,1,1,Van Halen,327392,10709202,0.99 3097,Once,245,1,1,Van Halen,462837,15378082,0.99 3098,Fire in the Hole,245,1,1,Van Halen,331728,10846768,0.99 3099,Josephina,245,1,1,Van Halen,342491,11161521,0.99 3100,Year to the Day,245,1,1,Van Halen,514612,16621333,0.99 3101,Primary,245,1,1,Van Halen,86987,2812555,0.99 3102,Ballot or the Bullet,245,1,1,Van Halen,342282,11212955,0.99 3103,How Many Say I,245,1,1,Van Halen,363937,11716855,0.99 3104,Sucker Train Blues,246,1,1,"Dave Kushner, Duff, Matt Sorum, Scott Weiland & Slash",267859,8738780,0.99 3105,Do It For The Kids,246,1,1,"Dave Kushner, Duff, Matt Sorum, Scott Weiland & Slash",235911,7693331,0.99 3106,Big Machine,246,1,1,"Dave Kushner, Duff, Matt Sorum, Scott Weiland & Slash",265613,8673442,0.99 3107,Illegal I Song,246,1,1,"Dave Kushner, Duff, Matt Sorum, Scott Weiland & Slash",257750,8483347,0.99 3108,Spectacle,246,1,1,"Dave Kushner, Duff, Matt Sorum, Scott Weiland & Slash",221701,7252876,0.99 3109,Fall To Pieces,246,1,1,"Dave Kushner, Duff, Matt Sorum, Scott Weiland & Slash",270889,8823096,0.99 3110,Headspace,246,1,1,"Dave Kushner, Duff, Matt Sorum, Scott Weiland & Slash",223033,7237986,0.99 3111,Superhuman,246,1,1,"Dave Kushner, Duff, Matt Sorum, Scott Weiland & Slash",255921,8365328,0.99 3112,Set Me Free,246,1,1,"Dave Kushner, Duff, Matt Sorum, Scott Weiland & Slash",247954,8053388,0.99 3113,You Got No Right,246,1,1,"Dave Kushner, Duff, Matt Sorum, Scott Weiland & Slash",335412,10991094,0.99 3114,Slither,246,1,1,"Dave Kushner, Duff, Matt Sorum, Scott Weiland & Slash",248398,8118785,0.99 3115,Dirty Little Thing,246,1,1,"Dave Kushner, Duff, Keith Nelson, Matt Sorum, Scott Weiland & Slash",237844,7732982,0.99 3116,Loving The Alien,246,1,1,"Dave Kushner, Duff, Matt Sorum, Scott Weiland & Slash",348786,11412762,0.99 3117,Pela Luz Dos Olhos Teus,247,1,7,,119196,3905715,0.99 3118,A Bencao E Outros,247,1,7,,421093,14234427,0.99 3119,Tudo Na Mais Santa Paz,247,1,7,,222406,7426757,0.99 3120,O Velho E Aflor,247,1,7,,275121,9126828,0.99 3121,Cotidiano N 2,247,1,7,,55902,1805797,0.99 3122,Adeus,247,1,7,,221884,7259351,0.99 3123,Samba Pra Endrigo,247,1,7,,259265,8823551,0.99 3124,So Por Amor,247,1,7,,236591,7745764,0.99 3125,Meu Pranto Rolou,247,1,7,,181760,6003345,0.99 3126,Mulher Carioca,247,1,7,,191686,6395048,0.99 3127,Um Homem Chamado Alfredo,247,1,7,,151640,4976227,0.99 3128,Samba Do Jato,247,1,7,,220813,7357840,0.99 3129,"Oi, La",247,1,7,,167053,5562700,0.99 3130,"Vinicius, Poeta Do Encontro",247,1,7,,336431,10858776,0.99 3131,Soneto Da Separacao,247,1,7,,193880,6277511,0.99 3132,Still Of The Night,141,1,3,Sykes,398210,13043817,0.99 3133,Here I Go Again,141,1,3,Marsden,233874,7652473,0.99 3134,Is This Love,141,1,3,Sykes,283924,9262360,0.99 3135,Love Ain't No Stranger,141,1,3,Galley,259395,8490428,0.99 3136,Looking For Love,141,1,3,Sykes,391941,12769847,0.99 3137,Now You're Gone,141,1,3,Vandenberg,251141,8162193,0.99 3138,Slide It In,141,1,3,Coverdale,202475,6615152,0.99 3139,Slow An' Easy,141,1,3,Moody,367255,11961332,0.99 3140,Judgement Day,141,1,3,Vandenberg,317074,10326997,0.99 3141,You're Gonna Break My Hart Again,141,1,3,Sykes,250853,8176847,0.99 3142,The Deeper The Love,141,1,3,Vandenberg,262791,8606504,0.99 3143,Crying In The Rain,141,1,3,Coverdale,337005,10931921,0.99 3144,Fool For Your Loving,141,1,3,Marsden/Moody,250801,8129820,0.99 3145,Sweet Lady Luck,141,1,3,Vandenberg,273737,8919163,0.99 3146,Faixa Amarela,248,1,7,Beto Gogo/Jessé Pai/Luiz Carlos/Zeca Pagodinho,240692,8082036,0.99 3147,Posso Até Me Apaixonar,248,1,7,Dudu Nobre,200698,6735526,0.99 3148,Não Sou Mais Disso,248,1,7,Jorge Aragão/Zeca Pagodinho,225985,7613817,0.99 3149,Vivo Isolado Do Mundo,248,1,7,Alcides Dias Lopes,180035,6073995,0.99 3150,Coração Em Desalinho,248,1,7,Mauro Diniz/Ratino Sigem,185208,6225948,0.99 3151,Seu Balancê,248,1,7,Paulinho Rezende/Toninho Geraes,219454,7311219,0.99 3152,Vai Adiar,248,1,7,Alcino Corrêa/Monarco,270393,9134882,0.99 3153,Rugas,248,1,7,Augusto Garcez/Nelson Cavaquinho,140930,4703182,0.99 3154,Feirinha da Pavuna/Luz do Repente/Bagaço da Laranja,248,1,7,"Arlindo Cruz/Franco/Marquinhos PQD/Negro, Jovelina Pérolo/Zeca Pagodinho",107206,3593684,0.99 3155,Sem Essa de Malandro Agulha,248,1,7,Aldir Blanc/Jayme Vignoli,158484,5332668,0.99 3156,Chico Não Vai na Corimba,248,1,7,Dudu Nobre/Zeca Pagodinho,269374,9122188,0.99 3157,Papel Principal,248,1,7,Almir Guineto/Dedé Paraiso/Luverci Ernesto,217495,7325302,0.99 3158,Saudade Louca,248,1,7,Acyr Marques/Arlindo Cruz/Franco,243591,8136475,0.99 3159,Camarão que Dorme e Onda Leva,248,1,7,"Acyi Marques/Arlindo Bruz/Braço, Beto Sem/Zeca Pagodinho",299102,10012231,0.99 3160,Sapopemba e Maxambomba,248,1,7,Nei Lopes/Wilson Moreira,245394,8268712,0.99 3161,Minha Fé,248,1,7,Murilão,206994,6981474,0.99 3162,Lua de Ogum,248,1,7,Ratinho/Zeca Pagodinho,168463,5719129,0.99 3163,Samba pras moças,248,1,7,Grazielle/Roque Ferreira,152816,5121366,0.99 3164,Verdade,248,1,7,Carlinhos Santana/Nelson Rufino,332826,11120708,0.99 3165,The Brig,229,3,21,,2617325,488919543,1.99 3166,.07%,228,3,21,,2585794,541715199,1.99 3167,Five Years Gone,228,3,21,,2587712,530551890,1.99 3168,The Hard Part,228,3,21,,2601017,475996611,1.99 3169,The Man Behind the Curtain,229,3,21,,2615990,493951081,1.99 3170,Greatest Hits,229,3,21,,2617117,522102916,1.99 3171,Landslide,228,3,21,,2600725,518677861,1.99 3172,The Office: An American Workplace (Pilot),249,3,19,,1380833,290482361,1.99 3173,Diversity Day,249,3,19,,1306416,257879716,1.99 3174,Health Care,249,3,19,,1321791,260493577,1.99 3175,The Alliance,249,3,19,,1317125,266203162,1.99 3176,Basketball,249,3,19,,1323541,267464180,1.99 3177,Hot Girl,249,3,19,,1325458,267836576,1.99 3178,The Dundies,250,3,19,,1253541,246845576,1.99 3179,Sexual Harassment,250,3,19,,1294541,273069146,1.99 3180,Office Olympics,250,3,19,,1290458,256247623,1.99 3181,The Fire,250,3,19,,1288166,266856017,1.99 3182,Halloween,250,3,19,,1315333,249205209,1.99 3183,The Fight,250,3,19,,1320028,277149457,1.99 3184,The Client,250,3,19,,1299341,253836788,1.99 3185,Performance Review,250,3,19,,1292458,256143822,1.99 3186,Email Surveillance,250,3,19,,1328870,265101113,1.99 3187,Christmas Party,250,3,19,,1282115,260891300,1.99 3188,Booze Cruise,250,3,19,,1267958,252518021,1.99 3189,The Injury,250,3,19,,1275275,253912762,1.99 3190,The Secret,250,3,19,,1264875,253143200,1.99 3191,The Carpet,250,3,19,,1264375,256477011,1.99 3192,Boys and Girls,250,3,19,,1278333,255245729,1.99 3193,Valentine's Day,250,3,19,,1270375,253552710,1.99 3194,Dwight's Speech,250,3,19,,1278041,255001728,1.99 3195,Take Your Daughter to Work Day,250,3,19,,1268333,253451012,1.99 3196,Michael's Birthday,250,3,19,,1237791,247238398,1.99 3197,Drug Testing,250,3,19,,1278625,244626927,1.99 3198,Conflict Resolution,250,3,19,,1274583,253808658,1.99 3199,Casino Night - Season Finale,250,3,19,,1712791,327642458,1.99 3200,Gay Witch Hunt,251,3,19,,1326534,276942637,1.99 3201,The Convention,251,3,19,,1297213,255117055,1.99 3202,The Coup,251,3,19,,1276526,267205501,1.99 3203,Grief Counseling,251,3,19,,1282615,256912833,1.99 3204,The Initiation,251,3,19,,1280113,251728257,1.99 3205,Diwali,251,3,19,,1279904,252726644,1.99 3206,Branch Closing,251,3,19,,1822781,358761786,1.99 3207,The Merger,251,3,19,,1801926,345960631,1.99 3208,The Convict,251,3,22,,1273064,248863427,1.99 3209,"A Benihana Christmas, Pts. 1 & 2",251,3,22,,2519436,515301752,1.99 3210,Back from Vacation,251,3,22,,1271688,245378749,1.99 3211,Traveling Salesmen,251,3,22,,1289039,250822697,1.99 3212,Producer's Cut: The Return,251,3,22,,1700241,337219980,1.99 3213,Ben Franklin,251,3,22,,1271938,264168080,1.99 3214,Phyllis's Wedding,251,3,22,,1271521,258561054,1.99 3215,Business School,251,3,22,,1302093,254402605,1.99 3216,Cocktails,251,3,22,,1272522,259011909,1.99 3217,The Negotiation,251,3,22,,1767851,371663719,1.99 3218,Safety Training,251,3,22,,1271229,253054534,1.99 3219,Product Recall,251,3,22,,1268268,251208610,1.99 3220,Women's Appreciation,251,3,22,,1732649,338778844,1.99 3221,Beach Games,251,3,22,,1676134,333671149,1.99 3222,The Job,251,3,22,,2541875,501060138,1.99 3223,How to Stop an Exploding Man,228,3,21,,2687103,487881159,1.99 3224,Through a Looking Glass,229,3,21,,5088838,1059546140,1.99 3225,Your Time Is Gonna Come,252,2,1,"Page, Jones",310774,5126563,0.99 3226,"Battlestar Galactica, Pt. 1",253,3,20,,2952702,541359437,1.99 3227,"Battlestar Galactica, Pt. 2",253,3,20,,2956081,521387924,1.99 3228,"Battlestar Galactica, Pt. 3",253,3,20,,2927802,554509033,1.99 3229,"Lost Planet of the Gods, Pt. 1",253,3,20,,2922547,537812711,1.99 3230,"Lost Planet of the Gods, Pt. 2",253,3,20,,2914664,534343985,1.99 3231,The Lost Warrior,253,3,20,,2920045,558872190,1.99 3232,The Long Patrol,253,3,20,,2925008,513122217,1.99 3233,"The Gun On Ice Planet Zero, Pt. 1",253,3,20,,2907615,540980196,1.99 3234,"The Gun On Ice Planet Zero, Pt. 2",253,3,20,,2924341,546542281,1.99 3235,The Magnificent Warriors,253,3,20,,2924716,570152232,1.99 3236,The Young Lords,253,3,20,,2863571,587051735,1.99 3237,"The Living Legend, Pt. 1",253,3,20,,2924507,503641007,1.99 3238,"The Living Legend, Pt. 2",253,3,20,,2923298,515632754,1.99 3239,Fire In Space,253,3,20,,2926593,536784757,1.99 3240,"War of the Gods, Pt. 1",253,3,20,,2922630,505761343,1.99 3241,"War of the Gods, Pt. 2",253,3,20,,2923381,487899692,1.99 3242,The Man With Nine Lives,253,3,20,,2956998,577829804,1.99 3243,Murder On the Rising Star,253,3,20,,2935894,551759986,1.99 3244,"Greetings from Earth, Pt. 1",253,3,20,,2960293,536824558,1.99 3245,"Greetings from Earth, Pt. 2",253,3,20,,2903778,527842860,1.99 3246,Baltar's Escape,253,3,20,,2922088,525564224,1.99 3247,Experiment In Terra,253,3,20,,2923548,547982556,1.99 3248,Take the Celestra,253,3,20,,2927677,512381289,1.99 3249,The Hand of God,253,3,20,,2924007,536583079,1.99 3250,Pilot,254,3,19,,2484567,492670102,1.99 3251,"Through the Looking Glass, Pt. 2",229,3,21,,2617117,550943353,1.99 3252,"Through the Looking Glass, Pt. 1",229,3,21,,2610860,493211809,1.99 3253,Instant Karma,255,2,9,,193188,3150090,0.99 3254,#9 Dream,255,2,9,,278312,4506425,0.99 3255,Mother,255,2,9,,287740,4656660,0.99 3256,Give Peace a Chance,255,2,9,,274644,4448025,0.99 3257,Cold Turkey,255,2,9,,281424,4556003,0.99 3258,Whatever Gets You Thru the Night,255,2,9,,215084,3499018,0.99 3259,I'm Losing You,255,2,9,,240719,3907467,0.99 3260,Gimme Some Truth,255,2,9,,232778,3780807,0.99 3261,"Oh, My Love",255,2,9,,159473,2612788,0.99 3262,Imagine,255,2,9,,192329,3136271,0.99 3263,Nobody Told Me,255,2,9,,210348,3423395,0.99 3264,Jealous Guy,255,2,9,,239094,3881620,0.99 3265,Working Class Hero,255,2,9,,265449,4301430,0.99 3266,Power to the People,255,2,9,,213018,3466029,0.99 3267,Imagine,255,2,9,,219078,3562542,0.99 3268,Beautiful Boy,255,2,9,,227995,3704642,0.99 3269,Isolation,255,2,9,,156059,2558399,0.99 3270,Watching the Wheels,255,2,9,,198645,3237063,0.99 3271,Grow Old With Me,255,2,9,,149093,2447453,0.99 3272,Gimme Some Truth,255,2,9,,187546,3060083,0.99 3273,[Just Like] Starting Over,255,2,9,,215549,3506308,0.99 3274,God,255,2,9,,260410,4221135,0.99 3275,Real Love,255,2,9,,236911,3846658,0.99 3276,Sympton of the Universe,256,2,1,,340890,5489313,0.99 3277,Snowblind,256,2,1,,295960,4773171,0.99 3278,Black Sabbath,256,2,1,,364180,5860455,0.99 3279,Fairies Wear Boots,256,2,1,,392764,6315916,0.99 3280,War Pigs,256,2,1,,515435,8270194,0.99 3281,The Wizard,256,2,1,,282678,4561796,0.99 3282,N.I.B.,256,2,1,,335248,5399456,0.99 3283,Sweet Leaf,256,2,1,,354706,5709700,0.99 3284,Never Say Die,256,2,1,,258343,4173799,0.99 3285,"Sabbath, Bloody Sabbath",256,2,1,,333622,5373633,0.99 3286,Iron Man/Children of the Grave,256,2,1,,552308,8858616,0.99 3287,Paranoid,256,2,1,,189171,3071042,0.99 3288,Rock You Like a Hurricane,257,2,1,,255766,4300973,0.99 3289,No One Like You,257,2,1,,240325,4050259,0.99 3290,The Zoo,257,2,1,,332740,5550779,0.99 3291,Loving You Sunday Morning,257,2,1,,339125,5654493,0.99 3292,Still Loving You,257,2,1,,390674,6491444,0.99 3293,Big City Nights,257,2,1,,251865,4237651,0.99 3294,Believe in Love,257,2,1,,325774,5437651,0.99 3295,Rhythm of Love,257,2,1,,231246,3902834,0.99 3296,I Can't Explain,257,2,1,,205332,3482099,0.99 3297,Tease Me Please Me,257,2,1,,287229,4811894,0.99 3298,Wind of Change,257,2,1,,315325,5268002,0.99 3299,Send Me an Angel,257,2,1,,273041,4581492,0.99 3300,Jump Around,258,1,17,E. Schrody/L. Muggerud,217835,8715653,0.99 3301,Salutations,258,1,17,E. Schrody/L. Dimant,69120,2767047,0.99 3302,Put Your Head Out,258,1,17,E. Schrody/L. Freese/L. Muggerud,182230,7291473,0.99 3303,Top O' The Morning To Ya,258,1,17,E. Schrody/L. Dimant,216633,8667599,0.99 3304,Commercial 1,258,1,17,L. Muggerud,7941,319888,0.99 3305,House And The Rising Sun,258,1,17,E. Schrody/J. Vasquez/L. Dimant,219402,8778369,0.99 3306,Shamrocks And Shenanigans,258,1,17,E. Schrody/L. Dimant,218331,8735518,0.99 3307,House Of Pain Anthem,258,1,17,E. Schrody/L. Dimant,155611,6226713,0.99 3308,"Danny Boy, Danny Boy",258,1,17,E. Schrody/L. Muggerud,114520,4583091,0.99 3309,Guess Who's Back,258,1,17,E. Schrody/L. Muggerud,238393,9537994,0.99 3310,Commercial 2,258,1,17,L. Muggerud,21211,850698,0.99 3311,Put On Your Shit Kickers,258,1,17,E. Schrody/L. Muggerud,190432,7619569,0.99 3312,Come And Get Some Of This,258,1,17,E. Schrody/L. Muggerud/R. Medrano,170475,6821279,0.99 3313,Life Goes On,258,1,17,E. Schrody/R. Medrano,163030,6523458,0.99 3314,One For The Road,258,1,17,E. Schrody/L. Dimant/L. Muggerud,170213,6810820,0.99 3315,Feel It,258,1,17,E. Schrody/R. Medrano,239908,9598588,0.99 3316,All My Love,258,1,17,E. Schrody/L. Dimant,200620,8027065,0.99 3317,Jump Around (Pete Rock Remix),258,1,17,E. Schrody/L. Muggerud,236120,9447101,0.99 3318,Shamrocks And Shenanigans (Boom Shalock Lock Boom/Butch Vig Mix),258,1,17,E. Schrody/L. Dimant,237035,9483705,0.99 3319,Instinto Colectivo,259,1,15,,300564,12024875,0.99 3320,Chapa o Coco,259,1,15,,143830,5755478,0.99 3321,Prostituta,259,1,15,,359000,14362307,0.99 3322,Eu So Queria Sumir,259,1,15,,269740,10791921,0.99 3323,Tres Reis,259,1,15,,304143,12168015,0.99 3324,Um Lugar ao Sol,259,1,15,,212323,8495217,0.99 3325,Batalha Naval,259,1,15,,285727,11431382,0.99 3326,Todo o Carnaval tem seu Fim,259,1,15,,237426,9499371,0.99 3327,O Misterio do Samba,259,1,15,,226142,9047970,0.99 3328,Armadura,259,1,15,,232881,9317533,0.99 3329,Na Ladeira,259,1,15,,221570,8865099,0.99 3330,Carimbo,259,1,15,,328751,13152314,0.99 3331,Catimbo,259,1,15,,254484,10181692,0.99 3332,Funk de Bamba,259,1,15,,237322,9495184,0.99 3333,Chega no Suingue,259,1,15,,221805,8874509,0.99 3334,Mun-Ra,259,1,15,,274651,10988338,0.99 3335,Freestyle Love,259,1,15,,318484,12741680,0.99 3336,War Pigs,260,4,23,,234013,8052374,0.99 3337,"Past, Present, and Future",261,3,21,,2492867,490796184,1.99 3338,The Beginning of the End,261,3,21,,2611903,526865050,1.99 3339,LOST Season 4 Trailer,261,3,21,,112712,20831818,1.99 3340,LOST In 8:15,261,3,21,,497163,98460675,1.99 3341,Confirmed Dead,261,3,21,,2611986,512168460,1.99 3342,The Economist,261,3,21,,2609025,516934914,1.99 3343,Eggtown,261,3,19,,2608817,501061240,1.99 3344,The Constant,261,3,21,,2611569,520209363,1.99 3345,The Other Woman,261,3,21,,2605021,513246663,1.99 3346,Ji Yeon,261,3,19,,2588797,506458858,1.99 3347,Meet Kevin Johnson,261,3,19,,2612028,504132981,1.99 3348,The Shape of Things to Come,261,3,21,,2591299,502284266,1.99 3349,Amanda,262,5,2,Luca Gusella,246503,4011615,0.99 3350,Despertar,262,5,2,Andrea Dulbecco,307385,4821485,0.99 3351,Din Din Wo (Little Child),263,5,16,Habib Koité,285837,4615841,0.99 3352,Distance,264,5,15,Karsh Kale/Vishal Vaid,327122,5327463,0.99 3353,I Guess You're Right,265,5,1,"Darius ""Take One"" Minwalla/Jon Auer/Ken Stringfellow/Matt Harris",212044,3453849,0.99 3354,I Ka Barra (Your Work),263,5,16,Habib Koité,300605,4855457,0.99 3355,Love Comes,265,5,1,"Darius ""Take One"" Minwalla/Jon Auer/Ken Stringfellow/Matt Harris",199923,3240609,0.99 3356,Muita Bobeira,266,5,7,Luciana Souza,172710,2775071,0.99 3357,OAM's Blues,267,5,2,Aaron Goldberg,266936,4292028,0.99 3358,One Step Beyond,264,5,15,Karsh Kale,366085,6034098,0.99 3359,"Symphony No. 3 in E-flat major, Op. 55, ""Eroica"" - Scherzo: Allegro Vivace",268,5,24,Ludwig van Beethoven,356426,5817216,0.99 3360,Something Nice Back Home,261,3,21,,2612779,484711353,1.99 3361,Cabin Fever,261,3,21,,2612028,477733942,1.99 3362,"There's No Place Like Home, Pt. 1",261,3,21,,2609526,522919189,1.99 3363,"There's No Place Like Home, Pt. 2",261,3,21,,2497956,523748920,1.99 3364,"There's No Place Like Home, Pt. 3",261,3,21,,2582957,486161766,1.99 3365,Say Hello 2 Heaven,269,2,23,,384497,6477217,0.99 3366,Reach Down,269,2,23,,672773,11157785,0.99 3367,Hunger Strike,269,2,23,,246292,4233212,0.99 3368,Pushin Forward Back,269,2,23,,225278,3892066,0.99 3369,Call Me a Dog,269,2,23,,304458,5177612,0.99 3370,Times of Trouble,269,2,23,,342539,5795951,0.99 3371,Wooden Jesus,269,2,23,,250565,4302603,0.99 3372,Your Savior,269,2,23,,244226,4199626,0.99 3373,Four Walled World,269,2,23,,414474,6964048,0.99 3374,All Night Thing,269,2,23,,231803,3997982,0.99 3375,No Such Thing,270,2,23,Chris Cornell,224837,3691272,0.99 3376,Poison Eye,270,2,23,Chris Cornell,237120,3890037,0.99 3377,Arms Around Your Love,270,2,23,Chris Cornell,214016,3516224,0.99 3378,Safe and Sound,270,2,23,Chris Cornell,256764,4207769,0.99 3379,She'll Never Be Your Man,270,2,23,Chris Cornell,204078,3355715,0.99 3380,Ghosts,270,2,23,Chris Cornell,231547,3799745,0.99 3381,Killing Birds,270,2,23,Chris Cornell,218498,3588776,0.99 3382,Billie Jean,270,2,23,Michael Jackson,281401,4606408,0.99 3383,Scar On the Sky,270,2,23,Chris Cornell,220193,3616618,0.99 3384,Your Soul Today,270,2,23,Chris Cornell,205959,3385722,0.99 3385,Finally Forever,270,2,23,Chris Cornell,217035,3565098,0.99 3386,Silence the Voices,270,2,23,Chris Cornell,267376,4379597,0.99 3387,Disappearing Act,270,2,23,Chris Cornell,273320,4476203,0.99 3388,You Know My Name,270,2,23,Chris Cornell,240255,3940651,0.99 3389,Revelations,271,2,23,,252376,4111051,0.99 3390,One and the Same,271,2,23,,217732,3559040,0.99 3391,Sound of a Gun,271,2,23,,260154,4234990,0.99 3392,Until We Fall,271,2,23,,230758,3766605,0.99 3393,Original Fire,271,2,23,,218916,3577821,0.99 3394,Broken City,271,2,23,,228366,3728955,0.99 3395,Somedays,271,2,23,,213831,3497176,0.99 3396,Shape of Things to Come,271,2,23,,274597,4465399,0.99 3397,Jewel of the Summertime,271,2,23,,233242,3806103,0.99 3398,Wide Awake,271,2,23,,266308,4333050,0.99 3399,Nothing Left to Say But Goodbye,271,2,23,,213041,3484335,0.99 3400,Moth,271,2,23,,298049,4838884,0.99 3401,Show Me How to Live (Live at the Quart Festival),271,2,23,,301974,4901540,0.99 3402,"Band Members Discuss Tracks from ""Revelations""",271,3,23,,294294,61118891,0.99 3403,Intoitus: Adorate Deum,272,2,24,Anonymous,245317,4123531,0.99 3404,"Miserere mei, Deus",273,2,24,Gregorio Allegri,501503,8285941,0.99 3405,Canon and Gigue in D Major: I. Canon,274,2,24,Johann Pachelbel,271788,4438393,0.99 3406,"Concerto No. 1 in E Major, RV 269 ""Spring"": I. Allegro",275,2,24,Antonio Vivaldi,199086,3347810,0.99 3407,"Concerto for 2 Violins in D Minor, BWV 1043: I. Vivace",276,2,24,Johann Sebastian Bach,193722,3192890,0.99 3408,"Aria Mit 30 Veränderungen, BWV 988 ""Goldberg Variations"": Aria",277,2,24,Johann Sebastian Bach,120463,2081895,0.99 3409,"Suite for Solo Cello No. 1 in G Major, BWV 1007: I. Prélude",278,2,24,Johann Sebastian Bach,143288,2315495,0.99 3410,"The Messiah: Behold, I Tell You a Mystery... The Trumpet Shall Sound",279,2,24,George Frideric Handel,582029,9553140,0.99 3411,Solomon HWV 67: The Arrival of the Queen of Sheba,280,2,24,George Frideric Handel,197135,3247914,0.99 3412,"""Eine Kleine Nachtmusik"" Serenade In G, K. 525: I. Allegro",281,2,24,Wolfgang Amadeus Mozart,348971,5760129,0.99 3413,"Concerto for Clarinet in A Major, K. 622: II. Adagio",282,2,24,Wolfgang Amadeus Mozart,394482,6474980,0.99 3414,"Symphony No. 104 in D Major ""London"": IV. Finale: Spiritoso",283,4,24,Franz Joseph Haydn,306687,10085867,0.99 3415,Symphony No.5 in C Minor: I. Allegro con brio,284,2,24,Ludwig van Beethoven,392462,6419730,0.99 3416,Ave Maria,285,2,24,Franz Schubert,338243,5605648,0.99 3417,"Nabucco: Chorus, ""Va, Pensiero, Sull'ali Dorate""",286,2,24,Giuseppe Verdi,274504,4498583,0.99 3418,Die Walküre: The Ride of the Valkyries,287,2,24,Richard Wagner,189008,3114209,0.99 3419,"Requiem, Op.48: 4. Pie Jesu",288,2,24,Gabriel Fauré,258924,4314850,0.99 3420,"The Nutcracker, Op. 71a, Act II: Scene 14: Pas de deux: Dance of the Prince & the Sugar-Plum Fairy",289,2,24,Peter Ilyich Tchaikovsky,304226,5184289,0.99 3421,"Nimrod (Adagio) from Variations On an Original Theme, Op. 36 ""Enigma""",290,2,24,Edward Elgar,250031,4124707,0.99 3422,Madama Butterfly: Un Bel Dì Vedremo,291,2,24,Giacomo Puccini,277639,4588197,0.99 3423,"Jupiter, the Bringer of Jollity",292,2,24,Gustav Holst,522099,8547876,0.99 3424,"Turandot, Act III, Nessun dorma!",293,2,24,Giacomo Puccini,176911,2920890,0.99 3425,"Adagio for Strings from the String Quartet, Op. 11",294,2,24,Samuel Barber,596519,9585597,0.99 3426,Carmina Burana: O Fortuna,295,2,24,Carl Orff,156710,2630293,0.99 3427,Fanfare for the Common Man,296,2,24,Aaron Copland,198064,3211245,0.99 3428,Branch Closing,251,3,22,,1814855,360331351,1.99 3429,The Return,251,3,22,,1705080,343877320,1.99 3430,"Toccata and Fugue in D Minor, BWV 565: I. Toccata",297,2,24,Johann Sebastian Bach,153901,2649938,0.99 3431,"Symphony No.1 in D Major, Op.25 ""Classical"", Allegro Con Brio",298,2,24,Sergei Prokofiev,254001,4195542,0.99 3432,"Scheherazade, Op. 35: I. The Sea and Sindbad's Ship",299,2,24,Nikolai Rimsky-Korsakov,545203,8916313,0.99 3433,"Concerto No.2 in F Major, BWV1047, I. Allegro",300,2,24,Johann Sebastian Bach,307244,5064553,0.99 3434,"Concerto for Piano No. 2 in F Minor, Op. 21: II. Larghetto",301,2,24,Frédéric Chopin,560342,9160082,0.99 3435,Cavalleria Rusticana \ Act \ Intermezzo Sinfonico,302,2,24,Pietro Mascagni,243436,4001276,0.99 3436,"Karelia Suite, Op.11: 2. Ballade (Tempo Di Menuetto)",303,2,24,Jean Sibelius,406000,5908455,0.99 3437,"Piano Sonata No. 14 in C Sharp Minor, Op. 27, No. 2, ""Moonlight"": I. Adagio sostenuto",304,2,24,Ludwig van Beethoven,391000,6318740,0.99 3438,Fantasia On Greensleeves,280,2,24,Ralph Vaughan Williams,268066,4513190,0.99 3439,"Das Lied Von Der Erde, Von Der Jugend",305,2,24,Gustav Mahler,223583,3700206,0.99 3440,"Concerto for Cello and Orchestra in E minor, Op. 85: I. Adagio - Moderato",306,2,24,Edward Elgar,483133,7865479,0.99 3441,Two Fanfares for Orchestra: II. Short Ride in a Fast Machine,307,2,24,John Adams,254930,4310896,0.99 3442,"Wellington's Victory or the Battle Symphony, Op.91: 2. Symphony of Triumph",308,2,24,Ludwig van Beethoven,412000,6965201,0.99 3443,Missa Papae Marcelli: Kyrie,309,2,24,Giovanni Pierluigi da Palestrina,240666,4244149,0.99 3444,Romeo et Juliette: No. 11 - Danse des Chevaliers,310,2,24,,275015,4519239,0.99 3445,On the Beautiful Blue Danube,311,2,24,Johann Strauss II,526696,8610225,0.99 3446,"Symphonie Fantastique, Op. 14: V. Songe d'une nuit du sabbat",312,2,24,Hector Berlioz,561967,9173344,0.99 3447,Carmen: Overture,313,2,24,Georges Bizet,132932,2189002,0.99 3448,"Lamentations of Jeremiah, First Set \ Incipit Lamentatio",314,2,24,Thomas Tallis,69194,1208080,0.99 3449,"Music for the Royal Fireworks, HWV351 (1749): La Réjouissance",315,2,24,George Frideric Handel,120000,2193734,0.99 3450,"Peer Gynt Suite No.1, Op.46: 1. Morning Mood",316,2,24,Edvard Grieg,253422,4298769,0.99 3451,"Die Zauberflöte, K.620: ""Der Hölle Rache Kocht in Meinem Herze""",317,2,25,Wolfgang Amadeus Mozart,174813,2861468,0.99 3452,"SCRIABIN: Prelude in B Major, Op. 11, No. 11",318,4,24,,101293,3819535,0.99 3453,"Pavan, Lachrimae Antiquae",319,2,24,John Dowland,253281,4211495,0.99 3454,"Symphony No. 41 in C Major, K. 551, ""Jupiter"": IV. Molto allegro",320,2,24,Wolfgang Amadeus Mozart,362933,6173269,0.99 3455,Rehab,321,2,14,,213240,3416878,0.99 3456,You Know I'm No Good,321,2,14,,256946,4133694,0.99 3457,Me & Mr. Jones,321,2,14,,151706,2449438,0.99 3458,Just Friends,321,2,14,,191933,3098906,0.99 3459,Back to Black,321,2,14,Mark Ronson,240320,3852953,0.99 3460,Love Is a Losing Game,321,2,14,,154386,2509409,0.99 3461,Tears Dry On Their Own,321,2,14,Nickolas Ashford & Valerie Simpson,185293,2996598,0.99 3462,Wake Up Alone,321,2,14,Paul O'duffy,221413,3576773,0.99 3463,Some Unholy War,321,2,14,,141520,2304465,0.99 3464,He Can Only Hold Her,321,2,14,Richard Poindexter & Robert Poindexter,166680,2666531,0.99 3465,You Know I'm No Good (feat. Ghostface Killah),321,2,14,,202320,3260658,0.99 3466,Rehab (Hot Chip Remix),321,2,14,,418293,6670600,0.99 3467,Intro / Stronger Than Me,322,2,9,,234200,3832165,0.99 3468,You Sent Me Flying / Cherry,322,2,9,,409906,6657517,0.99 3469,F**k Me Pumps,322,2,9,Salaam Remi,200253,3324343,0.99 3470,I Heard Love Is Blind,322,2,9,,129666,2190831,0.99 3471,(There Is) No Greater Love (Teo Licks),322,2,9,Isham Jones & Marty Symes,167933,2773507,0.99 3472,In My Bed,322,2,9,Salaam Remi,315960,5211774,0.99 3473,Take the Box,322,2,9,Luke Smith,199160,3281526,0.99 3474,October Song,322,2,9,Matt Rowe & Stefan Skarbek,204846,3358125,0.99 3475,What Is It About Men,322,2,9,"Delroy ""Chris"" Cooper, Donovan Jackson, Earl Chinna Smith, Felix Howard, Gordon Williams, Luke Smith, Paul Watson & Wilburn Squiddley Cole",209573,3426106,0.99 3476,Help Yourself,322,2,9,"Freddy James, Jimmy hogarth & Larry Stock",300884,5029266,0.99 3477,Amy Amy Amy (Outro),322,2,9,"Astor Campbell, Delroy ""Chris"" Cooper, Donovan Jackson, Dorothy Fields, Earl Chinna Smith, Felix Howard, Gordon Williams, James Moody, Jimmy McHugh, Matt Rowe, Salaam Remi & Stefan Skarbek",663426,10564704,0.99 3478,Slowness,323,2,23,,215386,3644793,0.99 3479,"Prometheus Overture, Op. 43",324,4,24,Ludwig van Beethoven,339567,10887931,0.99 3480,Sonata for Solo Violin: IV: Presto,325,4,24,Béla Bartók,299350,9785346,0.99 3481,"A Midsummer Night's Dream, Op.61 Incidental Music: No.7 Notturno",326,2,24,,387826,6497867,0.99 3482,"Suite No. 3 in D, BWV 1068: III. Gavotte I & II",327,2,24,Johann Sebastian Bach,225933,3847164,0.99 3483,"Concert pour 4 Parties de V**les, H. 545: I. Prelude",328,2,24,Marc-Antoine Charpentier,110266,1973559,0.99 3484,Adios nonino,329,2,24,Astor Piazzolla,289388,4781384,0.99 3485,"Symphony No. 3 Op. 36 for Orchestra and Soprano ""Symfonia Piesni Zalosnych"" \ Lento E Largo - Tranquillissimo",330,2,24,Henryk Górecki,567494,9273123,0.99 3486,"Act IV, Symphony",331,2,24,Henry Purcell,364296,5987695,0.99 3487,"3 Gymnopédies: No.1 - Lent Et Grave, No.3 - Lent Et Douloureux",332,2,24,Erik Satie,385506,6458501,0.99 3488,"Music for the Funeral of Queen Mary: VI. ""Thou Knowest, Lord, the Secrets of Our Hearts""",333,2,24,Henry Purcell,142081,2365930,0.99 3489,Symphony No. 2: III. Allegro vivace,334,2,24,Kurt Weill,376510,6129146,0.99 3490,"Partita in E Major, BWV 1006A: I. Prelude",335,2,24,Johann Sebastian Bach,285673,4744929,0.99 3491,Le Sacre Du Printemps: I.iv. Spring Rounds,336,2,24,Igor Stravinsky,234746,4072205,0.99 3492,Sing Joyfully,314,2,24,William Byrd,133768,2256484,0.99 3493,"Metopes, Op. 29: Calypso",337,2,24,Karol Szymanowski,333669,5548755,0.99 3494,"Symphony No. 2, Op. 16 - ""The Four Temperaments"": II. Allegro Comodo e Flemmatico",338,2,24,Carl Nielsen,286998,4834785,0.99 3495,"24 Caprices, Op. 1, No. 24, for Solo Violin, in A Minor",339,2,24,Niccolò Paganini,265541,4371533,0.99 3496,"Étude 1, In C Major - Preludio (Presto) - Liszt",340,4,24,,51780,2229617,0.99 3497,"Erlkonig, D.328",341,2,24,,261849,4307907,0.99 3498,"Concerto for Violin, Strings and Continuo in G Major, Op. 3, No. 9: I. Allegro",342,4,24,Pietro Antonio Locatelli,493573,16454937,0.99 3499,Pini Di Roma (Pinien Von Rom) \ I Pini Della Via Appia,343,2,24,,286741,4718950,0.99 3500,"String Quartet No. 12 in C Minor, D. 703 ""Quartettsatz"": II. Andante - Allegro assai",344,2,24,Franz Schubert,139200,2283131,0.99 3501,"L'orfeo, Act 3, Sinfonia (Orchestra)",345,2,24,Claudio Monteverdi,66639,1189062,0.99 3502,"Quintet for Horn, Violin, 2 Violas, and Cello in E Flat Major, K. 407/386c: III. Allegro",346,2,24,Wolfgang Amadeus Mozart,221331,3665114,0.99 3503,Koyaanisqatsi,347,2,10,Philip Glass,206005,3305164,0.99 ================================================ FILE: prqlc/prqlc/tests/integration/dbs/README.md ================================================ # PRQL test-databases Test PRQL queries against various SQL RDBMS. ## In-process DBs To run tests against DuckDB & SQLite, no additional setup is required; simply run: ```sh cargo test --features=test-dbs ``` ## External DBs To run tests against external databases — currently Postgres, MySQL, SQL Server, ClickHouse and GlareDB are tested using `docker compose` to create the databases. The steps are all covered by `task test-rust-external-dbs`; to run them manually: 1. Run `docker compose up` (may take a while on the first time): ```sh cd prqlc/prqlc/tests/integration/dbs && docker compose up -d ``` 2. Run the tests: ```sh cargo test --features=test-dbs-external -- --nocapture ``` (The `--no-capture` option isn't required, but shows all the dialects tested per query.) 3. After it's done, remove the containers: ```sh cd prqlc/prqlc/tests/integration/dbs && docker compose down ``` Note: on an M1, if the MSSQL docker container doesn't run, refer to [this comment](https://github.com/microsoft/mssql-docker/issues/668#issuecomment-1436802153) ## Tested databases Tests are by default run on all the DBs with `SupportLevel::Supported`. To test on a DB that is not yet at this support level like `MSSQL`, simply add `# mssql:test` on top of the query. To ignore one of the supported DBs like `sqlite`, simply add `# sqlite:skip` on top of the query. ## Data Columns are renamed to `snake_case`, so Postgres and DuckDb don't struggle with them. For optimal accessibility, portability between databases and file size, all tables are stored as CSV files. Their current size is 432kB, it could be gzip-ed to 112kB, but that would require a preprocessing step before running `cargo test`. ## Queries For databases like ClickHouse, where the order of results is ambiguous, please use `sort` for test queries to to guarantee the order of rows across DBs. For example, instead of the following query: ```elm from albums ``` Use a query including `sort`: ```elm from albums sort album_id ``` ## Test organization We follow the advice in . ================================================ FILE: prqlc/prqlc/tests/integration/dbs/docker-compose.yaml ================================================ services: postgres: # These aren't tagged yet, since there's no dependabot support for # docker-compose yet: https://github.com/dependabot/dependabot-core/issues/390 image: "postgres:alpine" ports: - "5432:5432" environment: POSTGRES_DB: dummy POSTGRES_USER: root POSTGRES_PASSWORD: root volumes: &vol - ../data/chinook:/tmp/chinook:ro mysql: image: "mysql:oracle" ports: - "3306:3306" environment: MYSQL_DATABASE: dummy MYSQL_ROOT_PASSWORD: root command: --secure-file-priv="" volumes: *vol mssql: image: "mcr.microsoft.com/mssql/server" ports: - "1433:1433" # https://github.com/microsoft/mssql-docker/issues/668#issuecomment-1436802153 platform: linux/amd64 environment: ACCEPT_EULA: Y MSSQL_PID: Developer MSSQL_SA_PASSWORD: Wordpass123## LC_ALL: en_US.UTF-8 MSSQL_COLLATION: Latin1_General_100_CS_AI_SC_UTF8 volumes: *vol clickhouse: # TODO: unpinning this causes an error, would be good to unpin & fix. image: "clickhouse/clickhouse-server:23.12.4.15-alpine" ports: # 9004 is MySQL emulation port # https://clickhouse.com/docs/en/guides/sre/network-ports - "9004:9004" environment: CLICKHOUSE_DB: dummy # Skip `chown` to user_files_path # https://github.com/ClickHouse/ClickHouse/blob/01c7d2fe719f9b9ed59fce58d5e9dec44167e42f/docker/server/entrypoint.sh#L7-L9 CLICKHOUSE_DO_NOT_CHOWN: "1" volumes: # ClickHouse can load csv only from user_files_path (default `/var/lib/clickhouse/user_files/`) # https://clickhouse.com/docs/en/operations/server-configuration-parameters/settings#server_configuration_parameters-user_scripts_path - ../data/chinook:/var/lib/clickhouse/user_files/chinook/:ro # TODO: reenable # glaredb: # build: # context: dockerfiles # dockerfile: glaredb.Dockerfile # ports: # - "6543:6543" # volumes: *vol ================================================ FILE: prqlc/prqlc/tests/integration/dbs/dockerfiles/glaredb.Dockerfile ================================================ # syntax=docker/dockerfile:1.4 # TODO: switch to `curlimages/curl` # glaredb binary releases are not compatible with alpine # So we can't use here now # https://github.com/GlareDB/glaredb/issues/1912 FROM --platform=linux/amd64 docker.io/library/buildpack-deps:stable RUN < &'static Vec>> { static RUNNERS: std::sync::OnceLock>>> = std::sync::OnceLock::new(); RUNNERS.get_or_init(|| { let mut runners = vec![]; let local_runners: Vec> = vec![ Box::new(runner::SQLiteTestRunner::new( "tests/integration/data/chinook".to_string(), )), Box::new(runner::DuckDbTestRunner::new( "tests/integration/data/chinook".to_string(), )), ]; runners.extend(local_runners); #[cfg(feature = "test-dbs-external")] { let external_runners: Vec> = vec![ // I considered putting these URLs into the `Protocol`s; but // - the `url` parameter exists for some-but-not-all of the // Protocols and given it's a trait implementation the // signatures need to be the same. // - Similar to the `import_csv` method, different DBs use the // same protocol but different URLs // We could push them down to the TestRunner structs Box::new(runner::PostgresTestRunner::new( "host=localhost user=root password=root dbname=dummy", "/tmp/chinook".to_string(), )), Box::new(runner::MySqlTestRunner::new( "mysql://root:root@localhost:3306/dummy", "/tmp/chinook".to_string(), )), // TODO: https://github.com/ClickHouse/ClickHouse/issues/69131 // Box::new(runner::ClickHouseTestRunner::new( // "mysql://default:@localhost:9004/dummy", // "chinook".to_string(), // )), // TODO: disabled to get CI passing; would like to re-enable // Box::new(runner::GlareDbTestRunner::new( // "host=localhost user=glaredb dbname=glaredb port=6543", // "/tmp/chinook".to_string(), // )), Box::new(runner::MsSqlTestRunner::new("/tmp/chinook".to_string())), ]; runners.extend(external_runners); } runners .into_iter() .map(|mut runner| { runner.setup(); std::sync::Mutex::new(runner) }) .collect() }) } /// Converts arrow::RecordBatch into ad-hoc CSV pub(crate) fn batch_to_csv(batch: arrow::record_batch::RecordBatch) -> String { let mut res = String::with_capacity((batch.num_rows() + 1) * batch.num_columns() * 20); // convert each column to string let mut arrays = Vec::with_capacity(batch.num_columns()); for col_i in 0..batch.num_columns() { let mut array = batch.columns().get(col_i).unwrap().clone(); if *array.data_type() == arrow::datatypes::DataType::Boolean { array = arrow::compute::cast(&array, &arrow::datatypes::DataType::UInt8).unwrap(); } let array = arrow::compute::cast(&array, &arrow::datatypes::DataType::Utf8).unwrap(); let array = arrow::array::AsArray::as_string::(&array).clone(); arrays.push(array); } let re = Regex::new(r"^-?\d+\.\d*0+$").unwrap(); for row_i in 0..batch.num_rows() { for (i, col) in arrays.iter().enumerate() { let mut value = col.value(row_i); // HACK: trim trailing 0 if re.is_match(value) { value = value.trim_end_matches('0').trim_end_matches('.'); } res.push_str(value); if i < batch.num_columns() - 1 { res.push(','); } else { res.push('\n'); } } } res.shrink_to_fit(); res } ================================================ FILE: prqlc/prqlc/tests/integration/dbs/protocol.rs ================================================ use anyhow::Result; use connector_arrow::api::{ResultReader, Statement}; use connector_arrow::arrow; use connector_arrow::arrow::record_batch::RecordBatch; fn read_to_batch<'a>(reader: impl ResultReader<'a>) -> Result { let batches = reader.into_iter().collect::, _>>()?; let schema = batches.first().unwrap().schema_ref(); Ok(arrow::compute::concat_batches(schema, &batches)?) } pub(crate) trait DbProtocol: Send { fn query(&mut self, sql: &str) -> Result; fn execute(&mut self, sql: &str) -> Result<()>; } impl DbProtocol for connector_arrow::rusqlite::SQLiteConnection { fn query(&mut self, sql: &str) -> Result { let mut statement = connector_arrow::api::Connector::query(self, sql)?; let reader = statement.start([])?; read_to_batch(reader) } fn execute(&mut self, sql: &str) -> Result<()> { self.inner_mut().execute(sql, ())?; Ok(()) } } impl DbProtocol for connector_arrow::duckdb::DuckDBConnection { fn query(&mut self, sql: &str) -> Result { let mut statement = connector_arrow::api::Connector::query(self, sql)?; let reader = statement.start([])?; read_to_batch(reader) } fn execute(&mut self, sql: &str) -> Result<()> { self.inner_mut().execute(sql, [])?; Ok(()) } } #[cfg(feature = "test-dbs-external")] pub(crate) mod external { use super::*; use futures::{AsyncRead, AsyncWrite}; impl DbProtocol for connector_arrow::postgres::PostgresConnection { fn query(&mut self, sql: &str) -> Result { let mut statement = connector_arrow::api::Connector::query(self, sql)?; let reader = statement.start([])?; read_to_batch(reader) } fn execute(&mut self, sql: &str) -> Result<()> { self.inner_mut().execute(sql, &[])?; Ok(()) } } impl DbProtocol for connector_arrow::mysql::MySQLConnection<::mysql::Conn> { fn query(&mut self, sql: &str) -> Result { let mut statement = connector_arrow::api::Connector::query(self, sql)?; let reader = statement.start([])?; read_to_batch(reader) } fn execute(&mut self, sql: &str) -> Result<()> { use mysql::prelude::Queryable; self.inner_mut().query_iter(sql)?; Ok(()) } } impl DbProtocol for connector_arrow::tiberius::TiberiusConnection { fn query(&mut self, sql: &str) -> Result { let mut statement = connector_arrow::api::Connector::query(self, sql)?; let reader = statement.start([])?; read_to_batch(reader) } fn execute(&mut self, sql: &str) -> Result<()> { let (rt, client) = self.inner_mut(); rt.block_on(client.execute(sql, &[]))?; Ok(()) } } } ================================================ FILE: prqlc/prqlc/tests/integration/dbs/runner.rs ================================================ use anyhow::Result; use connector_arrow::arrow::record_batch::RecordBatch; use glob::glob; use prqlc::sql::Dialect; use super::protocol::DbProtocol; static DATA_FILE_ROOT_KEYWORD: &str = "data_file_root"; pub(crate) trait DbTestRunner: Send { fn dialect(&self) -> Dialect; fn data_file_root(&self) -> &str; fn import_csv(&mut self, csv_path: &str, table_name: &str); fn modify_ddl(&self, sql: String) -> String; fn query(&mut self, prql: &str) -> Result { let prql = prql.replace(DATA_FILE_ROOT_KEYWORD, self.data_file_root()); let options = prqlc::Options::default().with_target(prqlc::Target::Sql(Some(self.dialect()))); let sql = prqlc::compile(&prql, &options)?; self.protocol().query(&sql) } fn protocol(&mut self) -> &mut dyn DbProtocol; fn setup(&mut self) { let schema = include_str!("../data/chinook/schema.sql"); let statements: Vec = schema .split(';') .map(str::trim) .filter(|s| !s.is_empty()) .map(|s| self.modify_ddl(s.to_string())) .collect(); for statement in statements { self.protocol().execute(&statement).unwrap(); } for file_path in glob("tests/integration/data/chinook/*.csv").unwrap() { let file_path = file_path.unwrap(); let stem = file_path.file_stem().unwrap().to_str().unwrap(); let path = format!("{}/{}.csv", self.data_file_root(), stem); self.import_csv(&path, stem); } } } pub(crate) struct DuckDbTestRunner { protocol: connector_arrow::duckdb::DuckDBConnection, data_file_root: String, } impl DuckDbTestRunner { pub(crate) fn new(data_file_root: String) -> Self { let conn = ::duckdb::Connection::open_in_memory().unwrap(); let conn_ar = connector_arrow::duckdb::DuckDBConnection::new(conn); Self { protocol: conn_ar, data_file_root, } } } impl DbTestRunner for DuckDbTestRunner { fn dialect(&self) -> Dialect { Dialect::DuckDb } fn protocol(&mut self) -> &mut dyn DbProtocol { &mut self.protocol } fn data_file_root(&self) -> &str { &self.data_file_root } fn import_csv(&mut self, csv_path: &str, table_name: &str) { self.protocol .execute(&format!( "COPY {table_name} FROM '{csv_path}' (AUTO_DETECT TRUE);" )) .unwrap(); } fn modify_ddl(&self, sql: String) -> String { sql.replace("FLOAT", "DOUBLE") } } pub(crate) struct SQLiteTestRunner { protocol: connector_arrow::rusqlite::SQLiteConnection, data_file_root: String, } impl SQLiteTestRunner { pub(crate) fn new(data_file_root: String) -> Self { let conn = rusqlite::Connection::open_in_memory().unwrap(); let conn_ar = connector_arrow::rusqlite::SQLiteConnection::new(conn); Self { protocol: conn_ar, data_file_root, } } } impl DbTestRunner for SQLiteTestRunner { fn dialect(&self) -> Dialect { Dialect::SQLite } fn protocol(&mut self) -> &mut dyn DbProtocol { &mut self.protocol } fn data_file_root(&self) -> &str { &self.data_file_root } fn import_csv(&mut self, csv_path: &str, table_name: &str) { let mut reader = csv::ReaderBuilder::new() .has_headers(true) .from_path(csv_path) .unwrap(); let headers = reader .headers() .unwrap() .iter() .map(|s| s.to_string()) .collect::>(); for result in reader.records() { let r = result.unwrap(); let q = format!( "INSERT INTO {table_name} ({}) VALUES ({})", headers.join(","), r.iter() .map(|s| if s.is_empty() { "null".to_string() } else { format!("\"{}\"", s.replace('"', "\"\"")) }) .collect::>() .join(",") ); self.protocol.execute(&q).unwrap(); } } fn modify_ddl(&self, sql: String) -> String { sql.replace("TIMESTAMP", "TEXT") // timestamps in chinook are stored as ISO8601 } } #[cfg(feature = "test-dbs-external")] pub(crate) use self::external::*; #[cfg(feature = "test-dbs-external")] pub(crate) mod external { use super::*; use std::fs; pub(crate) struct PostgresTestRunner { protocol: connector_arrow::postgres::PostgresConnection, data_file_root: String, } impl PostgresTestRunner { pub(crate) fn new(url: &str, data_file_root: String) -> Self { use connector_arrow::postgres::PostgresConnection; let client = ::postgres::Client::connect(url, ::postgres::NoTls).unwrap(); Self { protocol: PostgresConnection::new(client), data_file_root, } } } impl DbTestRunner for PostgresTestRunner { fn dialect(&self) -> Dialect { Dialect::Postgres } fn protocol(&mut self) -> &mut dyn DbProtocol { &mut self.protocol } fn data_file_root(&self) -> &str { &self.data_file_root } fn import_csv(&mut self, csv_path: &str, table_name: &str) { self.protocol .execute(&format!( "COPY {table_name} FROM '{csv_path}' DELIMITER ',' CSV HEADER;" )) .unwrap(); } fn modify_ddl(&self, sql: String) -> String { sql.replace("FLOAT", "DOUBLE PRECISION") } } pub(crate) struct MySqlTestRunner { protocol: connector_arrow::mysql::MySQLConnection<::mysql::Conn>, data_file_root: String, } impl MySqlTestRunner { pub(crate) fn new(url: &str, data_file_root: String) -> Self { let conn = ::mysql::Conn::new(url) .unwrap_or_else(|e| panic!("Failed to connect to {url}:\n{e}")); Self { protocol: connector_arrow::mysql::MySQLConnection::<::mysql::Conn>::new(conn), data_file_root, } } } impl DbTestRunner for MySqlTestRunner { fn dialect(&self) -> Dialect { Dialect::MySql } fn protocol(&mut self) -> &mut dyn DbProtocol { &mut self.protocol } fn data_file_root(&self) -> &str { &self.data_file_root } fn import_csv(&mut self, csv_path: &str, table_name: &str) { // MySQL-specific CSV import logic let csv_path_binding = std::path::PathBuf::from(csv_path); let local_csv_path = format!( "tests/integration/data/chinook/{}", csv_path_binding.file_name().unwrap().to_str().unwrap() ); let local_old_path = std::path::PathBuf::from(local_csv_path); let mut local_new_path = local_old_path.clone(); local_new_path.pop(); local_new_path.push(format!("{table_name}.my.csv").as_str()); let file_content = fs::read_to_string(local_old_path) .unwrap() .replace(",,", ",\\N,") .replace(",\n", ",\\N\n"); fs::write(&local_new_path, file_content).unwrap(); self.protocol.execute( &format!( "LOAD DATA INFILE '{}' INTO TABLE {table_name} FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"' LINES TERMINATED BY '\n' IGNORE 1 ROWS;", &csv_path_binding.parent().unwrap().join(local_new_path.file_name().unwrap()).to_str().unwrap() ) ).unwrap(); fs::remove_file(&local_new_path).unwrap(); } fn modify_ddl(&self, sql: String) -> String { sql.replace("TIMESTAMP", "DATETIME") } } pub(crate) struct ClickHouseTestRunner { protocol: connector_arrow::mysql::MySQLConnection<::mysql::Conn>, data_file_root: String, } impl ClickHouseTestRunner { #[allow(dead_code)] pub(crate) fn new(url: &str, data_file_root: String) -> Self { Self { protocol: connector_arrow::mysql::MySQLConnection::<::mysql::Conn>::new( ::mysql::Conn::new(url) .unwrap_or_else(|e| panic!("Failed to connect to {url}:\n{e}")), ), data_file_root, } } } impl DbTestRunner for ClickHouseTestRunner { fn dialect(&self) -> Dialect { Dialect::ClickHouse } fn protocol(&mut self) -> &mut dyn DbProtocol { &mut self.protocol } fn data_file_root(&self) -> &str { &self.data_file_root } fn import_csv(&mut self, csv_path: &str, table_name: &str) { self.protocol .execute(&format!( "INSERT INTO {table_name} SELECT * FROM file('{csv_path}')" )) .unwrap(); } fn modify_ddl(&self, sql: String) -> String { use regex::Regex; let re = Regex::new(r"(?s)\)$").unwrap(); re.replace(&sql, r") ENGINE = Memory") .replace("TIMESTAMP", "DATETIME64") .replace("FLOAT", "DOUBLE") .replace("VARCHAR(255)", "Nullable(String)") .to_string() } } pub(crate) struct MsSqlTestRunner { protocol: connector_arrow::tiberius::TiberiusConnection< tokio_util::compat::Compat, >, data_file_root: String, } impl MsSqlTestRunner { pub(crate) fn new(data_file_root: String) -> Self { use std::sync::Arc; use tokio_util::compat::TokioAsyncWriteCompatExt; let mut config = tiberius::Config::new(); config.host("localhost"); config.port(1433); config.trust_cert(); config.authentication(tiberius::AuthMethod::sql_server("sa", "Wordpass123##")); let rt = Arc::new( tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap(), ); let client = rt .block_on(async { let tcp = tokio::net::TcpStream::connect(config.get_addr()).await?; tcp.set_nodelay(true).unwrap(); tiberius::Client::connect(config, tcp.compat_write()).await }) .unwrap(); Self { protocol: connector_arrow::tiberius::TiberiusConnection::new(rt, client), data_file_root, } } } impl DbTestRunner for MsSqlTestRunner { fn dialect(&self) -> Dialect { Dialect::MsSql } fn protocol(&mut self) -> &mut dyn DbProtocol { &mut self.protocol } fn data_file_root(&self) -> &str { &self.data_file_root } fn import_csv(&mut self, csv_path: &str, table_name: &str) { self.protocol.execute(&format!( "BULK INSERT {table_name} FROM '{csv_path}' WITH (FIRSTROW = 2, FIELDTERMINATOR = ',', ROWTERMINATOR = '\n', TABLOCK, FORMAT = 'CSV', CODEPAGE = 'RAW');" )).unwrap(); } fn modify_ddl(&self, sql: String) -> String { sql.replace("TIMESTAMP", "DATETIME") .replace("FLOAT", "FLOAT(53)") .replace(" AS TEXT", " AS VARCHAR") } } #[allow(dead_code)] pub(crate) struct GlareDbTestRunner { protocol: connector_arrow::postgres::PostgresConnection, data_file_root: String, } impl GlareDbTestRunner { #[allow(dead_code)] pub(crate) fn new(url: &str, data_file_root: String) -> Self { use connector_arrow::postgres::PostgresConnection; let client = ::postgres::Client::connect(url, ::postgres::NoTls).unwrap(); Self { protocol: PostgresConnection::new(client), data_file_root, } } } impl DbTestRunner for GlareDbTestRunner { fn dialect(&self) -> Dialect { Dialect::GlareDb } fn protocol(&mut self) -> &mut dyn DbProtocol { &mut self.protocol } fn data_file_root(&self) -> &str { &self.data_file_root } fn import_csv(&mut self, csv_path: &str, table_name: &str) { self.protocol .execute(&format!( "INSERT INTO {table_name} SELECT * FROM '{csv_path}'" )) .unwrap(); } fn modify_ddl(&self, sql: String) -> String { sql.replace("FLOAT", "DOUBLE PRECISION") } } } ================================================ FILE: prqlc/prqlc/tests/integration/error_messages.rs ================================================ //! Test error messages. As of 2023-03, this can hopefully be expanded significantly. //! It's also fine to put errors by the things that they're testing. //! See also [test_bad_error_messages.rs](test_bad_error_messages.rs) for error //! messages which need to be improved. use insta::assert_snapshot; use super::sql::compile; #[test] fn test_errors() { assert_snapshot!(compile(r###" let addadd = a b -> a + b from x derive y = (addadd 4 5 6) "###).unwrap_err(), @r" Error: ╭─[ :5:17 ] │ 5 │ derive y = (addadd 4 5 6) │ ──────┬───── │ ╰─────── Too many arguments to function `addadd` ───╯ "); assert_snapshot!(compile(r###" from a select b "###).unwrap_err(), @r" Error: ╭─[ :2:5 ] │ 2 │ from a select b │ ───────┬─────── │ ╰───────── Too many arguments to function `from` ───╯ "); assert_snapshot!(compile(r###" from x select a select b "###).unwrap_err(), @r" Error: ╭─[ :4:12 ] │ 4 │ select b │ ┬ │ ╰── Unknown name `b` │ │ Help: available columns: x.a ───╯ "); assert_snapshot!(compile(r###" from employees take 1.8 "###).unwrap_err(), @r" Error: ╭─[ :3:10 ] │ 3 │ take 1.8 │ ─┬─ │ ╰─── `take` expected int or range, but found 1.8 ───╯ "); // LEXER output for Mississippi test (curly quotes are U+2019): use insta::assert_debug_snapshot; let mississippi = "Mississippi has four S’s and four I’s."; assert_debug_snapshot!(prqlc_parser::lexer::lex_source(mississippi).unwrap_err(), @r#" [ Error { kind: Error, span: Some( 0:22-23, ), reason: Unexpected { found: "'’'", }, hints: [], code: None, }, ] "#); // PARSER output (full compilation error): assert_snapshot!(compile(mississippi).unwrap_err(), @r" Error: ╭─[ :1:23 ] │ 1 │ Mississippi has four S’s and four I’s. │ ┬ │ ╰── unexpected '’' ───╯ "); assert_snapshot!(compile("Answer: T-H-A-T!").unwrap_err(), @r" Error: ╭─[ :1:16 ] │ 1 │ Answer: T-H-A-T! │ ┬ │ ╰── expected something else, but found ! ───╯ "); } #[test] fn test_union_all_sqlite() { // TODO: `SQLiteDialect` would be better as `sql.sqlite` or `sqlite`. assert_snapshot!(compile(r###" prql target:sql.sqlite from film remove film2 "###).unwrap_err(), @r" Error: The dialect SQLiteDialect does not support EXCEPT ALL ↳ Hint: providing more column information will allow the query to be translated to an anti-join. ") } #[test] fn test_regex_dialect() { assert_snapshot!(compile(r###" prql target:sql.mssql from foo filter bar ~= 'love' "###).unwrap_err(), @r" Error: ╭─[ :4:12 ] │ 4 │ filter bar ~= 'love' │ ──────┬────── │ ╰──────── operator std.regex_search is not supported for dialect mssql ───╯ ") } #[test] fn test_bad_function_type() { assert_snapshot!(compile(r###" from tracks group foo (take) "###, ) .unwrap_err(), @r" Error: ╭─[ :3:16 ] │ 3 │ group foo (take) │ ──┬─ │ ╰─── function std.group, param `pipeline` expected type `transform`, but found type `func ? relation -> relation` │ │ Help: Type `transform` expands to `func relation -> relation` ───╯ "); } #[test] #[ignore] // FIXME: This would be nice to catch those errors again // See https://github.com/PRQL/prql/issues/3127#issuecomment-1849032396 fn test_basic_type_checking() { assert_snapshot!(compile(r#" from foo select (a && b) + c "#) .unwrap_err(), @r###" Error: ╭─[:3:13] │ 3 │ select (a && b) + c │ ───┬── │ ╰──── function std.add, param `left` expected type `int || float || timestamp || date`, but found type `bool` ───╯ "###); } #[test] fn test_ambiguous() { assert_snapshot!(compile(r#" from a derive date = x select date "#) .unwrap_err(), @r" Error: ╭─[ :4:12 ] │ 4 │ select date │ ──┬─ │ ╰─── Ambiguous name │ │ Help: could be any of: std.date, this.date │ │ Note: available columns: date ───╯ "); } #[test] fn test_ambiguous_join() { assert_snapshot!(compile(r#" from a select x join (from b | select {x}) true select x "#) .unwrap_err(), @r" Error: ╭─[ :5:12 ] │ 5 │ select x │ ┬ │ ╰── Ambiguous name │ │ Help: could be any of: a.x, b.x │ │ Note: available columns: a.x, b.x ───╯ "); } #[test] fn test_ambiguous_inference() { assert_snapshot!(compile(r#" from a join b(==b_id) select x "#) .unwrap_err(), @r" Error: ╭─[ :4:12 ] │ 4 │ select x │ ┬ │ ╰── Ambiguous name │ │ Help: could be any of: a.x, b.x ───╯ "); } #[test] fn date_to_text_generic() { assert_snapshot!(compile(r#" [{d = @2021-01-01}] derive { d_str = (d | date.to_text "%Y/%m/%d") }"#).unwrap_err(), @r#" Error: ╭─[ :4:31 ] │ 4 │ d_str = (d | date.to_text "%Y/%m/%d") │ ─────┬──── │ ╰────── Date formatting requires a dialect ───╯ "#); } #[test] fn date_to_text_with_column_format() { assert_snapshot!(compile(r#" from dates_to_display select {my_date, my_format} select {std.date.to_text my_date my_format} "#).unwrap_err(), @r" Error: ╭─[ :4:11 ] │ 4 │ select {std.date.to_text my_date my_format} │ ─────────────────┬──────────────── │ ╰────────────────── `std.date.to_text` only supports a string literal as format ───╯ "); } #[test] fn date_to_text_unsupported_chrono_item() { assert_snapshot!(compile(r#" prql target:sql.duckdb from [{d = @2021-01-01}] derive { d_str = (d | date.to_text "%_j") }"#).unwrap_err(), @r#" Error: ╭─[ :6:33 ] │ 6 │ d_str = (d | date.to_text "%_j") │ ──┬── │ ╰──── PRQL doesn't support this format specifier ───╯ "#); } #[test] fn available_columns() { assert_snapshot!(compile(r#" from invoices select foo select bar "#).unwrap_err(), @r" Error: ╭─[ :4:12 ] │ 4 │ select bar │ ─┬─ │ ╰─── Unknown name `bar` │ │ Help: available columns: invoices.foo ───╯ "); } #[test] fn empty_interpolations() { assert_snapshot!(compile(r#"from x | select f"{}" "#).unwrap_err(), @r#" Error: ╭─[ :1:20 ] │ 1 │ from x | select f"{}" │ ┬ │ ╰── expected interpolated string variable or '{', but found "}" ───╯ "#); } #[test] fn no_query_entered() { // Empty query assert_snapshot!(compile("").unwrap_err(), @r" [E0001] Error: No PRQL query entered "); // Comment-only query assert_snapshot!(compile("# just a comment").unwrap_err(), @r" [E0001] Error: No PRQL query entered "); } #[test] fn query_must_begin_with_from() { // Query with declaration but no 'from' assert_snapshot!(compile("let x = 5").unwrap_err(), @r" [E0001] Error: PRQL queries must begin with 'from' ↳ Hint: A query must start with a 'from' statement to define the main pipeline "); // Query with multiple declarations but no pipeline assert_snapshot!(compile(r#" let x = 5 let y = 10 "#).unwrap_err(), @r" [E0001] Error: PRQL queries must begin with 'from' ↳ Hint: A query must start with a 'from' statement to define the main pipeline "); } #[test] fn negative_number_in_transform() { // Negative numbers need to be wrapped in parentheses when used in transforms assert_snapshot!(compile(r###" from artists sort -name "###).unwrap_err(), @r" Error: expected a pipeline that resolves to a table, but found `internal std.sub` ↳ Hint: wrap negative numbers in parentheses, e.g. `sort (-column_name)` "); assert_snapshot!(compile(r###" from pets take -10 "###).unwrap_err(), @r" Error: expected a pipeline that resolves to a table, but found `internal std.sub` ↳ Hint: wrap negative numbers in parentheses, e.g. `sort (-column_name)` "); assert_snapshot!(compile(r###" from tbl group id ( sort -val ) "###).unwrap_err(), @r" Error: expected a pipeline that resolves to a table, but found `internal std.sub` ↳ Hint: wrap negative numbers in parentheses, e.g. `sort (-column_name)` "); } #[test] fn empty_tuple_or_array_from() { assert_snapshot!(compile(r###" from {} "###).unwrap_err(), @r" Error: ╭─[ :2:10 ] │ 2 │ from {} │ ─┬ │ ╰── expected a table or query, but found an empty tuple `{}` ───╯ "); assert_snapshot!(compile(r###" from [] "###).unwrap_err(), @r" Error: ╭─[ :2:10 ] │ 2 │ from [] │ ─┬ │ ╰── expected a table or query, but found an empty array `[]` ───╯ "); assert_snapshot!(compile(r###" from {} select a "###).unwrap_err(), @r" Error: ╭─[ :2:10 ] │ 2 │ from {} │ ─┬ │ ╰── expected a table or query, but found an empty tuple `{}` ───╯ "); } #[test] fn window_rows_expects_range() { // Issue #5601: window with invalid rows parameter should produce error, not panic assert_snapshot!(compile(r###" from t group sid (window rows:2 (sid)) "###).unwrap_err(), @r" Error: ╭─[ :3:28 ] │ 3 │ group sid (window rows:2 (sid)) │ ┬ │ ╰── parameter `rows` expected a range, but found 2 ───╯ "); assert_snapshot!(compile(r###" from t group sid (window range:2 (sid)) "###).unwrap_err(), @r" Error: ╭─[ :3:29 ] │ 3 │ group sid (window range:2 (sid)) │ ┬ │ ╰── parameter `range` expected a range, but found 2 ───╯ "); } #[test] fn bare_lambda_expression() { // Issue #4280: A bare lambda expression like `x -> y` should produce // a clear error, not a confusing internal message. assert_snapshot!(compile(r###" x -> y "###).unwrap_err(), @r" Error: ╭─[ :2:5 ] │ 2 │ x -> y │ ───┬── │ ╰──── expected a table, but found a function ───╯ "); } ================================================ FILE: prqlc/prqlc/tests/integration/main.rs ================================================ mod bad_error_messages; mod dbs; mod error_messages; mod queries; mod sql; ================================================ FILE: prqlc/prqlc/tests/integration/project/Project.prql ================================================ let favorite_artists = [ {artist_id = 120, last_listen = @2023-05-18}, {artist_id = 7, last_listen = @2023-05-16}, ] favorite_artists join side:left artists.input (==artist_id) ================================================ FILE: prqlc/prqlc/tests/integration/project/artists.prql ================================================ let input = read_parquet "artists.parquet" ================================================ FILE: prqlc/prqlc/tests/integration/queries/aggregation.prql ================================================ # mysql:skip # clickhouse:skip # glaredb:skip (the string_agg function is not supported) from tracks filter genre_id == 100 derive empty_name = name == '' aggregate {sum track_id, concat_array name, all empty_name, any empty_name} ================================================ FILE: prqlc/prqlc/tests/integration/queries/append_select.prql ================================================ from invoices select { customer_id, invoice_id, billing_country } take 10..15 append ( from invoices select { customer_id, invoice_id, billing_country } take 40..45 ) select { billing_country, invoice_id } ================================================ FILE: prqlc/prqlc/tests/integration/queries/append_select_compute.prql ================================================ from invoices derive total = case [total < 10 => total * 2, true => total] select { customer_id, invoice_id, total } take 5 append ( from invoice_items derive unit_price = case [unit_price < 1 => unit_price * 2, true => unit_price] select { invoice_line_id, invoice_id, unit_price } take 5 ) select { a = customer_id * 2, b = math.round 1 (invoice_id * total) } ================================================ FILE: prqlc/prqlc/tests/integration/queries/append_select_multiple_with_null.prql ================================================ from invoices select { customer_id, invoice_id, billing_country } take 5 append ( from employees select { employee_id, employee_id, country } take 5 ) append ( from invoice_items select { invoice_line_id, invoice_id, null } take 5 ) select { billing_country, invoice_id } ================================================ FILE: prqlc/prqlc/tests/integration/queries/append_select_nulls.prql ================================================ from invoices select {an_id = invoice_id, name = null} take 2 append ( from employees select {an_id = null, name = first_name} take 2 ) ================================================ FILE: prqlc/prqlc/tests/integration/queries/append_select_simple.prql ================================================ from invoices select { invoice_id, billing_country } append ( from invoices select { invoice_id = `invoice_id` + 100, billing_country } ) filter (billing_country | text.starts_with("I")) ================================================ FILE: prqlc/prqlc/tests/integration/queries/arithmetic.prql ================================================ # mssql:test from [ { id = 1, x_int = 13, x_float = 13.0, k_int = 5, k_float = 5.0 }, { id = 2, x_int = -13, x_float = -13.0, k_int = 5, k_float = 5.0 }, { id = 3, x_int = 13, x_float = 13.0, k_int = -5, k_float = -5.0 }, { id = 4, x_int = -13, x_float = -13.0, k_int = -5, k_float = -5.0 }, ] select { id, x_int / k_int, x_int / k_float, x_float / k_int, x_float / k_float, q_ii = x_int // k_int, q_if = x_int // k_float, q_fi = x_float // k_int, q_ff = x_float // k_float, r_ii = x_int % k_int, r_if = x_int % k_float, r_fi = x_float % k_int, r_ff = x_float % k_float, (q_ii * k_int + r_ii | math.round 0), (q_if * k_float + r_if | math.round 0), (q_fi * k_int + r_fi | math.round 0), (q_ff * k_float + r_ff | math.round 0), } sort id ================================================ FILE: prqlc/prqlc/tests/integration/queries/cast.prql ================================================ # mssql:test from tracks sort {-bytes} select { name, bin = ((album_id | as REAL) * 99) } take 20 ================================================ FILE: prqlc/prqlc/tests/integration/queries/constants_only.prql ================================================ from genres take 10 filter true take 20 filter true select d = 10 ================================================ FILE: prqlc/prqlc/tests/integration/queries/date_to_text.prql ================================================ # generic:skip # glaredb:skip # sqlite:skip # mssql:test from invoices take 20 select { d1 = (invoice_date | date.to_text "%Y/%m/%d"), d2 = (invoice_date | date.to_text "%F"), d3 = (invoice_date | date.to_text "%D"), d4 = (invoice_date | date.to_text "%H:%M:%S.%f"), d5 = (invoice_date | date.to_text "%r"), d6 = (invoice_date | date.to_text "%A %B %-d %Y"), d7 = (invoice_date | date.to_text "%a, %-d %b %Y at %I:%M:%S %p"), d8 = (invoice_date | date.to_text "%+"), d9 = (invoice_date | date.to_text "%-d/%-m/%y"), d10 = (invoice_date | date.to_text "%-Hh %Mmin"), d11 = (invoice_date | date.to_text "%M'%S\""), d12 = (invoice_date | date.to_text "100%% in %d days"), } ================================================ FILE: prqlc/prqlc/tests/integration/queries/distinct.prql ================================================ # mssql:test from tracks select {album_id, genre_id} group tracks.* (take 1) sort tracks.* ================================================ FILE: prqlc/prqlc/tests/integration/queries/distinct_on.prql ================================================ # mssql:test from tracks select {genre_id, media_type_id, album_id} group {genre_id, media_type_id} (sort {-album_id} | take 1) sort {-genre_id, media_type_id} ================================================ FILE: prqlc/prqlc/tests/integration/queries/genre_counts.prql ================================================ # clickhouse:skip (ClickHouse prefers aliases to column names https://github.com/PRQL/prql/issues/2827) # mssql:test let genre_count = ( from genres aggregate {a = count name} ) from genre_count filter a > 0 select a = -a ================================================ FILE: prqlc/prqlc/tests/integration/queries/group_all.prql ================================================ # mssql:test from a=albums take 10 join tracks (==album_id) group {a.album_id, a.title} (aggregate price = (sum tracks.unit_price | math.round 2)) sort album_id ================================================ FILE: prqlc/prqlc/tests/integration/queries/group_sort.prql ================================================ # mssql:test from tracks derive d = album_id + 1 group d ( aggregate { n1 = (track_id | sum), } ) sort d take 10 select { d1 = d, n1 } ================================================ FILE: prqlc/prqlc/tests/integration/queries/group_sort_derive_select_join.prql ================================================ s"SELECT album_id,title,artist_id FROM albums" group {artist_id} (aggregate { album_title_count = count this.`title`}) sort {this.artist_id, this.album_title_count} derive {new_album_count = this.album_title_count} select {this.artist_id, this.new_album_count} join side:left ( s"SELECT artist_id,name as artist_name FROM artists" ) (this.artist_id == that.artist_id) ================================================ FILE: prqlc/prqlc/tests/integration/queries/group_sort_filter_derive_select_join.prql ================================================ s"SELECT album_id,title,artist_id FROM albums" group {artist_id} (aggregate { album_title_count = count this.`title`}) sort {this.artist_id, this.album_title_count} filter (this.album_title_count) > 10 derive {new_album_count = this.album_title_count} select {this.artist_id, this.new_album_count} join side:left ( s"SELECT artist_id,name as artist_name FROM artists" ) (this.artist_id == that.artist_id) ================================================ FILE: prqlc/prqlc/tests/integration/queries/group_sort_limit_take.prql ================================================ # Compute the 3 longest songs for each genre and sort by genre # mssql:test from tracks select {genre_id,milliseconds} group {genre_id} ( sort {-milliseconds} take 3 ) join genres (==genre_id) select {name, milliseconds} sort {+name,-milliseconds} ================================================ FILE: prqlc/prqlc/tests/integration/queries/invoice_totals.prql ================================================ # clickhouse:skip (clickhouse doesn't have lag function) #! Calculate a number of metrics about the sales of tracks in each city. from i=invoices join ii=invoice_items (==invoice_id) derive { city = i.billing_city, street = i.billing_address, } group {city, street} ( derive total = ii.unit_price * ii.quantity aggregate { num_orders = count_distinct i.invoice_id, num_tracks = sum ii.quantity, total_price = sum total, } ) group {city} ( sort street window expanding:true ( derive {running_total_num_tracks = sum num_tracks} ) ) sort {city, street} derive {num_tracks_last_week = lag 7 num_tracks} select { city, street, num_orders, num_tracks, running_total_num_tracks, num_tracks_last_week } take 20 ================================================ FILE: prqlc/prqlc/tests/integration/queries/loop_01.prql ================================================ # clickhouse:skip (DB::Exception: Syntax error) # glaredb:skip (DataFusion does not support recursive CTEs https://github.com/apache/arrow-datafusion/issues/462) from [{n = 1}] select n = n - 2 loop (filter n < 4 | select n = n + 1) select n = n * 2 sort n ================================================ FILE: prqlc/prqlc/tests/integration/queries/math_module.prql ================================================ # mssql:test # sqlite:skip (see https://github.com/rusqlite/rusqlite/issues/1211) from invoices take 5 select { total_original = (total | math.round 2), total_x = (math.pi - total | math.round 2 | math.abs), total_floor = (math.floor total), total_ceil = (math.ceil total), total_log10 = (math.log10 total | math.round 3), total_log2 = (math.log 2 total | math.round 3), total_sqrt = (math.sqrt total | math.round 3), total_ln = (math.ln total | math.exp | math.round 2), total_cos = (math.cos total | math.acos | math.round 2), total_sin = (math.sin total | math.asin | math.round 2), total_tan = (math.tan total | math.atan | math.round 2), total_deg = (total | math.degrees | math.radians | math.round 2), total_square = (total | math.pow 2 | math.round 2), total_square_op = ((total ** 2) | math.round 2), } ================================================ FILE: prqlc/prqlc/tests/integration/queries/pipelines.prql ================================================ # sqlite:skip (Only works on Sqlite implementations which have the extension # installed # https://stackoverflow.com/questions/24037982/how-to-use-regexp-in-sqlite) from tracks filter (name ~= "Love") filter ((milliseconds / 1000 / 60) | in 3..4) sort track_id take 1..15 select {name, composer} ================================================ FILE: prqlc/prqlc/tests/integration/queries/read_csv.prql ================================================ # sqlite:skip # postgres:skip # mysql:skip from (read_csv "data_file_root/media_types.csv") append (read_json "data_file_root/media_types.json") sort media_type_id ================================================ FILE: prqlc/prqlc/tests/integration/queries/set_ops_remove.prql ================================================ # mssql:test let distinct = rel -> (from t = _param.rel | group {t.*} (take 1)) from_text format:json '{ "columns": ["a"], "data": [[1], [2], [2], [3]] }' distinct remove (from_text format:json '{ "columns": ["a"], "data": [[1], [2]] }') sort a ================================================ FILE: prqlc/prqlc/tests/integration/queries/sort.prql ================================================ # mssql:test from e=employees filter first_name != "Mitchell" sort {first_name, last_name} # joining may use HashMerge, which can undo ORDER BY join manager=employees side:left (e.reports_to == manager.employee_id) select {e.first_name, e.last_name, manager.first_name} ================================================ FILE: prqlc/prqlc/tests/integration/queries/sort_2.prql ================================================ from albums select { AA=album_id, artist_id } sort AA filter AA >= 25 join artists (==artist_id) ================================================ FILE: prqlc/prqlc/tests/integration/queries/sort_3.prql ================================================ from [{track_id=0, album_id=1, genre_id=2}] select { AA=track_id, album_id, genre_id } sort AA join side:left [{album_id=1, album_title="Songs"}] (==album_id) select { AA, AT = album_title ?? "unknown", genre_id } filter AA < 25 join side:left [{genre_id=1, genre_title="Rock"}] (==genre_id) select { AA, AT, GT = genre_title ?? "unknown" } ================================================ FILE: prqlc/prqlc/tests/integration/queries/switch.prql ================================================ # glaredb:skip (May be a bag of String type conversion for Postgres Client) # mssql:test from tracks sort milliseconds select display = case [ composer != null => composer, genre_id < 17 => 'no composer', true => f'unknown composer' ] take 10 ================================================ FILE: prqlc/prqlc/tests/integration/queries/take.prql ================================================ # mssql:test from tracks sort {+track_id} take 3..5 ================================================ FILE: prqlc/prqlc/tests/integration/queries/text_module.prql ================================================ # mssql:test # glaredb:skip — TODO: started raising an error on 2024-05-20; see `window.prql` # for more details from albums select { title, title_and_spaces = f" {title} ", low = (title | text.lower), up = (title | text.upper), ltrimmed = (title | text.ltrim), rtrimmed = (title | text.rtrim), trimmed = (title | text.trim), len = (title | text.length), subs = (title | text.extract 2 5), replace = (title | text.replace "al" "PIKA"), } sort {title} filter (title | text.starts_with "Black") || (title | text.contains "Sabbath") || (title | text.ends_with "os") ================================================ FILE: prqlc/prqlc/tests/integration/queries/window.prql ================================================ # clickhouse:skip problems with DISTINCT ON # glaredb:skip — TODO: started raising an error on 2024-05-20, from https://github.com/PRQL/prql/actions/runs/9154902656/job/25198160283: # ERROR: This feature is not implemented: Unsupported ast node in sqltorel: # Substring { expr: Identifier(Ident { value: "title", quote_style: None }), # substring_from: Some(Value(Number("2", false))), substring_for: # Some(Value(Number("5", false))), special: true } from tracks group genre_id ( sort milliseconds derive { num = row_number this, total = count this, last_val = last track_id, } take 10 ) sort {genre_id, milliseconds} select {track_id, genre_id, num, total, last_val} filter genre_id >= 22 ================================================ FILE: prqlc/prqlc/tests/integration/queries.rs ================================================ #![cfg(not(target_family = "wasm"))] use std::fs; use std::path::Path; use insta::assert_debug_snapshot; use insta::{assert_snapshot, with_settings}; use prqlc::sql::Dialect; use prqlc::sql::SupportLevel; use prqlc::{Options, Target}; use test_each_file::test_each_path; mod lex { use super::*; test_each_path! { in "./prqlc/prqlc/tests/integration/queries" => run } fn run(prql_path: &Path) { let test_name = prql_path.file_stem().unwrap().to_str().unwrap(); let prql = fs::read_to_string(prql_path).unwrap(); let tokens = prqlc_parser::lexer::lex_source(&prql).unwrap(); with_settings!({ input_file => prql_path }, { assert_debug_snapshot!(test_name, tokens) }); } } mod compile { use super::*; // If this is giving compilation errors saying `expected identifier, found keyword`, // rename the filenames in queries to something that's not a keyword in Rust. test_each_path! { in "./prqlc/prqlc/tests/integration/queries" => run } fn run(prql_path: &Path) { let test_name = prql_path.file_stem().unwrap().to_str().unwrap(); let prql = fs::read_to_string(prql_path).unwrap(); if prql.contains("generic:skip") { return; } let target = Target::Sql(Some(Dialect::Generic)); let options = Options::default().no_signature().with_target(target); let sql = prqlc::compile(&prql, &options).unwrap(); with_settings!({ input_file => prql_path }, { assert_snapshot!(test_name, &sql, &prql) }); } } fn should_run_query(dialect: Dialect, prql: &str) -> bool { let dialect_str = dialect.to_string().to_lowercase(); match dialect.support_level() { SupportLevel::Supported => !prql.contains(&format!("{dialect_str}:skip")), SupportLevel::Unsupported => prql.contains(&format!("{dialect_str}:test")), SupportLevel::Nascent => false, } } mod compileall { use super::*; use similar::TextDiff; use strum::IntoEnumIterator; test_each_path! { in "./prqlc/prqlc/tests/integration/queries" => run } fn run(prql_path: &Path) { let test_name = prql_path.file_stem().unwrap().to_str().unwrap(); let prql = fs::read_to_string(prql_path).unwrap(); if prql.contains("generic:skip") { return; } // first compile with the generic dialect let target = Target::Sql(Some(Dialect::Generic)); let options = Options::default().no_signature().with_target(target); let generic_sql = prqlc::compile(&prql, &options).unwrap(); // next compile with each dialect let mut diffsnap = "".to_owned(); for dialect in Dialect::iter() { if !should_run_query(dialect, &prql) { continue; } let dialect_target = Target::Sql(Some(dialect)); let dialect_options = Options::default() .no_signature() .with_target(dialect_target); let dialect_sql = prqlc::compile(&prql, &dialect_options).unwrap(); let diff = TextDiff::from_lines(&generic_sql, &dialect_sql); diffsnap = format!( "{diffsnap}\n{}", diff.unified_diff() .context_radius(10) .header("generic", &dialect.to_string()) ); } with_settings!({ input_file => prql_path }, { assert_snapshot!(test_name, diffsnap, &prql) }); } } mod fmt { use super::*; test_each_path! { in "./prqlc/prqlc/tests/integration/queries" => run } fn run(prql_path: &Path) { let test_name = prql_path.file_stem().unwrap().to_str().unwrap(); let prql = fs::read_to_string(prql_path).unwrap(); let pl = prqlc::prql_to_pl(&prql).unwrap(); let formatted = prqlc::pl_to_prql(&pl).unwrap(); with_settings!({ input_file => prql_path }, { assert_snapshot!(test_name, &formatted, &prql) }); // Check the formatted queries can still compile prqlc::prql_to_pl(&formatted).unwrap(); } } #[cfg(feature = "serde_yaml")] mod debug_lineage { use super::*; test_each_path! { in "./prqlc/prqlc/tests/integration/queries" => run } fn run(prql_path: &Path) { let test_name = prql_path.file_stem().unwrap().to_str().unwrap(); let prql = fs::read_to_string(prql_path).unwrap(); let pl = prqlc::prql_to_pl(&prql).unwrap(); let fc = prqlc::internal::pl_to_lineage(pl).unwrap(); let lineage = serde_yaml::to_string(&fc).unwrap(); with_settings!({ input_file => prql_path }, { assert_snapshot!(test_name, &lineage, &prql) }); } } #[cfg(any(feature = "test-dbs", feature = "test-dbs-external"))] mod results { use itertools::Itertools; use super::*; use crate::dbs::{batch_to_csv, runners}; test_each_path! { in "./prqlc/prqlc/tests/integration/queries" => run } fn run(prql_path: &Path) { let test_name = prql_path.file_stem().unwrap().to_str().unwrap(); let prql = fs::read_to_string(prql_path).unwrap(); // for each of the runners, get the query let results: Vec<(Dialect, String)> = runners() .iter() .filter_map(|runner| { let mut runner = runner.lock().unwrap(); let dialect = runner.dialect(); if !should_run_query(dialect, &prql) { return None; } eprintln!("Executing {test_name} for {dialect}"); match runner.query(&prql) { Ok(batch) => { let csv = batch_to_csv(batch); Some(Ok((dialect, csv))) } Err(e) => Some(Err(e)), } }) .try_collect() .unwrap(); if results.is_empty() { panic!("No valid dialects to run the query at {prql_path:#?} against"); } // similar to `insta::allow_duplicates!`, but with reporting of which two cases are // not matching. let ((first_dialect, first_text), others) = results.split_first().unwrap(); with_settings!({ input_file => prql_path }, { assert_snapshot!(test_name, first_text, &prql) }); for (dialect, text) in others { similar_asserts::assert_eq!( first_text, text, "{} {} {}", test_name, first_dialect, dialect ); } } } ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__aggregation.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mysql:skip\n# clickhouse:skip\n# glaredb:skip (the string_agg function is not supported)\nfrom tracks\nfilter genre_id == 100\nderive empty_name = name == ''\naggregate {sum track_id, concat_array name, all empty_name, any empty_name}\n" input_file: prqlc/prqlc/tests/integration/queries/aggregation.prql --- SELECT COALESCE(SUM(track_id), 0), COALESCE(STRING_AGG(name, ''), ''), COALESCE(BOOL_AND(name = ''), TRUE), COALESCE(BOOL_OR(name = ''), FALSE) FROM tracks WHERE genre_id = 100 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__append_select.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nselect { customer_id, invoice_id, billing_country }\ntake 10..15\nappend (\n from invoices\n select { customer_id, invoice_id, billing_country }\n take 40..45\n)\nselect { billing_country, invoice_id }\n" input_file: prqlc/prqlc/tests/integration/queries/append_select.prql snapshot_kind: text --- SELECT * FROM ( SELECT billing_country, invoice_id FROM invoices LIMIT 6 OFFSET 9 ) AS table_2 UNION ALL SELECT * FROM ( SELECT billing_country, invoice_id FROM invoices LIMIT 6 OFFSET 39 ) AS table_3 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__append_select_compute.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nderive total = case [total < 10 => total * 2, true => total]\nselect { customer_id, invoice_id, total }\ntake 5\nappend (\n from invoice_items\n derive unit_price = case [unit_price < 1 => unit_price * 2, true => unit_price]\n select { invoice_line_id, invoice_id, unit_price }\n take 5\n)\nselect { a = customer_id * 2, b = math.round 1 (invoice_id * total) }\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_compute.prql snapshot_kind: text --- WITH table_1 AS ( SELECT * FROM ( SELECT invoice_id, CASE WHEN total < 10 THEN total * 2 ELSE total END AS _expr_0, customer_id FROM invoices LIMIT 5 ) AS table_3 UNION ALL SELECT * FROM ( SELECT invoice_id, CASE WHEN unit_price < 1 THEN unit_price * 2 ELSE unit_price END AS unit_price, invoice_line_id FROM invoice_items LIMIT 5 ) AS table_4 ) SELECT customer_id * 2 AS a, ROUND(invoice_id * _expr_0, 1) AS b FROM table_1 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__append_select_multiple_with_null.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nselect { customer_id, invoice_id, billing_country }\ntake 5\nappend (\n from employees\n select { employee_id, employee_id, country }\n take 5\n)\nappend (\n from invoice_items\n select { invoice_line_id, invoice_id, null }\n take 5\n)\nselect { billing_country, invoice_id }\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_multiple_with_null.prql snapshot_kind: text --- SELECT * FROM ( SELECT billing_country, invoice_id FROM invoices LIMIT 5 ) AS table_4 UNION ALL SELECT * FROM ( SELECT country, employee_id FROM employees LIMIT 5 ) AS table_5 UNION ALL SELECT * FROM ( SELECT NULL, invoice_id FROM invoice_items LIMIT 5 ) AS table_6 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__append_select_nulls.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# duckdb:skip\n# postgres:skip\n\nfrom invoices\nselect {an_id = invoice_id, name = null}\ntake 2\nappend (\n from employees\n select {an_id = null, name = first_name}\n take 2\n)\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_nulls.prql --- SELECT * FROM ( SELECT invoice_id AS an_id, NULL AS name FROM invoices LIMIT 2 ) AS table_2 UNION ALL SELECT * FROM ( SELECT NULL AS an_id, first_name AS name FROM employees LIMIT 2 ) AS table_3 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__append_select_simple.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nselect { invoice_id, billing_country }\nappend (\n from invoices\n select { invoice_id = `invoice_id` + 100, billing_country }\n)\nfilter (billing_country | text.starts_with(\"I\"))\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_simple.prql --- WITH table_1 AS ( SELECT invoice_id, billing_country FROM invoices UNION ALL SELECT invoice_id + 100 AS invoice_id, billing_country FROM invoices ) SELECT invoice_id, billing_country FROM table_1 WHERE billing_country LIKE CONCAT('I', '%') ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__arithmetic.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom [\n { id = 1, x_int = 13, x_float = 13.0, k_int = 5, k_float = 5.0 },\n { id = 2, x_int = -13, x_float = -13.0, k_int = 5, k_float = 5.0 },\n { id = 3, x_int = 13, x_float = 13.0, k_int = -5, k_float = -5.0 },\n { id = 4, x_int = -13, x_float = -13.0, k_int = -5, k_float = -5.0 },\n]\nselect {\n id,\n\n x_int / k_int,\n x_int / k_float,\n x_float / k_int,\n x_float / k_float,\n\n q_ii = x_int // k_int,\n q_if = x_int // k_float,\n q_fi = x_float // k_int,\n q_ff = x_float // k_float,\n\n r_ii = x_int % k_int,\n r_if = x_int % k_float,\n r_fi = x_float % k_int,\n r_ff = x_float % k_float,\n\n (q_ii * k_int + r_ii | math.round 0),\n (q_if * k_float + r_if | math.round 0),\n (q_fi * k_int + r_fi | math.round 0),\n (q_ff * k_float + r_ff | math.round 0),\n}\nsort id\n" input_file: prqlc/prqlc/tests/integration/queries/arithmetic.prql --- WITH table_0 AS ( SELECT 1 AS id, 13 AS x_int, 13.0 AS x_float, 5 AS k_int, 5.0 AS k_float UNION ALL SELECT 2 AS id, -13 AS x_int, -13.0 AS x_float, 5 AS k_int, 5.0 AS k_float UNION ALL SELECT 3 AS id, 13 AS x_int, 13.0 AS x_float, -5 AS k_int, -5.0 AS k_float UNION ALL SELECT 4 AS id, -13 AS x_int, -13.0 AS x_float, -5 AS k_int, -5.0 AS k_float ) SELECT id, x_int / k_int, x_int / k_float, x_float / k_int, x_float / k_float, FLOOR(ABS(x_int / k_int)) * SIGN(x_int) * SIGN(k_int) AS q_ii, FLOOR(ABS(x_int / k_float)) * SIGN(x_int) * SIGN(k_float) AS q_if, FLOOR(ABS(x_float / k_int)) * SIGN(x_float) * SIGN(k_int) AS q_fi, FLOOR(ABS(x_float / k_float)) * SIGN(x_float) * SIGN(k_float) AS q_ff, x_int % k_int AS r_ii, x_int % k_float AS r_if, x_float % k_int AS r_fi, x_float % k_float AS r_ff, ROUND( FLOOR(ABS(x_int / k_int)) * SIGN(x_int) * SIGN(k_int) * k_int + x_int % k_int, 0 ), ROUND( FLOOR(ABS(x_int / k_float)) * SIGN(x_int) * SIGN(k_float) * k_float + x_int % k_float, 0 ), ROUND( FLOOR(ABS(x_float / k_int)) * SIGN(x_float) * SIGN(k_int) * k_int + x_float % k_int, 0 ), ROUND( FLOOR(ABS(x_float / k_float)) * SIGN(x_float) * SIGN(k_float) * k_float + x_float % k_float, 0 ) FROM table_0 ORDER BY id ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__cast.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nsort {-bytes}\nselect {\n name,\n bin = ((album_id | as REAL) * 99)\n}\ntake 20\n" input_file: prqlc/prqlc/tests/integration/queries/cast.prql --- WITH table_0 AS ( SELECT name, CAST(album_id AS REAL) * 99 AS bin, bytes FROM tracks ORDER BY bytes DESC LIMIT 20 ) SELECT name, bin FROM table_0 ORDER BY bytes DESC ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__constants_only.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from genres\ntake 10\nfilter true\ntake 20\nfilter true\nselect d = 10\n" input_file: prqlc/prqlc/tests/integration/queries/constants_only.prql --- WITH table_1 AS ( SELECT NULL FROM genres LIMIT 10 ), table_0 AS ( SELECT NULL FROM table_1 WHERE true LIMIT 20 ) SELECT 10 AS d FROM table_0 WHERE true ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__distinct.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nselect {album_id, genre_id}\ngroup tracks.* (take 1)\nsort tracks.*\n" input_file: prqlc/prqlc/tests/integration/queries/distinct.prql --- WITH table_0 AS ( SELECT DISTINCT album_id, genre_id FROM tracks ) SELECT album_id, genre_id FROM table_0 ORDER BY album_id, genre_id ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__distinct_on.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nselect {genre_id, media_type_id, album_id}\ngroup {genre_id, media_type_id} (sort {-album_id} | take 1)\nsort {-genre_id, media_type_id}\n" input_file: prqlc/prqlc/tests/integration/queries/distinct_on.prql --- WITH table_0 AS ( SELECT genre_id, media_type_id, album_id, ROW_NUMBER() OVER ( PARTITION BY genre_id, media_type_id ORDER BY album_id DESC ) AS _expr_0 FROM tracks ) SELECT genre_id, media_type_id, album_id FROM table_0 WHERE _expr_0 <= 1 ORDER BY genre_id DESC, media_type_id ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__genre_counts.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip (ClickHouse prefers aliases to column names https://github.com/PRQL/prql/issues/2827)\n# mssql:test\nlet genre_count = (\n from genres\n aggregate {a = count name}\n)\n\nfrom genre_count\nfilter a > 0\nselect a = -a\n" input_file: prqlc/prqlc/tests/integration/queries/genre_counts.prql --- WITH genre_count AS ( SELECT COUNT(*) AS a FROM genres ) SELECT - a AS a FROM genre_count WHERE a > 0 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__group_all.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom a=albums\ntake 10\njoin tracks (==album_id)\ngroup {a.album_id, a.title} (aggregate price = (sum tracks.unit_price | math.round 2))\nsort album_id\n" input_file: prqlc/prqlc/tests/integration/queries/group_all.prql --- WITH table_0 AS ( SELECT album_id, title FROM albums AS a LIMIT 10 ) SELECT table_0.album_id, table_0.title, ROUND(COALESCE(SUM(tracks.unit_price), 0), 2) AS price FROM table_0 INNER JOIN tracks ON table_0.album_id = tracks.album_id GROUP BY table_0.album_id, table_0.title ORDER BY table_0.album_id ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__group_sort.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nderive d = album_id + 1\ngroup d (\n aggregate {\n n1 = (track_id | sum),\n }\n)\nsort d\ntake 10\nselect { d1 = d, n1 }\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort.prql --- WITH table_0 AS ( SELECT COALESCE(SUM(track_id), 0) AS n1, album_id + 1 AS _expr_0 FROM tracks GROUP BY album_id + 1 ), table_1 AS ( SELECT _expr_0 AS d1, n1, _expr_0 FROM table_0 ORDER BY _expr_0 LIMIT 10 ) SELECT d1, n1 FROM table_1 ORDER BY d1 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__group_sort_derive_select_join.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "s\"SELECT album_id,title,artist_id FROM albums\"\ngroup {artist_id} (aggregate { album_title_count = count this.`title`})\nsort {this.artist_id, this.album_title_count}\nderive {new_album_count = this.album_title_count}\nselect {this.artist_id, this.new_album_count}\njoin side:left ( s\"SELECT artist_id,name as artist_name FROM artists\" ) (this.artist_id == that.artist_id)\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort_derive_select_join.prql --- WITH table_0 AS ( SELECT album_id, title, artist_id FROM albums ), table_4 AS ( SELECT artist_id, COUNT(*) AS _expr_0 FROM table_0 GROUP BY artist_id ), table_2 AS ( SELECT artist_id, _expr_0 AS new_album_count, _expr_0 FROM table_4 ), table_1 AS ( SELECT artist_id, name as artist_name FROM artists ), table_3 AS ( SELECT table_2.artist_id, table_2.new_album_count, table_1.artist_id AS _expr_1, table_1.artist_name, table_2._expr_0 FROM table_2 LEFT OUTER JOIN table_1 ON table_2.artist_id = table_1.artist_id ) SELECT artist_id, new_album_count, _expr_1, artist_name FROM table_3 ORDER BY artist_id, new_album_count ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__group_sort_filter_derive_select_join.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "s\"SELECT album_id,title,artist_id FROM albums\"\ngroup {artist_id} (aggregate { album_title_count = count this.`title`})\nsort {this.artist_id, this.album_title_count}\nfilter (this.album_title_count) > 10\nderive {new_album_count = this.album_title_count}\nselect {this.artist_id, this.new_album_count}\njoin side:left ( s\"SELECT artist_id,name as artist_name FROM artists\" ) (this.artist_id == that.artist_id)\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort_filter_derive_select_join.prql --- WITH table_0 AS ( SELECT album_id, title, artist_id FROM albums ), table_3 AS ( SELECT artist_id, COUNT(*) AS _expr_0 FROM table_0 GROUP BY artist_id ), table_4 AS ( SELECT artist_id, _expr_0 AS new_album_count, _expr_0 FROM table_3 WHERE _expr_0 > 10 ), table_2 AS ( SELECT artist_id, new_album_count, _expr_0 FROM table_4 ), table_1 AS ( SELECT artist_id, name as artist_name FROM artists ) SELECT table_2.artist_id, table_2.new_album_count, table_1.artist_id, table_1.artist_name FROM table_2 LEFT OUTER JOIN table_1 ON table_2.artist_id = table_1.artist_id ORDER BY table_2.artist_id, table_2.new_album_count ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__group_sort_limit_take.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# Compute the 3 longest songs for each genre and sort by genre\n# mssql:test\nfrom tracks\nselect {genre_id,milliseconds}\ngroup {genre_id} (\n sort {-milliseconds}\n take 3\n)\njoin genres (==genre_id)\nselect {name, milliseconds}\nsort {+name,-milliseconds}\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort_limit_take.prql --- WITH table_1 AS ( SELECT milliseconds, genre_id, ROW_NUMBER() OVER ( PARTITION BY genre_id ORDER BY milliseconds DESC ) AS _expr_0 FROM tracks ), table_0 AS ( SELECT milliseconds, genre_id FROM table_1 WHERE _expr_0 <= 3 ) SELECT genres.name, table_0.milliseconds FROM table_0 INNER JOIN genres ON table_0.genre_id = genres.genre_id ORDER BY genres.name, table_0.milliseconds DESC ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__invoice_totals.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip (clickhouse doesn't have lag function)\n\n#! Calculate a number of metrics about the sales of tracks in each city.\nfrom i=invoices\njoin ii=invoice_items (==invoice_id)\nderive {\n city = i.billing_city,\n street = i.billing_address,\n}\ngroup {city, street} (\n derive total = ii.unit_price * ii.quantity\n aggregate {\n num_orders = count_distinct i.invoice_id,\n num_tracks = sum ii.quantity,\n total_price = sum total,\n }\n)\ngroup {city} (\n sort street\n window expanding:true (\n derive {running_total_num_tracks = sum num_tracks}\n )\n)\nsort {city, street}\nderive {num_tracks_last_week = lag 7 num_tracks}\nselect {\n city,\n street,\n num_orders,\n num_tracks,\n running_total_num_tracks,\n num_tracks_last_week\n}\ntake 20\n" input_file: prqlc/prqlc/tests/integration/queries/invoice_totals.prql --- WITH table_0 AS ( SELECT i.billing_city AS city, i.billing_address AS street, COUNT(DISTINCT i.invoice_id) AS num_orders, COALESCE(SUM(ii.quantity), 0) AS num_tracks FROM invoices AS i INNER JOIN invoice_items AS ii ON i.invoice_id = ii.invoice_id GROUP BY i.billing_city, i.billing_address ) SELECT city, street, num_orders, num_tracks, SUM(num_tracks) OVER ( PARTITION BY city ORDER BY street ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS running_total_num_tracks, LAG(num_tracks, 7) OVER ( ORDER BY city, street ) AS num_tracks_last_week FROM table_0 ORDER BY city, street LIMIT 20 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__loop_01.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip (DB::Exception: Syntax error)\n# glaredb:skip (DataFusion does not support recursive CTEs https://github.com/apache/arrow-datafusion/issues/462)\nfrom [{n = 1}]\nselect n = n - 2\nloop (filter n < 4 | select n = n + 1)\nselect n = n * 2\nsort n\n" input_file: prqlc/prqlc/tests/integration/queries/loop_01.prql --- WITH RECURSIVE table_0 AS ( SELECT 1 AS n ), table_1 AS ( SELECT n - 2 AS _expr_0 FROM table_0 UNION ALL SELECT _expr_0 + 1 FROM table_1 WHERE _expr_0 < 4 ) SELECT _expr_0 * 2 AS n FROM table_1 AS table_2 ORDER BY n ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__math_module.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\n# sqlite:skip (see https://github.com/rusqlite/rusqlite/issues/1211)\nfrom invoices\ntake 5\nselect {\n total_original = (total | math.round 2),\n total_x = (math.pi - total | math.round 2 | math.abs),\n total_floor = (math.floor total),\n total_ceil = (math.ceil total),\n total_log10 = (math.log10 total | math.round 3),\n total_log2 = (math.log 2 total | math.round 3),\n total_sqrt = (math.sqrt total | math.round 3),\n total_ln = (math.ln total | math.exp | math.round 2),\n total_cos = (math.cos total | math.acos | math.round 2),\n total_sin = (math.sin total | math.asin | math.round 2),\n total_tan = (math.tan total | math.atan | math.round 2),\n total_deg = (total | math.degrees | math.radians | math.round 2),\n total_square = (total | math.pow 2 | math.round 2),\n total_square_op = ((total ** 2) | math.round 2),\n}\n" input_file: prqlc/prqlc/tests/integration/queries/math_module.prql --- SELECT ROUND(total, 2) AS total_original, ABS(ROUND(PI() - total, 2)) AS total_x, FLOOR(total) AS total_floor, CEIL(total) AS total_ceil, ROUND(LOG10(total), 3) AS total_log10, ROUND(LOG10(total) / LOG10(2), 3) AS total_log2, ROUND(SQRT(total), 3) AS total_sqrt, ROUND(EXP(LN(total)), 2) AS total_ln, ROUND(ACOS(COS(total)), 2) AS total_cos, ROUND(ASIN(SIN(total)), 2) AS total_sin, ROUND(ATAN(TAN(total)), 2) AS total_tan, ROUND(RADIANS(DEGREES(total)), 2) AS total_deg, ROUND(POW(total, 2), 2) AS total_square, ROUND(POW(total, 2), 2) AS total_square_op FROM invoices LIMIT 5 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__pipelines.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# sqlite:skip (Only works on Sqlite implementations which have the extension\n# installed\n# https://stackoverflow.com/questions/24037982/how-to-use-regexp-in-sqlite)\n\nfrom tracks\n\nfilter (name ~= \"Love\")\nfilter ((milliseconds / 1000 / 60) | in 3..4)\nsort track_id\ntake 1..15\nselect {name, composer}\n" input_file: prqlc/prqlc/tests/integration/queries/pipelines.prql --- WITH table_0 AS ( SELECT name, composer, track_id FROM tracks WHERE REGEXP(name, 'Love') AND milliseconds / 1000 / 60 BETWEEN 3 AND 4 ORDER BY track_id LIMIT 15 ) SELECT name, composer FROM table_0 ORDER BY track_id ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__read_csv.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# sqlite:skip\n# postgres:skip\n# mysql:skip\nfrom (read_csv \"data_file_root/media_types.csv\")\nappend (read_json \"data_file_root/media_types.json\")\nsort media_type_id\n" input_file: prqlc/prqlc/tests/integration/queries/read_csv.prql --- WITH table_0 AS ( SELECT * FROM read_csv('data_file_root/media_types.csv') ), table_2 AS ( SELECT * FROM table_0 UNION ALL SELECT * FROM read_json('data_file_root/media_types.json') ) SELECT * FROM table_2 ORDER BY media_type_id ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__set_ops_remove.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nlet distinct = rel -> (from t = _param.rel | group {t.*} (take 1))\n\nfrom_text format:json '{ \"columns\": [\"a\"], \"data\": [[1], [2], [2], [3]] }'\ndistinct\nremove (from_text format:json '{ \"columns\": [\"a\"], \"data\": [[1], [2]] }')\nsort a\n" input_file: prqlc/prqlc/tests/integration/queries/set_ops_remove.prql --- WITH table_0 AS ( SELECT 1 AS a UNION ALL SELECT 2 AS a UNION ALL SELECT 2 AS a UNION ALL SELECT 3 AS a ), table_1 AS ( SELECT 1 AS a UNION ALL SELECT 2 AS a ), table_2 AS ( SELECT a FROM table_0 EXCEPT DISTINCT SELECT * FROM table_1 ) SELECT a FROM table_2 ORDER BY a ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__sort.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom e=employees\nfilter first_name != \"Mitchell\"\nsort {first_name, last_name}\n\n# joining may use HashMerge, which can undo ORDER BY\njoin manager=employees side:left (e.reports_to == manager.employee_id)\n\nselect {e.first_name, e.last_name, manager.first_name}\n" input_file: prqlc/prqlc/tests/integration/queries/sort.prql --- WITH table_0 AS ( SELECT first_name, last_name, reports_to FROM employees AS e WHERE first_name <> 'Mitchell' ) SELECT table_0.first_name, table_0.last_name, manager.first_name FROM table_0 LEFT OUTER JOIN employees AS manager ON table_0.reports_to = manager.employee_id ORDER BY table_0.first_name, table_0.last_name ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__sort_2.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from albums\nselect { AA=album_id, artist_id }\nsort AA\nfilter AA >= 25\njoin artists (==artist_id)\n" input_file: prqlc/prqlc/tests/integration/queries/sort_2.prql --- WITH table_1 AS ( SELECT album_id AS "AA", artist_id FROM albums ), table_0 AS ( SELECT "AA", artist_id FROM table_1 WHERE "AA" >= 25 ) SELECT table_0."AA", table_0.artist_id, artists.* FROM table_0 INNER JOIN artists ON table_0.artist_id = artists.artist_id ORDER BY table_0."AA" ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__sort_3.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from [{track_id=0, album_id=1, genre_id=2}]\nselect { AA=track_id, album_id, genre_id }\nsort AA\njoin side:left [{album_id=1, album_title=\"Songs\"}] (==album_id)\nselect { AA, AT = album_title ?? \"unknown\", genre_id }\nfilter AA < 25\njoin side:left [{genre_id=1, genre_title=\"Rock\"}] (==genre_id)\nselect { AA, AT, GT = genre_title ?? \"unknown\" }\n" input_file: prqlc/prqlc/tests/integration/queries/sort_3.prql --- WITH table_0 AS ( SELECT 0 AS track_id, 1 AS album_id, 2 AS genre_id ), table_5 AS ( SELECT track_id AS "AA", genre_id, album_id FROM table_0 ), table_1 AS ( SELECT 1 AS album_id, 'Songs' AS album_title ), table_4 AS ( SELECT table_5."AA", COALESCE(table_1.album_title, 'unknown') AS "AT", table_5.genre_id FROM table_5 LEFT OUTER JOIN table_1 ON table_5.album_id = table_1.album_id ), table_3 AS ( SELECT "AA", "AT", genre_id FROM table_4 WHERE "AA" < 25 ), table_2 AS ( SELECT 1 AS genre_id, 'Rock' AS genre_title ) SELECT table_3."AA", table_3."AT", COALESCE(table_2.genre_title, 'unknown') AS "GT" FROM table_3 LEFT OUTER JOIN table_2 ON table_3.genre_id = table_2.genre_id ORDER BY table_3."AA" ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__switch.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# glaredb:skip (May be a bag of String type conversion for Postgres Client)\n# mssql:test\nfrom tracks\nsort milliseconds\nselect display = case [\n composer != null => composer,\n genre_id < 17 => 'no composer',\n true => f'unknown composer'\n]\ntake 10\n" input_file: prqlc/prqlc/tests/integration/queries/switch.prql --- WITH table_0 AS ( SELECT CASE WHEN composer IS NOT NULL THEN composer WHEN genre_id < 17 THEN 'no composer' ELSE 'unknown composer' END AS display, milliseconds FROM tracks ORDER BY milliseconds LIMIT 10 ) SELECT display FROM table_0 ORDER BY milliseconds ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__take.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nsort {+track_id}\ntake 3..5\n" input_file: prqlc/prqlc/tests/integration/queries/take.prql --- SELECT * FROM tracks ORDER BY track_id LIMIT 3 OFFSET 2 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__text_module.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\n# glaredb:skip — TODO: started raising an error on 2024-05-20; see `window.prql`\n# for more details\nfrom albums\nselect {\n title,\n title_and_spaces = f\" {title} \",\n low = (title | text.lower),\n up = (title | text.upper),\n ltrimmed = (title | text.ltrim),\n rtrimmed = (title | text.rtrim),\n trimmed = (title | text.trim),\n len = (title | text.length),\n subs = (title | text.extract 2 5),\n replace = (title | text.replace \"al\" \"PIKA\"),\n}\nsort {title}\nfilter (title | text.starts_with \"Black\") || (title | text.contains \"Sabbath\") || (title | text.ends_with \"os\")\n" input_file: prqlc/prqlc/tests/integration/queries/text_module.prql --- WITH table_0 AS ( SELECT title, CONCAT(' ', title, ' ') AS title_and_spaces, LOWER(title) AS low, UPPER(title) AS up, LTRIM(title) AS ltrimmed, RTRIM(title) AS rtrimmed, TRIM(title) AS trimmed, CHAR_LENGTH(title) AS len, SUBSTRING(title, 2, 5) AS subs, REPLACE(title, 'al', 'PIKA') AS "replace" FROM albums ) SELECT title, title_and_spaces, low, up, ltrimmed, rtrimmed, trimmed, len, subs, "replace" FROM table_0 WHERE title LIKE CONCAT('Black', '%') OR title LIKE CONCAT('%', 'Sabbath', '%') OR title LIKE CONCAT('%', 'os') ORDER BY title ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compile__window.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip problems with DISTINCT ON\n# glaredb:skip — TODO: started raising an error on 2024-05-20, from https://github.com/PRQL/prql/actions/runs/9154902656/job/25198160283:\n # ERROR: This feature is not implemented: Unsupported ast node in sqltorel:\n # Substring { expr: Identifier(Ident { value: \"title\", quote_style: None }),\n # substring_from: Some(Value(Number(\"2\", false))), substring_for:\n # Some(Value(Number(\"5\", false))), special: true }\nfrom tracks\ngroup genre_id (\n sort milliseconds\n derive {\n num = row_number this,\n total = count this,\n last_val = last track_id,\n }\n take 10\n)\nsort {genre_id, milliseconds}\nselect {track_id, genre_id, num, total, last_val}\nfilter genre_id >= 22\n" input_file: prqlc/prqlc/tests/integration/queries/window.prql --- WITH table_0 AS ( SELECT track_id, genre_id, ROW_NUMBER() OVER ( PARTITION BY genre_id ORDER BY milliseconds ) AS num, COUNT(*) OVER ( PARTITION BY genre_id ORDER BY milliseconds ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS total, LAST_VALUE(track_id) OVER ( PARTITION BY genre_id ORDER BY milliseconds ) AS last_val, milliseconds, ROW_NUMBER() OVER ( PARTITION BY genre_id ORDER BY milliseconds ) AS _expr_0 FROM tracks ), table_1 AS ( SELECT track_id, genre_id, num, total, last_val, milliseconds FROM table_0 WHERE _expr_0 <= 10 AND genre_id >= 22 ) SELECT track_id, genre_id, num, total, last_val FROM table_1 ORDER BY genre_id, milliseconds ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__aggregation.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mysql:skip\n# clickhouse:skip\n# glaredb:skip (the string_agg function is not supported)\nfrom tracks\nfilter genre_id == 100\nderive empty_name = name == ''\naggregate {sum track_id, concat_array name, all empty_name, any empty_name}\n" input_file: prqlc/prqlc/tests/integration/queries/aggregation.prql --- --- generic +++ sqlite @@ -1,9 +1,9 @@ SELECT COALESCE(SUM(track_id), 0), - COALESCE(STRING_AGG(name, ''), ''), - COALESCE(BOOL_AND(name = ''), TRUE), - COALESCE(BOOL_OR(name = ''), FALSE) + COALESCE(GROUP_CONCAT(name, ''), ''), + COALESCE(MIN(name = '') > 0, TRUE), + COALESCE(MAX(name = '') > 0, FALSE) FROM tracks WHERE genre_id = 100 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__append_select.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nselect { customer_id, invoice_id, billing_country }\ntake 10..15\nappend (\n from invoices\n select { customer_id, invoice_id, billing_country }\n take 40..45\n)\nselect { billing_country, invoice_id }\n" input_file: prqlc/prqlc/tests/integration/queries/append_select.prql snapshot_kind: text --- --- generic +++ postgres @@ -1,26 +1,19 @@ -SELECT - * -FROM - ( - SELECT - billing_country, - invoice_id - FROM - invoices - LIMIT - 6 OFFSET 9 - ) AS table_2 +( + SELECT + billing_country, + invoice_id + FROM + invoices + LIMIT + 6 OFFSET 9 +) UNION -ALL -SELECT - * -FROM - ( - SELECT - billing_country, - invoice_id - FROM - invoices - LIMIT - 6 OFFSET 39 - ) AS table_3 +ALL ( + SELECT + billing_country, + invoice_id + FROM + invoices + LIMIT + 6 OFFSET 39 +) --- generic +++ redshift @@ -1,26 +1,19 @@ -SELECT - * -FROM - ( - SELECT - billing_country, - invoice_id - FROM - invoices - LIMIT - 6 OFFSET 9 - ) AS table_2 +( + SELECT + billing_country, + invoice_id + FROM + invoices + LIMIT + 6 OFFSET 9 +) UNION -ALL -SELECT - * -FROM - ( - SELECT - billing_country, - invoice_id - FROM - invoices - LIMIT - 6 OFFSET 39 - ) AS table_3 +ALL ( + SELECT + billing_country, + invoice_id + FROM + invoices + LIMIT + 6 OFFSET 39 +) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__append_select_compute.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nderive total = case [total < 10 => total * 2, true => total]\nselect { customer_id, invoice_id, total }\ntake 5\nappend (\n from invoice_items\n derive unit_price = case [unit_price < 1 => unit_price * 2, true => unit_price]\n select { invoice_line_id, invoice_id, unit_price }\n take 5\n)\nselect { a = customer_id * 2, b = math.round 1 (invoice_id * total) }\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_compute.prql snapshot_kind: text --- --- generic +++ glaredb @@ -29,13 +29,13 @@ END AS unit_price, invoice_line_id FROM invoice_items LIMIT 5 ) AS table_4 ) SELECT customer_id * 2 AS a, - ROUND(invoice_id * _expr_0, 1) AS b + ROUND((invoice_id * _expr_0)::numeric, 1) AS b FROM table_1 --- generic +++ postgres @@ -1,41 +1,34 @@ WITH table_1 AS ( - SELECT - * - FROM - ( - SELECT - invoice_id, - CASE - WHEN total < 10 THEN total * 2 - ELSE total - END AS _expr_0, - customer_id - FROM - invoices - LIMIT - 5 - ) AS table_3 + ( + SELECT + invoice_id, + CASE + WHEN total < 10 THEN total * 2 + ELSE total + END AS _expr_0, + customer_id + FROM + invoices + LIMIT + 5 + ) UNION - ALL - SELECT - * - FROM - ( - SELECT - invoice_id, - CASE - WHEN unit_price < 1 THEN unit_price * 2 - ELSE unit_price - END AS unit_price, - invoice_line_id - FROM - invoice_items - LIMIT - 5 - ) AS table_4 + ALL ( + SELECT + invoice_id, + CASE + WHEN unit_price < 1 THEN unit_price * 2 + ELSE unit_price + END AS unit_price, + invoice_line_id + FROM + invoice_items + LIMIT + 5 + ) ) SELECT customer_id * 2 AS a, - ROUND(invoice_id * _expr_0, 1) AS b + ROUND((invoice_id * _expr_0)::numeric, 1) AS b FROM table_1 --- generic +++ redshift @@ -1,41 +1,34 @@ WITH table_1 AS ( - SELECT - * - FROM - ( - SELECT - invoice_id, - CASE - WHEN total < 10 THEN total * 2 - ELSE total - END AS _expr_0, - customer_id - FROM - invoices - LIMIT - 5 - ) AS table_3 + ( + SELECT + invoice_id, + CASE + WHEN total < 10 THEN total * 2 + ELSE total + END AS _expr_0, + customer_id + FROM + invoices + LIMIT + 5 + ) UNION - ALL - SELECT - * - FROM - ( - SELECT - invoice_id, - CASE - WHEN unit_price < 1 THEN unit_price * 2 - ELSE unit_price - END AS unit_price, - invoice_line_id - FROM - invoice_items - LIMIT - 5 - ) AS table_4 + ALL ( + SELECT + invoice_id, + CASE + WHEN unit_price < 1 THEN unit_price * 2 + ELSE unit_price + END AS unit_price, + invoice_line_id + FROM + invoice_items + LIMIT + 5 + ) ) SELECT customer_id * 2 AS a, ROUND(invoice_id * _expr_0, 1) AS b FROM table_1 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__append_select_multiple_with_null.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nselect { customer_id, invoice_id, billing_country }\ntake 5\nappend (\n from employees\n select { employee_id, employee_id, country }\n take 5\n)\nappend (\n from invoice_items\n select { invoice_line_id, invoice_id, null }\n take 5\n)\nselect { billing_country, invoice_id }\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_multiple_with_null.prql snapshot_kind: text --- --- generic +++ postgres @@ -1,40 +1,29 @@ -SELECT - * -FROM - ( - SELECT - billing_country, - invoice_id - FROM - invoices - LIMIT - 5 - ) AS table_4 +( + SELECT + billing_country, + invoice_id + FROM + invoices + LIMIT + 5 +) UNION -ALL -SELECT - * -FROM - ( - SELECT - country, - employee_id - FROM - employees - LIMIT - 5 - ) AS table_5 +ALL ( + SELECT + country, + employee_id + FROM + employees + LIMIT + 5 +) UNION -ALL -SELECT - * -FROM - ( - SELECT - NULL, - invoice_id - FROM - invoice_items - LIMIT - 5 - ) AS table_6 +ALL ( + SELECT + NULL, + invoice_id + FROM + invoice_items + LIMIT + 5 +) --- generic +++ redshift @@ -1,40 +1,29 @@ -SELECT - * -FROM - ( - SELECT - billing_country, - invoice_id - FROM - invoices - LIMIT - 5 - ) AS table_4 +( + SELECT + billing_country, + invoice_id + FROM + invoices + LIMIT + 5 +) UNION -ALL -SELECT - * -FROM - ( - SELECT - country, - employee_id - FROM - employees - LIMIT - 5 - ) AS table_5 +ALL ( + SELECT + country, + employee_id + FROM + employees + LIMIT + 5 +) UNION -ALL -SELECT - * -FROM - ( - SELECT - NULL, - invoice_id - FROM - invoice_items - LIMIT - 5 - ) AS table_6 +ALL ( + SELECT + NULL, + invoice_id + FROM + invoice_items + LIMIT + 5 +) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__append_select_nulls.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nselect {an_id = invoice_id, name = null}\ntake 2\nappend (\n from employees\n select {an_id = null, name = first_name}\n take 2\n)\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_nulls.prql --- --- generic +++ postgres @@ -1,26 +1,19 @@ -SELECT - * -FROM - ( - SELECT - invoice_id AS an_id, - NULL AS name - FROM - invoices - LIMIT - 2 - ) AS table_2 +( + SELECT + invoice_id AS an_id, + NULL AS name + FROM + invoices + LIMIT + 2 +) UNION -ALL -SELECT - * -FROM - ( - SELECT - NULL AS an_id, - first_name AS name - FROM - employees - LIMIT - 2 - ) AS table_3 +ALL ( + SELECT + NULL AS an_id, + first_name AS name + FROM + employees + LIMIT + 2 +) --- generic +++ redshift @@ -1,26 +1,19 @@ -SELECT - * -FROM - ( - SELECT - invoice_id AS an_id, - NULL AS name - FROM - invoices - LIMIT - 2 - ) AS table_2 +( + SELECT + invoice_id AS an_id, + NULL AS name + FROM + invoices + LIMIT + 2 +) UNION -ALL -SELECT - * -FROM - ( - SELECT - NULL AS an_id, - first_name AS name - FROM - employees - LIMIT - 2 - ) AS table_3 +ALL ( + SELECT + NULL AS an_id, + first_name AS name + FROM + employees + LIMIT + 2 +) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__append_select_simple.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nselect { invoice_id, billing_country }\nappend (\n from invoices\n select { invoice_id = `invoice_id` + 100, billing_country }\n)\nfilter (billing_country | text.starts_with(\"I\"))\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_simple.prql --- --- generic +++ sqlite @@ -11,11 +11,11 @@ billing_country FROM invoices ) SELECT invoice_id, billing_country FROM table_1 WHERE - billing_country LIKE CONCAT('I', '%') + billing_country LIKE 'I' || '%' ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__arithmetic.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom [\n { id = 1, x_int = 13, x_float = 13.0, k_int = 5, k_float = 5.0 },\n { id = 2, x_int = -13, x_float = -13.0, k_int = 5, k_float = 5.0 },\n { id = 3, x_int = 13, x_float = 13.0, k_int = -5, k_float = -5.0 },\n { id = 4, x_int = -13, x_float = -13.0, k_int = -5, k_float = -5.0 },\n]\nselect {\n id,\n\n x_int / k_int,\n x_int / k_float,\n x_float / k_int,\n x_float / k_float,\n\n q_ii = x_int // k_int,\n q_if = x_int // k_float,\n q_fi = x_float // k_int,\n q_ff = x_float // k_float,\n\n r_ii = x_int % k_int,\n r_if = x_int % k_float,\n r_fi = x_float % k_int,\n r_ff = x_float % k_float,\n\n (q_ii * k_int + r_ii | math.round 0),\n (q_if * k_float + r_if | math.round 0),\n (q_fi * k_int + r_fi | math.round 0),\n (q_ff * k_float + r_ff | math.round 0),\n}\nsort id\n" input_file: prqlc/prqlc/tests/integration/queries/arithmetic.prql snapshot_kind: text --- --- generic +++ clickhouse @@ -25,42 +25,36 @@ ALL SELECT 4 AS id, -13 AS x_int, -13.0 AS x_float, -5 AS k_int, -5.0 AS k_float ) SELECT id, - x_int / k_int, - x_int / k_float, - x_float / k_int, - x_float / k_float, - FLOOR(ABS(x_int / k_int)) * SIGN(x_int) * SIGN(k_int) AS q_ii, - FLOOR(ABS(x_int / k_float)) * SIGN(x_int) * SIGN(k_float) AS q_if, - FLOOR(ABS(x_float / k_int)) * SIGN(x_float) * SIGN(k_int) AS q_fi, - FLOOR(ABS(x_float / k_float)) * SIGN(x_float) * SIGN(k_float) AS q_ff, + (x_int / k_int), + (x_int / k_float), + (x_float / k_int), + (x_float / k_float), + (x_int DIV k_int) AS q_ii, + (x_int DIV k_float) AS q_if, + (x_float DIV k_int) AS q_fi, + (x_float DIV k_float) AS q_ff, x_int % k_int AS r_ii, x_int % k_float AS r_if, x_float % k_int AS r_fi, x_float % k_float AS r_ff, - ROUND( - FLOOR(ABS(x_int / k_int)) * SIGN(x_int) * SIGN(k_int) * k_int + x_int % k_int, - 0 - ), - ROUND( - FLOOR(ABS(x_int / k_float)) * SIGN(x_int) * SIGN(k_float) * k_float + x_int % k_float, - 0 - ), + ROUND((x_int DIV k_int) * k_int + x_int % k_int, 0), ROUND( - FLOOR(ABS(x_float / k_int)) * SIGN(x_float) * SIGN(k_int) * k_int + x_float % k_int, + (x_int DIV k_float) * k_float + x_int % k_float, 0 ), + ROUND((x_float DIV k_int) * k_int + x_float % k_int, 0), ROUND( - FLOOR(ABS(x_float / k_float)) * SIGN(x_float) * SIGN(k_float) * k_float + x_float % k_float, + (x_float DIV k_float) * k_float + x_float % k_float, 0 ) FROM table_0 ORDER BY id --- generic +++ duckdb @@ -25,42 +25,39 @@ ALL SELECT 4 AS id, -13 AS x_int, -13.0 AS x_float, -5 AS k_int, -5.0 AS k_float ) SELECT id, - x_int / k_int, - x_int / k_float, - x_float / k_int, - x_float / k_float, - FLOOR(ABS(x_int / k_int)) * SIGN(x_int) * SIGN(k_int) AS q_ii, - FLOOR(ABS(x_int / k_float)) * SIGN(x_int) * SIGN(k_float) AS q_if, - FLOOR(ABS(x_float / k_int)) * SIGN(x_float) * SIGN(k_int) AS q_fi, - FLOOR(ABS(x_float / k_float)) * SIGN(x_float) * SIGN(k_float) AS q_ff, + (x_int / k_int), + (x_int / k_float), + (x_float / k_int), + (x_float / k_float), + TRUNC(x_int / k_int) AS q_ii, + TRUNC(x_int / k_float) AS q_if, + TRUNC(x_float / k_int) AS q_fi, + TRUNC(x_float / k_float) AS q_ff, x_int % k_int AS r_ii, x_int % k_float AS r_if, x_float % k_int AS r_fi, x_float % k_float AS r_ff, - ROUND( - FLOOR(ABS(x_int / k_int)) * SIGN(x_int) * SIGN(k_int) * k_int + x_int % k_int, - 0 - ), + ROUND(TRUNC(x_int / k_int) * k_int + x_int % k_int, 0), ROUND( - FLOOR(ABS(x_int / k_float)) * SIGN(x_int) * SIGN(k_float) * k_float + x_int % k_float, + TRUNC(x_int / k_float) * k_float + x_int % k_float, 0 ), ROUND( - FLOOR(ABS(x_float / k_int)) * SIGN(x_float) * SIGN(k_int) * k_int + x_float % k_int, + TRUNC(x_float / k_int) * k_int + x_float % k_int, 0 ), ROUND( - FLOOR(ABS(x_float / k_float)) * SIGN(x_float) * SIGN(k_float) * k_float + x_float % k_float, + TRUNC(x_float / k_float) * k_float + x_float % k_float, 0 ) FROM table_0 ORDER BY id --- generic +++ glaredb @@ -25,42 +25,46 @@ ALL SELECT 4 AS id, -13 AS x_int, -13.0 AS x_float, -5 AS k_int, -5.0 AS k_float ) SELECT id, - x_int / k_int, - x_int / k_float, - x_float / k_int, - x_float / k_float, - FLOOR(ABS(x_int / k_int)) * SIGN(x_int) * SIGN(k_int) AS q_ii, - FLOOR(ABS(x_int / k_float)) * SIGN(x_int) * SIGN(k_float) AS q_if, - FLOOR(ABS(x_float / k_int)) * SIGN(x_float) * SIGN(k_int) AS q_fi, - FLOOR(ABS(x_float / k_float)) * SIGN(x_float) * SIGN(k_float) AS q_ff, + (x_int * 1.0 / k_int), + (x_int * 1.0 / k_float), + (x_float * 1.0 / k_int), + (x_float * 1.0 / k_float), + TRUNC(x_int / k_int) AS q_ii, + TRUNC(x_int / k_float) AS q_if, + TRUNC(x_float / k_int) AS q_fi, + TRUNC(x_float / k_float) AS q_ff, x_int % k_int AS r_ii, x_int % k_float AS r_if, x_float % k_int AS r_fi, x_float % k_float AS r_ff, ROUND( - FLOOR(ABS(x_int / k_int)) * SIGN(x_int) * SIGN(k_int) * k_int + x_int % k_int, + (TRUNC(x_int / k_int) * k_int + x_int % k_int)::numeric, 0 ), ROUND( - FLOOR(ABS(x_int / k_float)) * SIGN(x_int) * SIGN(k_float) * k_float + x_int % k_float, + ( + TRUNC(x_int / k_float) * k_float + x_int % k_float + )::numeric, 0 ), ROUND( - FLOOR(ABS(x_float / k_int)) * SIGN(x_float) * SIGN(k_int) * k_int + x_float % k_int, + (TRUNC(x_float / k_int) * k_int + x_float % k_int)::numeric, 0 ), ROUND( - FLOOR(ABS(x_float / k_float)) * SIGN(x_float) * SIGN(k_float) * k_float + x_float % k_float, + ( + TRUNC(x_float / k_float) * k_float + x_float % k_float + )::numeric, 0 ) FROM table_0 ORDER BY id --- generic +++ mssql @@ -25,24 +25,24 @@ ALL SELECT 4 AS id, -13 AS x_int, -13.0 AS x_float, -5 AS k_int, -5.0 AS k_float ) SELECT id, - x_int / k_int, - x_int / k_float, - x_float / k_int, - x_float / k_float, + (x_int * 1.0 / k_int), + (x_int * 1.0 / k_float), + (x_float * 1.0 / k_int), + (x_float * 1.0 / k_float), FLOOR(ABS(x_int / k_int)) * SIGN(x_int) * SIGN(k_int) AS q_ii, FLOOR(ABS(x_int / k_float)) * SIGN(x_int) * SIGN(k_float) AS q_if, FLOOR(ABS(x_float / k_int)) * SIGN(x_float) * SIGN(k_int) AS q_fi, FLOOR(ABS(x_float / k_float)) * SIGN(x_float) * SIGN(k_float) AS q_ff, x_int % k_int AS r_ii, x_int % k_float AS r_if, x_float % k_int AS r_fi, x_float % k_float AS r_ff, ROUND( FLOOR(ABS(x_int / k_int)) * SIGN(x_int) * SIGN(k_int) * k_int + x_int % k_int, --- generic +++ mysql @@ -25,42 +25,42 @@ ALL SELECT 4 AS id, -13 AS x_int, -13.0 AS x_float, -5 AS k_int, -5.0 AS k_float ) SELECT id, - x_int / k_int, - x_int / k_float, - x_float / k_int, - x_float / k_float, - FLOOR(ABS(x_int / k_int)) * SIGN(x_int) * SIGN(k_int) AS q_ii, - FLOOR(ABS(x_int / k_float)) * SIGN(x_int) * SIGN(k_float) AS q_if, - FLOOR(ABS(x_float / k_int)) * SIGN(x_float) * SIGN(k_int) AS q_fi, - FLOOR(ABS(x_float / k_float)) * SIGN(x_float) * SIGN(k_float) AS q_ff, - x_int % k_int AS r_ii, - x_int % k_float AS r_if, - x_float % k_int AS r_fi, - x_float % k_float AS r_ff, + (x_int / k_int), + (x_int / k_float), + (x_float / k_int), + (x_float / k_float), + (x_int DIV k_int) AS q_ii, + (x_int DIV k_float) AS q_if, + (x_float DIV k_int) AS q_fi, + (x_float DIV k_float) AS q_ff, + ROUND(MOD(x_int, k_int)) AS r_ii, + ROUND(MOD(x_int, k_float)) AS r_if, + ROUND(MOD(x_float, k_int)) AS r_fi, + ROUND(MOD(x_float, k_float)) AS r_ff, ROUND( - FLOOR(ABS(x_int / k_int)) * SIGN(x_int) * SIGN(k_int) * k_int + x_int % k_int, + (x_int DIV k_int) * k_int + ROUND(MOD(x_int, k_int)), 0 ), ROUND( - FLOOR(ABS(x_int / k_float)) * SIGN(x_int) * SIGN(k_float) * k_float + x_int % k_float, + (x_int DIV k_float) * k_float + ROUND(MOD(x_int, k_float)), 0 ), ROUND( - FLOOR(ABS(x_float / k_int)) * SIGN(x_float) * SIGN(k_int) * k_int + x_float % k_int, + (x_float DIV k_int) * k_int + ROUND(MOD(x_float, k_int)), 0 ), ROUND( - FLOOR(ABS(x_float / k_float)) * SIGN(x_float) * SIGN(k_float) * k_float + x_float % k_float, + (x_float DIV k_float) * k_float + ROUND(MOD(x_float, k_float)), 0 ) FROM table_0 ORDER BY id --- generic +++ postgres @@ -25,42 +25,46 @@ ALL SELECT 4 AS id, -13 AS x_int, -13.0 AS x_float, -5 AS k_int, -5.0 AS k_float ) SELECT id, - x_int / k_int, - x_int / k_float, - x_float / k_int, - x_float / k_float, - FLOOR(ABS(x_int / k_int)) * SIGN(x_int) * SIGN(k_int) AS q_ii, - FLOOR(ABS(x_int / k_float)) * SIGN(x_int) * SIGN(k_float) AS q_if, - FLOOR(ABS(x_float / k_int)) * SIGN(x_float) * SIGN(k_int) AS q_fi, - FLOOR(ABS(x_float / k_float)) * SIGN(x_float) * SIGN(k_float) AS q_ff, + (x_int * 1.0 / k_int), + (x_int * 1.0 / k_float), + (x_float * 1.0 / k_int), + (x_float * 1.0 / k_float), + TRUNC(x_int / k_int) AS q_ii, + TRUNC(x_int / k_float) AS q_if, + TRUNC(x_float / k_int) AS q_fi, + TRUNC(x_float / k_float) AS q_ff, x_int % k_int AS r_ii, x_int % k_float AS r_if, x_float % k_int AS r_fi, x_float % k_float AS r_ff, ROUND( - FLOOR(ABS(x_int / k_int)) * SIGN(x_int) * SIGN(k_int) * k_int + x_int % k_int, + (TRUNC(x_int / k_int) * k_int + x_int % k_int)::numeric, 0 ), ROUND( - FLOOR(ABS(x_int / k_float)) * SIGN(x_int) * SIGN(k_float) * k_float + x_int % k_float, + ( + TRUNC(x_int / k_float) * k_float + x_int % k_float + )::numeric, 0 ), ROUND( - FLOOR(ABS(x_float / k_int)) * SIGN(x_float) * SIGN(k_int) * k_int + x_float % k_int, + (TRUNC(x_float / k_int) * k_int + x_float % k_int)::numeric, 0 ), ROUND( - FLOOR(ABS(x_float / k_float)) * SIGN(x_float) * SIGN(k_float) * k_float + x_float % k_float, + ( + TRUNC(x_float / k_float) * k_float + x_float % k_float + )::numeric, 0 ) FROM table_0 ORDER BY id --- generic +++ redshift @@ -25,24 +25,24 @@ ALL SELECT 4 AS id, -13 AS x_int, -13.0 AS x_float, -5 AS k_int, -5.0 AS k_float ) SELECT id, - x_int / k_int, - x_int / k_float, - x_float / k_int, - x_float / k_float, + (x_int * 1.0 / k_int), + (x_int * 1.0 / k_float), + (x_float * 1.0 / k_int), + (x_float * 1.0 / k_float), FLOOR(ABS(x_int / k_int)) * SIGN(x_int) * SIGN(k_int) AS q_ii, FLOOR(ABS(x_int / k_float)) * SIGN(x_int) * SIGN(k_float) AS q_if, FLOOR(ABS(x_float / k_int)) * SIGN(x_float) * SIGN(k_int) AS q_fi, FLOOR(ABS(x_float / k_float)) * SIGN(x_float) * SIGN(k_float) AS q_ff, x_int % k_int AS r_ii, x_int % k_float AS r_if, x_float % k_int AS r_fi, x_float % k_float AS r_ff, ROUND( FLOOR(ABS(x_int / k_int)) * SIGN(x_int) * SIGN(k_int) * k_int + x_int % k_int, --- generic +++ sqlite @@ -25,42 +25,42 @@ ALL SELECT 4 AS id, -13 AS x_int, -13.0 AS x_float, -5 AS k_int, -5.0 AS k_float ) SELECT id, - x_int / k_int, - x_int / k_float, - x_float / k_int, - x_float / k_float, - FLOOR(ABS(x_int / k_int)) * SIGN(x_int) * SIGN(k_int) AS q_ii, - FLOOR(ABS(x_int / k_float)) * SIGN(x_int) * SIGN(k_float) AS q_if, - FLOOR(ABS(x_float / k_int)) * SIGN(x_float) * SIGN(k_int) AS q_fi, - FLOOR(ABS(x_float / k_float)) * SIGN(x_float) * SIGN(k_float) AS q_ff, + (x_int * 1.0 / k_int), + (x_int * 1.0 / k_float), + (x_float * 1.0 / k_int), + (x_float * 1.0 / k_float), + ROUND(ABS(x_int / k_int) - 0.5) * SIGN(x_int) * SIGN(k_int) AS q_ii, + ROUND(ABS(x_int / k_float) - 0.5) * SIGN(x_int) * SIGN(k_float) AS q_if, + ROUND(ABS(x_float / k_int) - 0.5) * SIGN(x_float) * SIGN(k_int) AS q_fi, + ROUND(ABS(x_float / k_float) - 0.5) * SIGN(x_float) * SIGN(k_float) AS q_ff, x_int % k_int AS r_ii, x_int % k_float AS r_if, x_float % k_int AS r_fi, x_float % k_float AS r_ff, ROUND( - FLOOR(ABS(x_int / k_int)) * SIGN(x_int) * SIGN(k_int) * k_int + x_int % k_int, + ROUND(ABS(x_int / k_int) - 0.5) * SIGN(x_int) * SIGN(k_int) * k_int + x_int % k_int, 0 ), ROUND( - FLOOR(ABS(x_int / k_float)) * SIGN(x_int) * SIGN(k_float) * k_float + x_int % k_float, + ROUND(ABS(x_int / k_float) - 0.5) * SIGN(x_int) * SIGN(k_float) * k_float + x_int % k_float, 0 ), ROUND( - FLOOR(ABS(x_float / k_int)) * SIGN(x_float) * SIGN(k_int) * k_int + x_float % k_int, + ROUND(ABS(x_float / k_int) - 0.5) * SIGN(x_float) * SIGN(k_int) * k_int + x_float % k_int, 0 ), ROUND( - FLOOR(ABS(x_float / k_float)) * SIGN(x_float) * SIGN(k_float) * k_float + x_float % k_float, + ROUND(ABS(x_float / k_float) - 0.5) * SIGN(x_float) * SIGN(k_float) * k_float + x_float % k_float, 0 ) FROM table_0 ORDER BY id ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__cast.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nsort {-bytes}\nselect {\n name,\n bin = ((album_id | as REAL) * 99)\n}\ntake 20\n" input_file: prqlc/prqlc/tests/integration/queries/cast.prql --- --- generic +++ mssql @@ -1,19 +1,19 @@ WITH table_0 AS ( SELECT name, CAST(album_id AS REAL) * 99 AS bin, bytes FROM tracks ORDER BY - bytes DESC - LIMIT - 20 + bytes DESC OFFSET 0 ROWS + FETCH FIRST + 20 ROWS ONLY ) SELECT name, bin FROM table_0 ORDER BY bytes DESC ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__constants_only.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from genres\ntake 10\nfilter true\ntake 20\nfilter true\nselect d = 10\n" input_file: prqlc/prqlc/tests/integration/queries/constants_only.prql --- --- generic +++ postgres @@ -1,20 +1,18 @@ WITH table_1 AS ( SELECT - NULL FROM genres LIMIT 10 ), table_0 AS ( SELECT - NULL FROM table_1 WHERE true LIMIT 20 ) SELECT 10 AS d FROM --- generic +++ redshift @@ -1,20 +1,18 @@ WITH table_1 AS ( SELECT - NULL FROM genres LIMIT 10 ), table_0 AS ( SELECT - NULL FROM table_1 WHERE true LIMIT 20 ) SELECT 10 AS d FROM ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__distinct.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nselect {album_id, genre_id}\ngroup tracks.* (take 1)\nsort tracks.*\n" input_file: prqlc/prqlc/tests/integration/queries/distinct.prql --- ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__distinct_on.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nselect {genre_id, media_type_id, album_id}\ngroup {genre_id, media_type_id} (sort {-album_id} | take 1)\nsort {-genre_id, media_type_id}\n" input_file: prqlc/prqlc/tests/integration/queries/distinct_on.prql --- --- generic +++ clickhouse @@ -1,25 +1,21 @@ WITH table_0 AS ( SELECT - genre_id, + DISTINCT ON (genre_id, media_type_id) genre_id, media_type_id, - album_id, - ROW_NUMBER() OVER ( - PARTITION BY genre_id, - media_type_id - ORDER BY - album_id DESC - ) AS _expr_0 + album_id FROM tracks + ORDER BY + genre_id, + media_type_id, + album_id DESC ) SELECT genre_id, media_type_id, album_id FROM table_0 -WHERE - _expr_0 <= 1 ORDER BY genre_id DESC, media_type_id --- generic +++ duckdb @@ -1,25 +1,21 @@ WITH table_0 AS ( SELECT - genre_id, + DISTINCT ON (genre_id, media_type_id) genre_id, media_type_id, - album_id, - ROW_NUMBER() OVER ( - PARTITION BY genre_id, - media_type_id - ORDER BY - album_id DESC - ) AS _expr_0 + album_id FROM tracks + ORDER BY + genre_id, + media_type_id, + album_id DESC ) SELECT genre_id, media_type_id, album_id FROM table_0 -WHERE - _expr_0 <= 1 ORDER BY genre_id DESC, media_type_id --- generic +++ postgres @@ -1,25 +1,21 @@ WITH table_0 AS ( SELECT - genre_id, + DISTINCT ON (genre_id, media_type_id) genre_id, media_type_id, - album_id, - ROW_NUMBER() OVER ( - PARTITION BY genre_id, - media_type_id - ORDER BY - album_id DESC - ) AS _expr_0 + album_id FROM tracks + ORDER BY + genre_id, + media_type_id, + album_id DESC ) SELECT genre_id, media_type_id, album_id FROM table_0 -WHERE - _expr_0 <= 1 ORDER BY genre_id DESC, media_type_id ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__genre_counts.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip (ClickHouse prefers aliases to column names https://github.com/PRQL/prql/issues/2827)\n# mssql:test\nlet genre_count = (\n from genres\n aggregate {a = count name}\n)\n\nfrom genre_count\nfilter a > 0\nselect a = -a\n" input_file: prqlc/prqlc/tests/integration/queries/genre_counts.prql --- ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__group_all.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom a=albums\ntake 10\njoin tracks (==album_id)\ngroup {a.album_id, a.title} (aggregate price = (sum tracks.unit_price | math.round 2))\nsort album_id\n" input_file: prqlc/prqlc/tests/integration/queries/group_all.prql --- --- generic +++ glaredb @@ -3,19 +3,22 @@ album_id, title FROM albums AS a LIMIT 10 ) SELECT table_0.album_id, table_0.title, - ROUND(COALESCE(SUM(tracks.unit_price), 0), 2) AS price + ROUND( + (COALESCE(SUM(tracks.unit_price), 0))::numeric, + 2 + ) AS price FROM table_0 INNER JOIN tracks ON table_0.album_id = tracks.album_id GROUP BY table_0.album_id, table_0.title ORDER BY table_0.album_id --- generic +++ mssql @@ -1,18 +1,23 @@ WITH table_0 AS ( SELECT album_id, title FROM albums AS a - LIMIT - 10 + ORDER BY + ( + SELECT + NULL + ) OFFSET 0 ROWS + FETCH FIRST + 10 ROWS ONLY ) SELECT table_0.album_id, table_0.title, ROUND(COALESCE(SUM(tracks.unit_price), 0), 2) AS price FROM table_0 INNER JOIN tracks ON table_0.album_id = tracks.album_id GROUP BY table_0.album_id, --- generic +++ postgres @@ -3,19 +3,22 @@ album_id, title FROM albums AS a LIMIT 10 ) SELECT table_0.album_id, table_0.title, - ROUND(COALESCE(SUM(tracks.unit_price), 0), 2) AS price + ROUND( + (COALESCE(SUM(tracks.unit_price), 0))::numeric, + 2 + ) AS price FROM table_0 INNER JOIN tracks ON table_0.album_id = tracks.album_id GROUP BY table_0.album_id, table_0.title ORDER BY table_0.album_id ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__group_sort.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nderive d = album_id + 1\ngroup d (\n aggregate {\n n1 = (track_id | sum),\n }\n)\nsort d\ntake 10\nselect { d1 = d, n1 }\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort.prql --- --- generic +++ mssql @@ -8,21 +8,21 @@ album_id + 1 ), table_1 AS ( SELECT _expr_0 AS d1, n1, _expr_0 FROM table_0 ORDER BY - _expr_0 - LIMIT - 10 + _expr_0 OFFSET 0 ROWS + FETCH FIRST + 10 ROWS ONLY ) SELECT d1, n1 FROM table_1 ORDER BY d1 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__group_sort_derive_select_join.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "s\"SELECT album_id,title,artist_id FROM albums\"\ngroup {artist_id} (aggregate { album_title_count = count this.`title`})\nsort {this.artist_id, this.album_title_count}\nderive {new_album_count = this.album_title_count}\nselect {this.artist_id, this.new_album_count}\njoin side:left ( s\"SELECT artist_id,name as artist_name FROM artists\" ) (this.artist_id == that.artist_id)\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort_derive_select_join.prql --- ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__group_sort_filter_derive_select_join.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "s\"SELECT album_id,title,artist_id FROM albums\"\ngroup {artist_id} (aggregate { album_title_count = count this.`title`})\nsort {this.artist_id, this.album_title_count}\nfilter (this.album_title_count) > 10\nderive {new_album_count = this.album_title_count}\nselect {this.artist_id, this.new_album_count}\njoin side:left ( s\"SELECT artist_id,name as artist_name FROM artists\" ) (this.artist_id == that.artist_id)\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort_filter_derive_select_join.prql --- ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__group_sort_limit_take.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# Compute the 3 longest songs for each genre and sort by genre\n# mssql:test\nfrom tracks\nselect {genre_id,milliseconds}\ngroup {genre_id} (\n sort {-milliseconds}\n take 3\n)\njoin genres (==genre_id)\nselect {name, milliseconds}\nsort {+name,-milliseconds}\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort_limit_take.prql --- ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__invoice_totals.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip (clickhouse doesn't have lag function)\n\n#! Calculate a number of metrics about the sales of tracks in each city.\nfrom i=invoices\njoin ii=invoice_items (==invoice_id)\nderive {\n city = i.billing_city,\n street = i.billing_address,\n}\ngroup {city, street} (\n derive total = ii.unit_price * ii.quantity\n aggregate {\n num_orders = count_distinct i.invoice_id,\n num_tracks = sum ii.quantity,\n total_price = sum total,\n }\n)\ngroup {city} (\n sort street\n window expanding:true (\n derive {running_total_num_tracks = sum num_tracks}\n )\n)\nsort {city, street}\nderive {num_tracks_last_week = lag 7 num_tracks}\nselect {\n city,\n street,\n num_orders,\n num_tracks,\n running_total_num_tracks,\n num_tracks_last_week\n}\ntake 20\n" input_file: prqlc/prqlc/tests/integration/queries/invoice_totals.prql --- ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__loop_01.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip (DB::Exception: Syntax error)\n# glaredb:skip (DataFusion does not support recursive CTEs https://github.com/apache/arrow-datafusion/issues/462)\nfrom [{n = 1}]\nselect n = n - 2\nloop (filter n < 4 | select n = n + 1)\nselect n = n * 2\nsort n\n" input_file: prqlc/prqlc/tests/integration/queries/loop_01.prql --- ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__math_module.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\n# sqlite:skip (see https://github.com/rusqlite/rusqlite/issues/1211)\nfrom invoices\ntake 5\nselect {\n total_original = (total | math.round 2),\n total_x = (math.pi - total | math.round 2 | math.abs),\n total_floor = (math.floor total),\n total_ceil = (math.ceil total),\n total_log10 = (math.log10 total | math.round 3),\n total_log2 = (math.log 2 total | math.round 3),\n total_sqrt = (math.sqrt total | math.round 3),\n total_ln = (math.ln total | math.exp | math.round 2),\n total_cos = (math.cos total | math.acos | math.round 2),\n total_sin = (math.sin total | math.asin | math.round 2),\n total_tan = (math.tan total | math.atan | math.round 2),\n total_deg = (total | math.degrees | math.radians | math.round 2),\n total_square = (total | math.pow 2 | math.round 2),\n total_square_op = ((total ** 2) | math.round 2),\n}\n" input_file: prqlc/prqlc/tests/integration/queries/math_module.prql --- --- generic +++ glaredb @@ -1,19 +1,19 @@ SELECT - ROUND(total, 2) AS total_original, - ABS(ROUND(PI() - total, 2)) AS total_x, + ROUND((total)::numeric, 2) AS total_original, + ABS(ROUND((PI() - total)::numeric, 2)) AS total_x, FLOOR(total) AS total_floor, CEIL(total) AS total_ceil, - ROUND(LOG10(total), 3) AS total_log10, - ROUND(LOG10(total) / LOG10(2), 3) AS total_log2, - ROUND(SQRT(total), 3) AS total_sqrt, - ROUND(EXP(LN(total)), 2) AS total_ln, - ROUND(ACOS(COS(total)), 2) AS total_cos, - ROUND(ASIN(SIN(total)), 2) AS total_sin, - ROUND(ATAN(TAN(total)), 2) AS total_tan, - ROUND(RADIANS(DEGREES(total)), 2) AS total_deg, - ROUND(POW(total, 2), 2) AS total_square, - ROUND(POW(total, 2), 2) AS total_square_op + ROUND((LOG10(total))::numeric, 3) AS total_log10, + ROUND((LOG10(total) / LOG10(2))::numeric, 3) AS total_log2, + ROUND((SQRT(total))::numeric, 3) AS total_sqrt, + ROUND((EXP(LN(total)))::numeric, 2) AS total_ln, + ROUND((ACOS(COS(total)))::numeric, 2) AS total_cos, + ROUND((ASIN(SIN(total)))::numeric, 2) AS total_sin, + ROUND((ATAN(TAN(total)))::numeric, 2) AS total_tan, + ROUND((RADIANS(DEGREES(total)))::numeric, 2) AS total_deg, + ROUND((POW(total, 2))::numeric, 2) AS total_square, + ROUND((POW(total, 2))::numeric, 2) AS total_square_op FROM invoices LIMIT 5 --- generic +++ mssql @@ -1,19 +1,24 @@ SELECT ROUND(total, 2) AS total_original, ABS(ROUND(PI() - total, 2)) AS total_x, FLOOR(total) AS total_floor, - CEIL(total) AS total_ceil, + CEILING(total) AS total_ceil, ROUND(LOG10(total), 3) AS total_log10, ROUND(LOG10(total) / LOG10(2), 3) AS total_log2, ROUND(SQRT(total), 3) AS total_sqrt, - ROUND(EXP(LN(total)), 2) AS total_ln, + ROUND(EXP(LOG(total)), 2) AS total_ln, ROUND(ACOS(COS(total)), 2) AS total_cos, ROUND(ASIN(SIN(total)), 2) AS total_sin, ROUND(ATAN(TAN(total)), 2) AS total_tan, ROUND(RADIANS(DEGREES(total)), 2) AS total_deg, - ROUND(POW(total, 2), 2) AS total_square, - ROUND(POW(total, 2), 2) AS total_square_op + ROUND(POWER(total, 2), 2) AS total_square, + ROUND(POWER(total, 2), 2) AS total_square_op FROM invoices -LIMIT - 5 +ORDER BY + ( + SELECT + NULL + ) OFFSET 0 ROWS +FETCH FIRST + 5 ROWS ONLY --- generic +++ postgres @@ -1,19 +1,19 @@ SELECT - ROUND(total, 2) AS total_original, - ABS(ROUND(PI() - total, 2)) AS total_x, + ROUND((total)::numeric, 2) AS total_original, + ABS(ROUND((PI() - total)::numeric, 2)) AS total_x, FLOOR(total) AS total_floor, CEIL(total) AS total_ceil, - ROUND(LOG10(total), 3) AS total_log10, - ROUND(LOG10(total) / LOG10(2), 3) AS total_log2, - ROUND(SQRT(total), 3) AS total_sqrt, - ROUND(EXP(LN(total)), 2) AS total_ln, - ROUND(ACOS(COS(total)), 2) AS total_cos, - ROUND(ASIN(SIN(total)), 2) AS total_sin, - ROUND(ATAN(TAN(total)), 2) AS total_tan, - ROUND(RADIANS(DEGREES(total)), 2) AS total_deg, - ROUND(POW(total, 2), 2) AS total_square, - ROUND(POW(total, 2), 2) AS total_square_op + ROUND((LOG10(total))::numeric, 3) AS total_log10, + ROUND((LOG10(total) / LOG10(2))::numeric, 3) AS total_log2, + ROUND((SQRT(total))::numeric, 3) AS total_sqrt, + ROUND((EXP(LN(total)))::numeric, 2) AS total_ln, + ROUND((ACOS(COS(total)))::numeric, 2) AS total_cos, + ROUND((ASIN(SIN(total)))::numeric, 2) AS total_sin, + ROUND((ATAN(TAN(total)))::numeric, 2) AS total_tan, + ROUND((RADIANS(DEGREES(total)))::numeric, 2) AS total_deg, + ROUND((POW(total, 2))::numeric, 2) AS total_square, + ROUND((POW(total, 2))::numeric, 2) AS total_square_op FROM invoices LIMIT 5 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__pipelines.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# sqlite:skip (Only works on Sqlite implementations which have the extension\n# installed\n# https://stackoverflow.com/questions/24037982/how-to-use-regexp-in-sqlite)\n\nfrom tracks\n\nfilter (name ~= \"Love\")\nfilter ((milliseconds / 1000 / 60) | in 3..4)\nsort track_id\ntake 1..15\nselect {name, composer}\n" input_file: prqlc/prqlc/tests/integration/queries/pipelines.prql snapshot_kind: text --- --- generic +++ clickhouse @@ -1,20 +1,20 @@ WITH table_0 AS ( SELECT name, composer, track_id FROM tracks WHERE - REGEXP(name, 'Love') - AND milliseconds / 1000 / 60 BETWEEN 3 AND 4 + match(name, 'Love') + AND ((milliseconds / 1000) / 60) BETWEEN 3 AND 4 ORDER BY track_id LIMIT 15 ) SELECT name, composer FROM table_0 --- generic +++ duckdb @@ -1,20 +1,20 @@ WITH table_0 AS ( SELECT name, composer, track_id FROM tracks WHERE - REGEXP(name, 'Love') - AND milliseconds / 1000 / 60 BETWEEN 3 AND 4 + REGEXP_MATCHES(name, 'Love') + AND ((milliseconds / 1000) / 60) BETWEEN 3 AND 4 ORDER BY track_id LIMIT 15 ) SELECT name, composer FROM table_0 --- generic +++ glaredb @@ -1,20 +1,20 @@ WITH table_0 AS ( SELECT name, composer, track_id FROM tracks WHERE - REGEXP(name, 'Love') - AND milliseconds / 1000 / 60 BETWEEN 3 AND 4 + name ~ 'Love' + AND ((milliseconds * 1.0 / 1000) * 1.0 / 60) BETWEEN 3 AND 4 ORDER BY track_id LIMIT 15 ) SELECT name, composer FROM table_0 --- generic +++ mysql @@ -1,20 +1,20 @@ WITH table_0 AS ( SELECT name, composer, track_id FROM tracks WHERE - REGEXP(name, 'Love') - AND milliseconds / 1000 / 60 BETWEEN 3 AND 4 + REGEXP_LIKE(name, 'Love', 'c') + AND ((milliseconds / 1000) / 60) BETWEEN 3 AND 4 ORDER BY track_id LIMIT 15 ) SELECT name, composer FROM table_0 --- generic +++ postgres @@ -1,20 +1,20 @@ WITH table_0 AS ( SELECT name, composer, track_id FROM tracks WHERE - REGEXP(name, 'Love') - AND milliseconds / 1000 / 60 BETWEEN 3 AND 4 + name ~ 'Love' + AND ((milliseconds * 1.0 / 1000) * 1.0 / 60) BETWEEN 3 AND 4 ORDER BY track_id LIMIT 15 ) SELECT name, composer FROM table_0 --- generic +++ redshift @@ -1,20 +1,20 @@ WITH table_0 AS ( SELECT name, composer, track_id FROM tracks WHERE REGEXP(name, 'Love') - AND milliseconds / 1000 / 60 BETWEEN 3 AND 4 + AND ((milliseconds * 1.0 / 1000) * 1.0 / 60) BETWEEN 3 AND 4 ORDER BY track_id LIMIT 15 ) SELECT name, composer FROM table_0 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__read_csv.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# sqlite:skip\n# postgres:skip\n# mysql:skip\nfrom (read_csv \"data_file_root/media_types.csv\")\nappend (read_json \"data_file_root/media_types.json\")\nsort media_type_id\n" input_file: prqlc/prqlc/tests/integration/queries/read_csv.prql --- --- generic +++ clickhouse @@ -1,24 +1,24 @@ WITH table_0 AS ( SELECT * FROM - read_csv('data_file_root/media_types.csv') + file('data_file_root/media_types.csv', 'CSV') ), table_2 AS ( SELECT * FROM table_0 UNION ALL SELECT * FROM - read_json('data_file_root/media_types.json') + file('data_file_root/media_types.json', 'Json') ) SELECT * FROM table_2 ORDER BY media_type_id --- generic +++ duckdb @@ -1,24 +1,24 @@ WITH table_0 AS ( SELECT * FROM - read_csv('data_file_root/media_types.csv') + read_csv_auto('data_file_root/media_types.csv') ), table_2 AS ( SELECT * FROM table_0 UNION ALL SELECT * FROM - read_json('data_file_root/media_types.json') + read_json_auto('data_file_root/media_types.json') ) SELECT * FROM table_2 ORDER BY media_type_id --- generic +++ glaredb @@ -1,15 +1,15 @@ WITH table_0 AS ( SELECT * FROM - read_csv('data_file_root/media_types.csv') + csv_scan('data_file_root/media_types.csv') ), table_2 AS ( SELECT * FROM table_0 UNION ALL SELECT * ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__set_ops_remove.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nlet distinct = rel -> (from t = _param.rel | group {t.*} (take 1))\n\nfrom_text format:json '{ \"columns\": [\"a\"], \"data\": [[1], [2], [2], [3]] }'\ndistinct\nremove (from_text format:json '{ \"columns\": [\"a\"], \"data\": [[1], [2]] }')\nsort a\n" input_file: prqlc/prqlc/tests/integration/queries/set_ops_remove.prql snapshot_kind: text --- --- generic +++ mssql @@ -21,21 +21,20 @@ ALL SELECT 2 AS a ), table_2 AS ( SELECT a FROM table_0 EXCEPT - DISTINCT SELECT * FROM table_1 ) SELECT a FROM table_2 ORDER BY --- generic +++ sqlite @@ -21,21 +21,20 @@ ALL SELECT 2 AS a ), table_2 AS ( SELECT a FROM table_0 EXCEPT - DISTINCT SELECT * FROM table_1 ) SELECT a FROM table_2 ORDER BY ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__sort.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom e=employees\nfilter first_name != \"Mitchell\"\nsort {first_name, last_name}\n\n# joining may use HashMerge, which can undo ORDER BY\njoin manager=employees side:left (e.reports_to == manager.employee_id)\n\nselect {e.first_name, e.last_name, manager.first_name}\n" input_file: prqlc/prqlc/tests/integration/queries/sort.prql --- ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__sort_2.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from albums\nselect { AA=album_id, artist_id }\nsort AA\nfilter AA >= 25\njoin artists (==artist_id)\n" input_file: prqlc/prqlc/tests/integration/queries/sort_2.prql --- --- generic +++ clickhouse @@ -1,25 +1,25 @@ WITH table_1 AS ( SELECT - album_id AS "AA", + album_id AS `AA`, artist_id FROM albums ), table_0 AS ( SELECT - "AA", + `AA`, artist_id FROM table_1 WHERE - "AA" >= 25 + `AA` >= 25 ) SELECT - table_0."AA", + table_0.`AA`, table_0.artist_id, artists.* FROM table_0 INNER JOIN artists ON table_0.artist_id = artists.artist_id ORDER BY - table_0."AA" + table_0.`AA` --- generic +++ mysql @@ -1,25 +1,25 @@ WITH table_1 AS ( SELECT - album_id AS "AA", + album_id AS `AA`, artist_id FROM albums ), table_0 AS ( SELECT - "AA", + `AA`, artist_id FROM table_1 WHERE - "AA" >= 25 + `AA` >= 25 ) SELECT - table_0."AA", + table_0.`AA`, table_0.artist_id, artists.* FROM table_0 INNER JOIN artists ON table_0.artist_id = artists.artist_id ORDER BY - table_0."AA" + table_0.`AA` ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__sort_3.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from [{track_id=0, album_id=1, genre_id=2}]\nselect { AA=track_id, album_id, genre_id }\nsort AA\njoin side:left [{album_id=1, album_title=\"Songs\"}] (==album_id)\nselect { AA, AT = album_title ?? \"unknown\", genre_id }\nfilter AA < 25\njoin side:left [{genre_id=1, genre_title=\"Rock\"}] (==genre_id)\nselect { AA, AT, GT = genre_title ?? \"unknown\" }\n" input_file: prqlc/prqlc/tests/integration/queries/sort_3.prql --- --- generic +++ clickhouse @@ -1,52 +1,52 @@ WITH table_0 AS ( SELECT 0 AS track_id, 1 AS album_id, 2 AS genre_id ), table_5 AS ( SELECT - track_id AS "AA", + track_id AS `AA`, genre_id, album_id FROM table_0 ), table_1 AS ( SELECT 1 AS album_id, 'Songs' AS album_title ), table_4 AS ( SELECT - table_5."AA", - COALESCE(table_1.album_title, 'unknown') AS "AT", + table_5.`AA`, + COALESCE(table_1.album_title, 'unknown') AS `AT`, table_5.genre_id FROM table_5 LEFT OUTER JOIN table_1 ON table_5.album_id = table_1.album_id ), table_3 AS ( SELECT - "AA", - "AT", + `AA`, + `AT`, genre_id FROM table_4 WHERE - "AA" < 25 + `AA` < 25 ), table_2 AS ( SELECT 1 AS genre_id, 'Rock' AS genre_title ) SELECT - table_3."AA", - table_3."AT", - COALESCE(table_2.genre_title, 'unknown') AS "GT" + table_3.`AA`, + table_3.`AT`, + COALESCE(table_2.genre_title, 'unknown') AS `GT` FROM table_3 LEFT OUTER JOIN table_2 ON table_3.genre_id = table_2.genre_id ORDER BY - table_3."AA" + table_3.`AA` --- generic +++ mysql @@ -1,52 +1,52 @@ WITH table_0 AS ( SELECT 0 AS track_id, 1 AS album_id, 2 AS genre_id ), table_5 AS ( SELECT - track_id AS "AA", + track_id AS `AA`, genre_id, album_id FROM table_0 ), table_1 AS ( SELECT 1 AS album_id, 'Songs' AS album_title ), table_4 AS ( SELECT - table_5."AA", - COALESCE(table_1.album_title, 'unknown') AS "AT", + table_5.`AA`, + COALESCE(table_1.album_title, 'unknown') AS `AT`, table_5.genre_id FROM table_5 LEFT OUTER JOIN table_1 ON table_5.album_id = table_1.album_id ), table_3 AS ( SELECT - "AA", - "AT", + `AA`, + `AT`, genre_id FROM table_4 WHERE - "AA" < 25 + `AA` < 25 ), table_2 AS ( SELECT 1 AS genre_id, 'Rock' AS genre_title ) SELECT - table_3."AA", - table_3."AT", - COALESCE(table_2.genre_title, 'unknown') AS "GT" + table_3.`AA`, + table_3.`AT`, + COALESCE(table_2.genre_title, 'unknown') AS `GT` FROM table_3 LEFT OUTER JOIN table_2 ON table_3.genre_id = table_2.genre_id ORDER BY - table_3."AA" + table_3.`AA` ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__switch.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# glaredb:skip (May be a bag of String type conversion for Postgres Client)\n# mssql:test\nfrom tracks\nsort milliseconds\nselect display = case [\n composer != null => composer,\n genre_id < 17 => 'no composer',\n true => f'unknown composer'\n]\ntake 10\n" input_file: prqlc/prqlc/tests/integration/queries/switch.prql --- --- generic +++ mssql @@ -2,20 +2,20 @@ SELECT CASE WHEN composer IS NOT NULL THEN composer WHEN genre_id < 17 THEN 'no composer' ELSE 'unknown composer' END AS display, milliseconds FROM tracks ORDER BY - milliseconds - LIMIT - 10 + milliseconds OFFSET 0 ROWS + FETCH FIRST + 10 ROWS ONLY ) SELECT display FROM table_0 ORDER BY milliseconds ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__take.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nsort {+track_id}\ntake 3..5\n" input_file: prqlc/prqlc/tests/integration/queries/take.prql --- --- generic +++ mssql @@ -1,8 +1,8 @@ SELECT * FROM tracks ORDER BY - track_id -LIMIT - 3 OFFSET 2 + track_id OFFSET 2 ROWS +FETCH FIRST + 3 ROWS ONLY ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__text_module.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\n# glaredb:skip — TODO: started raising an error on 2024-05-20; see `window.prql`\n# for more details\nfrom albums\nselect {\n title,\n title_and_spaces = f\" {title} \",\n low = (title | text.lower),\n up = (title | text.upper),\n ltrimmed = (title | text.ltrim),\n rtrimmed = (title | text.rtrim),\n trimmed = (title | text.trim),\n len = (title | text.length),\n subs = (title | text.extract 2 5),\n replace = (title | text.replace \"al\" \"PIKA\"),\n}\nsort {title}\nfilter (title | text.starts_with \"Black\") || (title | text.contains \"Sabbath\") || (title | text.ends_with \"os\")\n" input_file: prqlc/prqlc/tests/integration/queries/text_module.prql --- --- generic +++ clickhouse @@ -2,33 +2,33 @@ SELECT title, CONCAT(' ', title, ' ') AS title_and_spaces, LOWER(title) AS low, UPPER(title) AS up, LTRIM(title) AS ltrimmed, RTRIM(title) AS rtrimmed, TRIM(title) AS trimmed, CHAR_LENGTH(title) AS len, SUBSTRING(title, 2, 5) AS subs, - REPLACE(title, 'al', 'PIKA') AS "replace" + REPLACE(title, 'al', 'PIKA') AS `replace` FROM albums ) SELECT title, title_and_spaces, low, up, ltrimmed, rtrimmed, trimmed, len, subs, - "replace" + `replace` FROM table_0 WHERE title LIKE CONCAT('Black', '%') OR title LIKE CONCAT('%', 'Sabbath', '%') OR title LIKE CONCAT('%', 'os') ORDER BY title --- generic +++ duckdb @@ -1,20 +1,20 @@ WITH table_0 AS ( SELECT title, CONCAT(' ', title, ' ') AS title_and_spaces, LOWER(title) AS low, UPPER(title) AS up, LTRIM(title) AS ltrimmed, RTRIM(title) AS rtrimmed, TRIM(title) AS trimmed, - CHAR_LENGTH(title) AS len, + LENGTH(title) AS len, SUBSTRING(title, 2, 5) AS subs, REPLACE(title, 'al', 'PIKA') AS "replace" FROM albums ) SELECT title, title_and_spaces, low, up, --- generic +++ mssql @@ -1,20 +1,20 @@ WITH table_0 AS ( SELECT title, CONCAT(' ', title, ' ') AS title_and_spaces, LOWER(title) AS low, UPPER(title) AS up, LTRIM(title) AS ltrimmed, RTRIM(title) AS rtrimmed, TRIM(title) AS trimmed, - CHAR_LENGTH(title) AS len, + LEN(title) AS len, SUBSTRING(title, 2, 5) AS subs, REPLACE(title, 'al', 'PIKA') AS "replace" FROM albums ) SELECT title, title_and_spaces, low, up, --- generic +++ mysql @@ -2,33 +2,33 @@ SELECT title, CONCAT(' ', title, ' ') AS title_and_spaces, LOWER(title) AS low, UPPER(title) AS up, LTRIM(title) AS ltrimmed, RTRIM(title) AS rtrimmed, TRIM(title) AS trimmed, CHAR_LENGTH(title) AS len, SUBSTRING(title, 2, 5) AS subs, - REPLACE(title, 'al', 'PIKA') AS "replace" + REPLACE(title, 'al', 'PIKA') AS `replace` FROM albums ) SELECT title, title_and_spaces, low, up, ltrimmed, rtrimmed, trimmed, len, subs, - "replace" + `replace` FROM table_0 WHERE title LIKE CONCAT('Black', '%') OR title LIKE CONCAT('%', 'Sabbath', '%') OR title LIKE CONCAT('%', 'os') ORDER BY title --- generic +++ postgres @@ -1,21 +1,21 @@ WITH table_0 AS ( SELECT title, CONCAT(' ', title, ' ') AS title_and_spaces, LOWER(title) AS low, UPPER(title) AS up, LTRIM(title) AS ltrimmed, RTRIM(title) AS rtrimmed, TRIM(title) AS trimmed, CHAR_LENGTH(title) AS len, - SUBSTRING(title, 2, 5) AS subs, + SUBSTR(title, 2, 5) AS subs, REPLACE(title, 'al', 'PIKA') AS "replace" FROM albums ) SELECT title, title_and_spaces, low, up, ltrimmed, --- generic +++ redshift @@ -1,14 +1,14 @@ WITH table_0 AS ( SELECT title, - CONCAT(' ', title, ' ') AS title_and_spaces, + ' ' || title || ' ' AS title_and_spaces, LOWER(title) AS low, UPPER(title) AS up, LTRIM(title) AS ltrimmed, RTRIM(title) AS rtrimmed, TRIM(title) AS trimmed, CHAR_LENGTH(title) AS len, SUBSTRING(title, 2, 5) AS subs, REPLACE(title, 'al', 'PIKA') AS "replace" FROM albums @@ -21,14 +21,14 @@ ltrimmed, rtrimmed, trimmed, len, subs, "replace" FROM table_0 WHERE title LIKE CONCAT('Black', '%') - OR title LIKE CONCAT('%', 'Sabbath', '%') + OR title LIKE '%' || 'Sabbath' || '%' OR title LIKE CONCAT('%', 'os') ORDER BY title --- generic +++ sqlite @@ -1,34 +1,34 @@ WITH table_0 AS ( SELECT title, - CONCAT(' ', title, ' ') AS title_and_spaces, + ' ' || title || ' ' AS title_and_spaces, LOWER(title) AS low, UPPER(title) AS up, LTRIM(title) AS ltrimmed, RTRIM(title) AS rtrimmed, TRIM(title) AS trimmed, - CHAR_LENGTH(title) AS len, + LENGTH(title) AS len, SUBSTRING(title, 2, 5) AS subs, REPLACE(title, 'al', 'PIKA') AS "replace" FROM albums ) SELECT title, title_and_spaces, low, up, ltrimmed, rtrimmed, trimmed, len, subs, "replace" FROM table_0 WHERE - title LIKE CONCAT('Black', '%') - OR title LIKE CONCAT('%', 'Sabbath', '%') - OR title LIKE CONCAT('%', 'os') + title LIKE 'Black' || '%' + OR title LIKE '%' || 'Sabbath' || '%' + OR title LIKE '%' || 'os' ORDER BY title ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__compileall__window.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip problems with DISTINCT ON\n# glaredb:skip — TODO: started raising an error on 2024-05-20, from https://github.com/PRQL/prql/actions/runs/9154902656/job/25198160283:\n # ERROR: This feature is not implemented: Unsupported ast node in sqltorel:\n # Substring { expr: Identifier(Ident { value: \"title\", quote_style: None }),\n # substring_from: Some(Value(Number(\"2\", false))), substring_for:\n # Some(Value(Number(\"5\", false))), special: true }\nfrom tracks\ngroup genre_id (\n sort milliseconds\n derive {\n num = row_number this,\n total = count this,\n last_val = last track_id,\n }\n take 10\n)\nsort {genre_id, milliseconds}\nselect {track_id, genre_id, num, total, last_val}\nfilter genre_id >= 22\n" input_file: prqlc/prqlc/tests/integration/queries/window.prql --- ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__aggregation.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mysql:skip\n# clickhouse:skip\n# glaredb:skip (the string_agg function is not supported)\nfrom tracks\nfilter genre_id == 100\nderive empty_name = name == ''\naggregate {sum track_id, concat_array name, all empty_name, any empty_name}\n" input_file: prqlc/prqlc/tests/integration/queries/aggregation.prql --- frames: - - 1:101-123 - columns: - !All input_id: 122 except: [] inputs: - id: 122 name: tracks table: - default_db - tracks - - 1:124-154 - columns: - !All input_id: 122 except: [] - !Single name: - empty_name target_id: 129 target_name: null inputs: - id: 122 name: tracks table: - default_db - tracks - - 1:155-230 - columns: - !Single name: null target_id: 135 target_name: null - !Single name: null target_id: 138 target_name: null - !Single name: null target_id: 141 target_name: null - !Single name: null target_id: 144 target_name: null inputs: - id: 122 name: tracks table: - default_db - tracks nodes: - id: 122 kind: Ident span: 1:89-100 ident: !Ident - default_db - tracks parent: 128 - id: 124 kind: RqOperator span: 1:108-123 targets: - 126 - 127 parent: 128 - id: 126 kind: Ident span: 1:108-116 ident: !Ident - this - tracks - genre_id targets: - 122 - id: 127 kind: Literal span: 1:120-123 - id: 128 kind: 'TransformCall: Filter' span: 1:101-123 children: - 122 - 124 parent: 134 - id: 129 kind: RqOperator span: 1:144-154 alias: empty_name targets: - 131 - 132 parent: 133 - id: 131 kind: Ident span: 1:144-148 ident: !Ident - this - tracks - name targets: - 122 - id: 132 kind: Literal span: 1:152-154 - id: 133 kind: Tuple span: 1:144-154 children: - 129 parent: 134 - id: 134 kind: 'TransformCall: Derive' span: 1:124-154 children: - 128 - 133 parent: 148 - id: 135 kind: RqOperator span: 1:166-178 targets: - 137 parent: 147 - id: 137 kind: Ident span: 1:170-178 ident: !Ident - this - tracks - track_id targets: - 122 - id: 138 kind: RqOperator span: 1:180-197 targets: - 140 parent: 147 - id: 140 kind: Ident span: 1:193-197 ident: !Ident - this - tracks - name targets: - 122 - id: 141 kind: RqOperator span: 1:199-213 targets: - 143 parent: 147 - id: 143 kind: Ident span: 1:203-213 ident: !Ident - this - empty_name targets: - 129 - id: 144 kind: RqOperator span: 1:215-229 targets: - 146 parent: 147 - id: 146 kind: Ident span: 1:219-229 ident: !Ident - this - empty_name targets: - 129 - id: 147 kind: Tuple span: 1:165-230 children: - 135 - 138 - 141 - 144 parent: 148 - id: 148 kind: 'TransformCall: Aggregate' span: 1:155-230 children: - 134 - 147 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:89-93 args: - Ident: - tracks span: 1:94-100 span: 1:89-100 - FuncCall: name: Ident: - filter span: 1:101-107 args: - Binary: left: Ident: - genre_id span: 1:108-116 op: Eq right: Literal: Integer: 100 span: 1:120-123 span: 1:108-123 span: 1:101-123 - FuncCall: name: Ident: - derive span: 1:124-130 args: - Binary: left: Ident: - name span: 1:144-148 op: Eq right: Literal: String: '' span: 1:152-154 span: 1:144-154 alias: empty_name span: 1:124-154 - FuncCall: name: Ident: - aggregate span: 1:155-164 args: - Tuple: - FuncCall: name: Ident: - sum span: 1:166-169 args: - Ident: - track_id span: 1:170-178 span: 1:166-178 - FuncCall: name: Ident: - concat_array span: 1:180-192 args: - Ident: - name span: 1:193-197 span: 1:180-197 - FuncCall: name: Ident: - all span: 1:199-202 args: - Ident: - empty_name span: 1:203-213 span: 1:199-213 - FuncCall: name: Ident: - any span: 1:215-218 args: - Ident: - empty_name span: 1:219-229 span: 1:215-229 span: 1:165-230 span: 1:155-230 span: 1:89-230 span: 1:0-230 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__append_select.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nselect { customer_id, invoice_id, billing_country }\ntake 10..15\nappend (\n from invoices\n select { customer_id, invoice_id, billing_country }\n take 40..45\n)\nselect { billing_country, invoice_id }\n" input_file: prqlc/prqlc/tests/integration/queries/append_select.prql --- frames: - - 1:14-65 - columns: - !Single name: - invoices - customer_id target_id: 146 target_name: null - !Single name: - invoices - invoice_id target_id: 147 target_name: null - !Single name: - invoices - billing_country target_id: 148 target_name: null inputs: - id: 144 name: invoices table: - default_db - invoices - - 1:66-77 - columns: - !Single name: - invoices - customer_id target_id: 146 target_name: null - !Single name: - invoices - invoice_id target_id: 147 target_name: null - !Single name: - invoices - billing_country target_id: 148 target_name: null inputs: - id: 144 name: invoices table: - default_db - invoices - - 1:105-156 - columns: - !Single name: - invoices - customer_id target_id: 127 target_name: null - !Single name: - invoices - invoice_id target_id: 128 target_name: null - !Single name: - invoices - billing_country target_id: 129 target_name: null inputs: - id: 125 name: invoices table: - default_db - invoices - - 1:159-170 - columns: - !Single name: - invoices - customer_id target_id: 127 target_name: null - !Single name: - invoices - invoice_id target_id: 128 target_name: null - !Single name: - invoices - billing_country target_id: 129 target_name: null inputs: - id: 125 name: invoices table: - default_db - invoices - - 1:78-172 - columns: - !Single name: - invoices - customer_id target_id: 146 target_name: null - !Single name: - invoices - invoice_id target_id: 147 target_name: null - !Single name: - invoices - billing_country target_id: 148 target_name: null inputs: - id: 144 name: invoices table: - default_db - invoices - id: 125 name: invoices table: - default_db - invoices - - 1:173-211 - columns: - !Single name: - invoices - billing_country target_id: 156 target_name: null - !Single name: - invoices - invoice_id target_id: 157 target_name: null inputs: - id: 144 name: invoices table: - default_db - invoices - id: 125 name: invoices table: - default_db - invoices nodes: - id: 125 kind: Ident span: 1:89-102 ident: !Ident - default_db - invoices parent: 131 - id: 127 kind: Ident span: 1:114-125 ident: !Ident - this - invoices - customer_id targets: - 125 parent: 130 - id: 128 kind: Ident span: 1:127-137 ident: !Ident - this - invoices - invoice_id targets: - 125 parent: 130 - id: 129 kind: Ident span: 1:139-154 ident: !Ident - this - invoices - billing_country targets: - 125 parent: 130 - id: 130 kind: Tuple span: 1:112-156 children: - 127 - 128 - 129 parent: 131 - id: 131 kind: 'TransformCall: Select' span: 1:105-156 children: - 125 - 130 parent: 135 - id: 132 kind: Literal span: 1:164-166 alias: start parent: 135 - id: 133 kind: Literal span: 1:168-170 alias: end parent: 135 - id: 135 kind: 'TransformCall: Take' span: 1:159-170 children: - 131 - 132 - 133 parent: 155 - id: 144 kind: Ident span: 1:0-13 ident: !Ident - default_db - invoices parent: 150 - id: 146 kind: Ident span: 1:23-34 ident: !Ident - this - invoices - customer_id targets: - 144 parent: 149 - id: 147 kind: Ident span: 1:36-46 ident: !Ident - this - invoices - invoice_id targets: - 144 parent: 149 - id: 148 kind: Ident span: 1:48-63 ident: !Ident - this - invoices - billing_country targets: - 144 parent: 149 - id: 149 kind: Tuple span: 1:21-65 children: - 146 - 147 - 148 parent: 150 - id: 150 kind: 'TransformCall: Select' span: 1:14-65 children: - 144 - 149 parent: 154 - id: 151 kind: Literal span: 1:71-73 alias: start parent: 154 - id: 152 kind: Literal span: 1:75-77 alias: end parent: 154 - id: 154 kind: 'TransformCall: Take' span: 1:66-77 children: - 150 - 151 - 152 parent: 155 - id: 155 kind: 'TransformCall: Append' span: 1:78-172 children: - 154 - 135 parent: 159 - id: 156 kind: Ident span: 1:182-197 ident: !Ident - this - invoices - billing_country targets: - 148 parent: 158 - id: 157 kind: Ident span: 1:199-209 ident: !Ident - this - invoices - invoice_id targets: - 147 parent: 158 - id: 158 kind: Tuple span: 1:180-211 children: - 156 - 157 parent: 159 - id: 159 kind: 'TransformCall: Select' span: 1:173-211 children: - 155 - 158 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:0-4 args: - Ident: - invoices span: 1:5-13 span: 1:0-13 - FuncCall: name: Ident: - select span: 1:14-20 args: - Tuple: - Ident: - customer_id span: 1:23-34 - Ident: - invoice_id span: 1:36-46 - Ident: - billing_country span: 1:48-63 span: 1:21-65 span: 1:14-65 - FuncCall: name: Ident: - take span: 1:66-70 args: - Range: start: Literal: Integer: 10 span: 1:71-73 end: Literal: Integer: 15 span: 1:75-77 span: 1:71-77 span: 1:66-77 - FuncCall: name: Ident: - append span: 1:78-84 args: - Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:89-93 args: - Ident: - invoices span: 1:94-102 span: 1:89-102 - FuncCall: name: Ident: - select span: 1:105-111 args: - Tuple: - Ident: - customer_id span: 1:114-125 - Ident: - invoice_id span: 1:127-137 - Ident: - billing_country span: 1:139-154 span: 1:112-156 span: 1:105-156 - FuncCall: name: Ident: - take span: 1:159-163 args: - Range: start: Literal: Integer: 40 span: 1:164-166 end: Literal: Integer: 45 span: 1:168-170 span: 1:164-170 span: 1:159-170 span: 1:89-170 span: 1:78-172 - FuncCall: name: Ident: - select span: 1:173-179 args: - Tuple: - Ident: - billing_country span: 1:182-197 - Ident: - invoice_id span: 1:199-209 span: 1:180-211 span: 1:173-211 span: 1:0-211 span: 1:0-211 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__append_select_compute.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nderive total = case [total < 10 => total * 2, true => total]\nselect { customer_id, invoice_id, total }\ntake 5\nappend (\n from invoice_items\n derive unit_price = case [unit_price < 1 => unit_price * 2, true => unit_price]\n select { invoice_line_id, invoice_id, unit_price }\n take 5\n)\nselect { a = customer_id * 2, b = math.round 1 (invoice_id * total) }\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_compute.prql --- frames: - - 1:14-74 - columns: - !All input_id: 162 except: [] - !Single name: - total target_id: 164 target_name: null inputs: - id: 162 name: invoices table: - default_db - invoices - - 1:75-116 - columns: - !Single name: - invoices - customer_id target_id: 177 target_name: null - !Single name: - invoices - invoice_id target_id: 178 target_name: null - !Single name: - total target_id: 179 target_name: null inputs: - id: 162 name: invoices table: - default_db - invoices - - 1:117-123 - columns: - !Single name: - invoices - customer_id target_id: 177 target_name: null - !Single name: - invoices - invoice_id target_id: 178 target_name: null - !Single name: - total target_id: 179 target_name: null inputs: - id: 162 name: invoices table: - default_db - invoices - - 1:156-235 - columns: - !All input_id: 128 except: [] - !Single name: - unit_price target_id: 130 target_name: null inputs: - id: 128 name: invoice_items table: - default_db - invoice_items - - 1:238-288 - columns: - !Single name: - invoice_items - invoice_line_id target_id: 143 target_name: null - !Single name: - invoice_items - invoice_id target_id: 144 target_name: null - !Single name: - unit_price target_id: 145 target_name: null inputs: - id: 128 name: invoice_items table: - default_db - invoice_items - - 1:291-297 - columns: - !Single name: - invoice_items - invoice_line_id target_id: 143 target_name: null - !Single name: - invoice_items - invoice_id target_id: 144 target_name: null - !Single name: - unit_price target_id: 145 target_name: null inputs: - id: 128 name: invoice_items table: - default_db - invoice_items - - 1:124-299 - columns: - !Single name: - invoices - customer_id target_id: 177 target_name: null - !Single name: - invoices - invoice_id target_id: 178 target_name: null - !Single name: - total target_id: 179 target_name: null inputs: - id: 162 name: invoices table: - default_db - invoices - id: 128 name: invoice_items table: - default_db - invoice_items - - 1:300-369 - columns: - !Single name: - a target_id: 186 target_name: null - !Single name: - b target_id: 190 target_name: null inputs: - id: 162 name: invoices table: - default_db - invoices - id: 128 name: invoice_items table: - default_db - invoice_items nodes: - id: 128 kind: Ident span: 1:135-153 ident: !Ident - default_db - invoice_items parent: 142 - id: 130 kind: Case span: 1:176-235 alias: unit_price targets: - 131 - 135 - 139 - 140 parent: 141 - id: 131 kind: RqOperator span: 1:182-196 targets: - 133 - 134 - id: 133 kind: Ident span: 1:182-192 ident: !Ident - this - invoice_items - unit_price targets: - 128 - id: 134 kind: Literal span: 1:195-196 - id: 135 kind: RqOperator span: 1:200-214 targets: - 137 - 138 - id: 137 kind: Ident span: 1:200-210 ident: !Ident - this - invoice_items - unit_price targets: - 128 - id: 138 kind: Literal span: 1:213-214 - id: 139 kind: Literal span: 1:216-220 - id: 140 kind: Ident span: 1:224-234 ident: !Ident - this - invoice_items - unit_price targets: - 128 - id: 141 kind: Tuple span: 1:176-235 children: - 130 parent: 142 - id: 142 kind: 'TransformCall: Derive' span: 1:156-235 children: - 128 - 141 parent: 147 - id: 143 kind: Ident span: 1:247-262 ident: !Ident - this - invoice_items - invoice_line_id targets: - 128 parent: 146 - id: 144 kind: Ident span: 1:264-274 ident: !Ident - this - invoice_items - invoice_id targets: - 128 parent: 146 - id: 145 kind: Ident span: 1:276-286 ident: !Ident - this - unit_price targets: - 130 parent: 146 - id: 146 kind: Tuple span: 1:245-288 children: - 143 - 144 - 145 parent: 147 - id: 147 kind: 'TransformCall: Select' span: 1:238-288 children: - 142 - 146 parent: 149 - id: 149 kind: 'TransformCall: Take' span: 1:291-297 children: - 147 - 150 parent: 185 - id: 150 kind: Literal parent: 149 - id: 162 kind: Ident span: 1:0-13 ident: !Ident - default_db - invoices parent: 176 - id: 164 kind: Case span: 1:29-74 alias: total targets: - 165 - 169 - 173 - 174 parent: 175 - id: 165 kind: RqOperator span: 1:35-45 targets: - 167 - 168 - id: 167 kind: Ident span: 1:35-40 ident: !Ident - this - invoices - total targets: - 162 - id: 168 kind: Literal span: 1:43-45 - id: 169 kind: RqOperator span: 1:49-58 targets: - 171 - 172 - id: 171 kind: Ident span: 1:49-54 ident: !Ident - this - invoices - total targets: - 162 - id: 172 kind: Literal span: 1:57-58 - id: 173 kind: Literal span: 1:60-64 - id: 174 kind: Ident span: 1:68-73 ident: !Ident - this - invoices - total targets: - 162 - id: 175 kind: Tuple span: 1:29-74 children: - 164 parent: 176 - id: 176 kind: 'TransformCall: Derive' span: 1:14-74 children: - 162 - 175 parent: 181 - id: 177 kind: Ident span: 1:84-95 ident: !Ident - this - invoices - customer_id targets: - 162 parent: 180 - id: 178 kind: Ident span: 1:97-107 ident: !Ident - this - invoices - invoice_id targets: - 162 parent: 180 - id: 179 kind: Ident span: 1:109-114 ident: !Ident - this - total targets: - 164 parent: 180 - id: 180 kind: Tuple span: 1:82-116 children: - 177 - 178 - 179 parent: 181 - id: 181 kind: 'TransformCall: Select' span: 1:75-116 children: - 176 - 180 parent: 183 - id: 183 kind: 'TransformCall: Take' span: 1:117-123 children: - 181 - 184 parent: 185 - id: 184 kind: Literal parent: 183 - id: 185 kind: 'TransformCall: Append' span: 1:124-299 children: - 183 - 149 parent: 198 - id: 186 kind: RqOperator span: 1:313-328 alias: a targets: - 188 - 189 parent: 197 - id: 188 kind: Ident span: 1:313-324 ident: !Ident - this - invoices - customer_id targets: - 177 - id: 189 kind: Literal span: 1:327-328 - id: 190 kind: RqOperator span: 1:334-367 alias: b targets: - 192 - 193 parent: 197 - id: 192 kind: Literal span: 1:345-346 - id: 193 kind: RqOperator span: 1:348-366 targets: - 195 - 196 - id: 195 kind: Ident span: 1:348-358 ident: !Ident - this - invoices - invoice_id targets: - 178 - id: 196 kind: Ident span: 1:361-366 ident: !Ident - this - total targets: - 179 - id: 197 kind: Tuple span: 1:307-369 children: - 186 - 190 parent: 198 - id: 198 kind: 'TransformCall: Select' span: 1:300-369 children: - 185 - 197 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:0-4 args: - Ident: - invoices span: 1:5-13 span: 1:0-13 - FuncCall: name: Ident: - derive span: 1:14-20 args: - Case: - condition: Binary: left: Ident: - total span: 1:35-40 op: Lt right: Literal: Integer: 10 span: 1:43-45 span: 1:35-45 value: Binary: left: Ident: - total span: 1:49-54 op: Mul right: Literal: Integer: 2 span: 1:57-58 span: 1:49-58 - condition: Literal: Boolean: true span: 1:60-64 value: Ident: - total span: 1:68-73 span: 1:29-74 alias: total span: 1:14-74 - FuncCall: name: Ident: - select span: 1:75-81 args: - Tuple: - Ident: - customer_id span: 1:84-95 - Ident: - invoice_id span: 1:97-107 - Ident: - total span: 1:109-114 span: 1:82-116 span: 1:75-116 - FuncCall: name: Ident: - take span: 1:117-121 args: - Literal: Integer: 5 span: 1:122-123 span: 1:117-123 - FuncCall: name: Ident: - append span: 1:124-130 args: - Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:135-139 args: - Ident: - invoice_items span: 1:140-153 span: 1:135-153 - FuncCall: name: Ident: - derive span: 1:156-162 args: - Case: - condition: Binary: left: Ident: - unit_price span: 1:182-192 op: Lt right: Literal: Integer: 1 span: 1:195-196 span: 1:182-196 value: Binary: left: Ident: - unit_price span: 1:200-210 op: Mul right: Literal: Integer: 2 span: 1:213-214 span: 1:200-214 - condition: Literal: Boolean: true span: 1:216-220 value: Ident: - unit_price span: 1:224-234 span: 1:176-235 alias: unit_price span: 1:156-235 - FuncCall: name: Ident: - select span: 1:238-244 args: - Tuple: - Ident: - invoice_line_id span: 1:247-262 - Ident: - invoice_id span: 1:264-274 - Ident: - unit_price span: 1:276-286 span: 1:245-288 span: 1:238-288 - FuncCall: name: Ident: - take span: 1:291-295 args: - Literal: Integer: 5 span: 1:296-297 span: 1:291-297 span: 1:135-297 span: 1:124-299 - FuncCall: name: Ident: - select span: 1:300-306 args: - Tuple: - Binary: left: Ident: - customer_id span: 1:313-324 op: Mul right: Literal: Integer: 2 span: 1:327-328 span: 1:313-328 alias: a - FuncCall: name: Ident: - math - round span: 1:334-344 args: - Literal: Integer: 1 span: 1:345-346 - Binary: left: Ident: - invoice_id span: 1:348-358 op: Mul right: Ident: - total span: 1:361-366 span: 1:348-366 span: 1:334-367 alias: b span: 1:307-369 span: 1:300-369 span: 1:0-369 span: 1:0-369 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__append_select_multiple_with_null.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nselect { customer_id, invoice_id, billing_country }\ntake 5\nappend (\n from employees\n select { employee_id, employee_id, country }\n take 5\n)\nappend (\n from invoice_items\n select { invoice_line_id, invoice_id, null }\n take 5\n)\nselect { billing_country, invoice_id }\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_multiple_with_null.prql --- frames: - - 1:14-65 - columns: - !Single name: - invoices - customer_id target_id: 166 target_name: null - !Single name: - invoices - invoice_id target_id: 167 target_name: null - !Single name: - invoices - billing_country target_id: 168 target_name: null inputs: - id: 164 name: invoices table: - default_db - invoices - - 1:66-72 - columns: - !Single name: - invoices - customer_id target_id: 166 target_name: null - !Single name: - invoices - invoice_id target_id: 167 target_name: null - !Single name: - invoices - billing_country target_id: 168 target_name: null inputs: - id: 164 name: invoices table: - default_db - invoices - - 1:101-145 - columns: - !Single name: null target_id: 148 target_name: null - !Single name: - employees - employee_id target_id: 149 target_name: null - !Single name: - employees - country target_id: 150 target_name: null inputs: - id: 146 name: employees table: - default_db - employees - - 1:148-154 - columns: - !Single name: null target_id: 148 target_name: null - !Single name: - employees - employee_id target_id: 149 target_name: null - !Single name: - employees - country target_id: 150 target_name: null inputs: - id: 146 name: employees table: - default_db - employees - - 1:73-156 - columns: - !Single name: - invoices - customer_id target_id: 166 target_name: null - !Single name: - invoices - invoice_id target_id: 167 target_name: null - !Single name: - invoices - billing_country target_id: 168 target_name: null inputs: - id: 164 name: invoices table: - default_db - invoices - id: 146 name: employees table: - default_db - employees - - 1:189-233 - columns: - !Single name: - invoice_items - invoice_line_id target_id: 127 target_name: null - !Single name: - invoice_items - invoice_id target_id: 128 target_name: null - !Single name: null target_id: 129 target_name: null inputs: - id: 125 name: invoice_items table: - default_db - invoice_items - - 1:236-242 - columns: - !Single name: - invoice_items - invoice_line_id target_id: 127 target_name: null - !Single name: - invoice_items - invoice_id target_id: 128 target_name: null - !Single name: null target_id: 129 target_name: null inputs: - id: 125 name: invoice_items table: - default_db - invoice_items - - 1:157-244 - columns: - !Single name: - invoices - customer_id target_id: 166 target_name: null - !Single name: - invoices - invoice_id target_id: 167 target_name: null - !Single name: - invoices - billing_country target_id: 168 target_name: null inputs: - id: 164 name: invoices table: - default_db - invoices - id: 146 name: employees table: - default_db - employees - id: 125 name: invoice_items table: - default_db - invoice_items - - 1:245-283 - columns: - !Single name: - invoices - billing_country target_id: 176 target_name: null - !Single name: - invoices - invoice_id target_id: 177 target_name: null inputs: - id: 164 name: invoices table: - default_db - invoices - id: 146 name: employees table: - default_db - employees - id: 125 name: invoice_items table: - default_db - invoice_items nodes: - id: 125 kind: Ident span: 1:168-186 ident: !Ident - default_db - invoice_items parent: 131 - id: 127 kind: Ident span: 1:198-213 ident: !Ident - this - invoice_items - invoice_line_id targets: - 125 parent: 130 - id: 128 kind: Ident span: 1:215-225 ident: !Ident - this - invoice_items - invoice_id targets: - 125 parent: 130 - id: 129 kind: Literal span: 1:227-231 parent: 130 - id: 130 kind: Tuple span: 1:196-233 children: - 127 - 128 - 129 parent: 131 - id: 131 kind: 'TransformCall: Select' span: 1:189-233 children: - 125 - 130 parent: 133 - id: 133 kind: 'TransformCall: Take' span: 1:236-242 children: - 131 - 134 parent: 175 - id: 134 kind: Literal parent: 133 - id: 146 kind: Ident span: 1:84-98 ident: !Ident - default_db - employees parent: 152 - id: 148 kind: Ident span: 1:110-121 ident: !Ident - this - employees - employee_id targets: - 146 parent: 151 - id: 149 kind: Ident span: 1:123-134 ident: !Ident - this - employees - employee_id targets: - 146 parent: 151 - id: 150 kind: Ident span: 1:136-143 ident: !Ident - this - employees - country targets: - 146 parent: 151 - id: 151 kind: Tuple span: 1:108-145 children: - 148 - 149 - 150 parent: 152 - id: 152 kind: 'TransformCall: Select' span: 1:101-145 children: - 146 - 151 parent: 154 - id: 154 kind: 'TransformCall: Take' span: 1:148-154 children: - 152 - 155 parent: 174 - id: 155 kind: Literal parent: 154 - id: 164 kind: Ident span: 1:0-13 ident: !Ident - default_db - invoices parent: 170 - id: 166 kind: Ident span: 1:23-34 ident: !Ident - this - invoices - customer_id targets: - 164 parent: 169 - id: 167 kind: Ident span: 1:36-46 ident: !Ident - this - invoices - invoice_id targets: - 164 parent: 169 - id: 168 kind: Ident span: 1:48-63 ident: !Ident - this - invoices - billing_country targets: - 164 parent: 169 - id: 169 kind: Tuple span: 1:21-65 children: - 166 - 167 - 168 parent: 170 - id: 170 kind: 'TransformCall: Select' span: 1:14-65 children: - 164 - 169 parent: 172 - id: 172 kind: 'TransformCall: Take' span: 1:66-72 children: - 170 - 173 parent: 174 - id: 173 kind: Literal parent: 172 - id: 174 kind: 'TransformCall: Append' span: 1:73-156 children: - 172 - 154 parent: 175 - id: 175 kind: 'TransformCall: Append' span: 1:157-244 children: - 174 - 133 parent: 179 - id: 176 kind: Ident span: 1:254-269 ident: !Ident - this - invoices - billing_country targets: - 168 parent: 178 - id: 177 kind: Ident span: 1:271-281 ident: !Ident - this - invoices - invoice_id targets: - 167 parent: 178 - id: 178 kind: Tuple span: 1:252-283 children: - 176 - 177 parent: 179 - id: 179 kind: 'TransformCall: Select' span: 1:245-283 children: - 175 - 178 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:0-4 args: - Ident: - invoices span: 1:5-13 span: 1:0-13 - FuncCall: name: Ident: - select span: 1:14-20 args: - Tuple: - Ident: - customer_id span: 1:23-34 - Ident: - invoice_id span: 1:36-46 - Ident: - billing_country span: 1:48-63 span: 1:21-65 span: 1:14-65 - FuncCall: name: Ident: - take span: 1:66-70 args: - Literal: Integer: 5 span: 1:71-72 span: 1:66-72 - FuncCall: name: Ident: - append span: 1:73-79 args: - Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:84-88 args: - Ident: - employees span: 1:89-98 span: 1:84-98 - FuncCall: name: Ident: - select span: 1:101-107 args: - Tuple: - Ident: - employee_id span: 1:110-121 - Ident: - employee_id span: 1:123-134 - Ident: - country span: 1:136-143 span: 1:108-145 span: 1:101-145 - FuncCall: name: Ident: - take span: 1:148-152 args: - Literal: Integer: 5 span: 1:153-154 span: 1:148-154 span: 1:84-154 span: 1:73-156 - FuncCall: name: Ident: - append span: 1:157-163 args: - Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:168-172 args: - Ident: - invoice_items span: 1:173-186 span: 1:168-186 - FuncCall: name: Ident: - select span: 1:189-195 args: - Tuple: - Ident: - invoice_line_id span: 1:198-213 - Ident: - invoice_id span: 1:215-225 - Literal: 'Null' span: 1:227-231 span: 1:196-233 span: 1:189-233 - FuncCall: name: Ident: - take span: 1:236-240 args: - Literal: Integer: 5 span: 1:241-242 span: 1:236-242 span: 1:168-242 span: 1:157-244 - FuncCall: name: Ident: - select span: 1:245-251 args: - Tuple: - Ident: - billing_country span: 1:254-269 - Ident: - invoice_id span: 1:271-281 span: 1:252-283 span: 1:245-283 span: 1:0-283 span: 1:0-283 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__append_select_nulls.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nselect {an_id = invoice_id, name = null}\ntake 2\nappend (\n from employees\n select {an_id = null, name = first_name}\n take 2\n)\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_nulls.prql --- frames: - - 1:14-54 - columns: - !Single name: - an_id target_id: 141 target_name: null - !Single name: - name target_id: 142 target_name: null inputs: - id: 139 name: invoices table: - default_db - invoices - - 1:55-61 - columns: - !Single name: - an_id target_id: 141 target_name: null - !Single name: - name target_id: 142 target_name: null inputs: - id: 139 name: invoices table: - default_db - invoices - - 1:90-130 - columns: - !Single name: - an_id target_id: 124 target_name: null - !Single name: - name target_id: 125 target_name: null inputs: - id: 122 name: employees table: - default_db - employees - - 1:133-139 - columns: - !Single name: - an_id target_id: 124 target_name: null - !Single name: - name target_id: 125 target_name: null inputs: - id: 122 name: employees table: - default_db - employees - - 1:62-141 - columns: - !Single name: - an_id target_id: 141 target_name: null - !Single name: - name target_id: 142 target_name: null inputs: - id: 139 name: invoices table: - default_db - invoices - id: 122 name: employees table: - default_db - employees nodes: - id: 122 kind: Ident span: 1:73-87 ident: !Ident - default_db - employees parent: 127 - id: 124 kind: Literal span: 1:106-110 alias: an_id parent: 126 - id: 125 kind: Ident span: 1:119-129 alias: name ident: !Ident - this - employees - first_name targets: - 122 parent: 126 - id: 126 kind: Tuple span: 1:97-130 children: - 124 - 125 parent: 127 - id: 127 kind: 'TransformCall: Select' span: 1:90-130 children: - 122 - 126 parent: 129 - id: 129 kind: 'TransformCall: Take' span: 1:133-139 children: - 127 - 130 parent: 148 - id: 130 kind: Literal parent: 129 - id: 139 kind: Ident span: 1:0-13 ident: !Ident - default_db - invoices parent: 144 - id: 141 kind: Ident span: 1:30-40 alias: an_id ident: !Ident - this - invoices - invoice_id targets: - 139 parent: 143 - id: 142 kind: Literal span: 1:49-53 alias: name parent: 143 - id: 143 kind: Tuple span: 1:21-54 children: - 141 - 142 parent: 144 - id: 144 kind: 'TransformCall: Select' span: 1:14-54 children: - 139 - 143 parent: 146 - id: 146 kind: 'TransformCall: Take' span: 1:55-61 children: - 144 - 147 parent: 148 - id: 147 kind: Literal parent: 146 - id: 148 kind: 'TransformCall: Append' span: 1:62-141 children: - 146 - 129 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:0-4 args: - Ident: - invoices span: 1:5-13 span: 1:0-13 - FuncCall: name: Ident: - select span: 1:14-20 args: - Tuple: - Ident: - invoice_id span: 1:30-40 alias: an_id - Literal: 'Null' span: 1:49-53 alias: name span: 1:21-54 span: 1:14-54 - FuncCall: name: Ident: - take span: 1:55-59 args: - Literal: Integer: 2 span: 1:60-61 span: 1:55-61 - FuncCall: name: Ident: - append span: 1:62-68 args: - Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:73-77 args: - Ident: - employees span: 1:78-87 span: 1:73-87 - FuncCall: name: Ident: - select span: 1:90-96 args: - Tuple: - Literal: 'Null' span: 1:106-110 alias: an_id - Ident: - first_name span: 1:119-129 alias: name span: 1:97-130 span: 1:90-130 - FuncCall: name: Ident: - take span: 1:133-137 args: - Literal: Integer: 2 span: 1:138-139 span: 1:133-139 span: 1:73-139 span: 1:62-141 span: 1:0-141 span: 1:0-141 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__append_select_simple.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nselect { invoice_id, billing_country }\nappend (\n from invoices\n select { invoice_id = `invoice_id` + 100, billing_country }\n)\nfilter (billing_country | text.starts_with(\"I\"))\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_simple.prql --- frames: - - 1:14-52 - columns: - !Single name: - invoices - invoice_id target_id: 138 target_name: null - !Single name: - invoices - billing_country target_id: 139 target_name: null inputs: - id: 136 name: invoices table: - default_db - invoices - - 1:80-139 - columns: - !Single name: - invoice_id target_id: 124 target_name: null - !Single name: - invoices - billing_country target_id: 128 target_name: null inputs: - id: 122 name: invoices table: - default_db - invoices - - 1:53-141 - columns: - !Single name: - invoices - invoice_id target_id: 138 target_name: null - !Single name: - invoices - billing_country target_id: 139 target_name: null inputs: - id: 136 name: invoices table: - default_db - invoices - id: 122 name: invoices table: - default_db - invoices - - 1:142-190 - columns: - !Single name: - invoices - invoice_id target_id: 138 target_name: null - !Single name: - invoices - billing_country target_id: 139 target_name: null inputs: - id: 136 name: invoices table: - default_db - invoices - id: 122 name: invoices table: - default_db - invoices nodes: - id: 122 kind: Ident span: 1:64-77 ident: !Ident - default_db - invoices parent: 130 - id: 124 kind: RqOperator span: 1:102-120 alias: invoice_id targets: - 126 - 127 parent: 129 - id: 126 kind: Ident span: 1:102-114 ident: !Ident - this - invoices - invoice_id targets: - 122 - id: 127 kind: Literal span: 1:117-120 - id: 128 kind: Ident span: 1:122-137 ident: !Ident - this - invoices - billing_country targets: - 122 parent: 129 - id: 129 kind: Tuple span: 1:87-139 children: - 124 - 128 parent: 130 - id: 130 kind: 'TransformCall: Select' span: 1:80-139 children: - 122 - 129 parent: 142 - id: 136 kind: Ident span: 1:0-13 ident: !Ident - default_db - invoices parent: 141 - id: 138 kind: Ident span: 1:23-33 ident: !Ident - this - invoices - invoice_id targets: - 136 parent: 140 - id: 139 kind: Ident span: 1:35-50 ident: !Ident - this - invoices - billing_country targets: - 136 parent: 140 - id: 140 kind: Tuple span: 1:21-52 children: - 138 - 139 parent: 141 - id: 141 kind: 'TransformCall: Select' span: 1:14-52 children: - 136 - 140 parent: 142 - id: 142 kind: 'TransformCall: Append' span: 1:53-141 children: - 141 - 130 parent: 148 - id: 143 kind: RqOperator span: 1:168-189 targets: - 146 - 147 parent: 148 - id: 146 kind: Literal span: 1:185-188 - id: 147 kind: Ident span: 1:150-165 ident: !Ident - this - invoices - billing_country targets: - 139 - id: 148 kind: 'TransformCall: Filter' span: 1:142-190 children: - 142 - 143 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:0-4 args: - Ident: - invoices span: 1:5-13 span: 1:0-13 - FuncCall: name: Ident: - select span: 1:14-20 args: - Tuple: - Ident: - invoice_id span: 1:23-33 - Ident: - billing_country span: 1:35-50 span: 1:21-52 span: 1:14-52 - FuncCall: name: Ident: - append span: 1:53-59 args: - Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:64-68 args: - Ident: - invoices span: 1:69-77 span: 1:64-77 - FuncCall: name: Ident: - select span: 1:80-86 args: - Tuple: - Binary: left: Ident: - invoice_id span: 1:102-114 op: Add right: Literal: Integer: 100 span: 1:117-120 span: 1:102-120 alias: invoice_id - Ident: - billing_country span: 1:122-137 span: 1:87-139 span: 1:80-139 span: 1:64-139 span: 1:53-141 - FuncCall: name: Ident: - filter span: 1:142-148 args: - Pipeline: exprs: - Ident: - billing_country span: 1:150-165 - FuncCall: name: Ident: - text - starts_with span: 1:168-184 args: - Literal: String: I span: 1:185-188 span: 1:168-189 span: 1:150-189 span: 1:142-190 span: 1:0-190 span: 1:0-190 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__arithmetic.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom [\n { id = 1, x_int = 13, x_float = 13.0, k_int = 5, k_float = 5.0 },\n { id = 2, x_int = -13, x_float = -13.0, k_int = 5, k_float = 5.0 },\n { id = 3, x_int = 13, x_float = 13.0, k_int = -5, k_float = -5.0 },\n { id = 4, x_int = -13, x_float = -13.0, k_int = -5, k_float = -5.0 },\n]\nselect {\n id,\n\n x_int / k_int,\n x_int / k_float,\n x_float / k_int,\n x_float / k_float,\n\n q_ii = x_int // k_int,\n q_if = x_int // k_float,\n q_fi = x_float // k_int,\n q_ff = x_float // k_float,\n\n r_ii = x_int % k_int,\n r_if = x_int % k_float,\n r_fi = x_float % k_int,\n r_ff = x_float % k_float,\n\n (q_ii * k_int + r_ii | math.round 0),\n (q_if * k_float + r_if | math.round 0),\n (q_fi * k_int + r_fi | math.round 0),\n (q_ff * k_float + r_ff | math.round 0),\n}\nsort id\n" input_file: prqlc/prqlc/tests/integration/queries/arithmetic.prql --- frames: - - 1:318-824 - columns: - !Single name: - _literal_119 - id target_id: 161 target_name: null - !Single name: null target_id: 162 target_name: null - !Single name: null target_id: 166 target_name: null - !Single name: null target_id: 170 target_name: null - !Single name: null target_id: 174 target_name: null - !Single name: - q_ii target_id: 178 target_name: null - !Single name: - q_if target_id: 182 target_name: null - !Single name: - q_fi target_id: 186 target_name: null - !Single name: - q_ff target_id: 190 target_name: null - !Single name: - r_ii target_id: 194 target_name: null - !Single name: - r_if target_id: 198 target_name: null - !Single name: - r_fi target_id: 202 target_name: null - !Single name: - r_ff target_id: 206 target_name: null - !Single name: null target_id: 210 target_name: null - !Single name: null target_id: 221 target_name: null - !Single name: null target_id: 232 target_name: null - !Single name: null target_id: 243 target_name: null inputs: - id: 119 name: _literal_119 table: - default_db - _literal_119 - - 1:825-832 - columns: - !Single name: - _literal_119 - id target_id: 161 target_name: null - !Single name: null target_id: 162 target_name: null - !Single name: null target_id: 166 target_name: null - !Single name: null target_id: 170 target_name: null - !Single name: null target_id: 174 target_name: null - !Single name: - q_ii target_id: 178 target_name: null - !Single name: - q_if target_id: 182 target_name: null - !Single name: - q_fi target_id: 186 target_name: null - !Single name: - q_ff target_id: 190 target_name: null - !Single name: - r_ii target_id: 194 target_name: null - !Single name: - r_if target_id: 198 target_name: null - !Single name: - r_fi target_id: 202 target_name: null - !Single name: - r_ff target_id: 206 target_name: null - !Single name: null target_id: 210 target_name: null - !Single name: null target_id: 221 target_name: null - !Single name: null target_id: 232 target_name: null - !Single name: null target_id: 243 target_name: null inputs: - id: 119 name: _literal_119 table: - default_db - _literal_119 nodes: - id: 119 kind: Array span: 1:13-317 children: - 120 - 126 - 136 - 146 parent: 255 - id: 120 kind: Tuple span: 1:24-92 children: - 121 - 122 - 123 - 124 - 125 parent: 119 - id: 121 kind: Literal span: 1:31-32 alias: id parent: 120 - id: 122 kind: Literal span: 1:43-45 alias: x_int parent: 120 - id: 123 kind: Literal span: 1:58-62 alias: x_float parent: 120 - id: 124 kind: Literal span: 1:73-74 alias: k_int parent: 120 - id: 125 kind: Literal span: 1:87-90 alias: k_float parent: 120 - id: 126 kind: Tuple span: 1:98-166 children: - 127 - 128 - 131 - 134 - 135 parent: 119 - id: 127 kind: Literal span: 1:105-106 alias: id parent: 126 - id: 128 kind: Literal span: 1:116-119 alias: x_int parent: 126 - id: 131 kind: Literal span: 1:131-136 alias: x_float parent: 126 - id: 134 kind: Literal span: 1:147-148 alias: k_int parent: 126 - id: 135 kind: Literal span: 1:161-164 alias: k_float parent: 126 - id: 136 kind: Tuple span: 1:172-240 children: - 137 - 138 - 139 - 140 - 143 parent: 119 - id: 137 kind: Literal span: 1:179-180 alias: id parent: 136 - id: 138 kind: Literal span: 1:191-193 alias: x_int parent: 136 - id: 139 kind: Literal span: 1:206-210 alias: x_float parent: 136 - id: 140 kind: Literal span: 1:220-222 alias: k_int parent: 136 - id: 143 kind: Literal span: 1:234-238 alias: k_float parent: 136 - id: 146 kind: Tuple span: 1:246-314 children: - 147 - 148 - 151 - 154 - 157 parent: 119 - id: 147 kind: Literal span: 1:253-254 alias: id parent: 146 - id: 148 kind: Literal span: 1:264-267 alias: x_int parent: 146 - id: 151 kind: Literal span: 1:279-284 alias: x_float parent: 146 - id: 154 kind: Literal span: 1:294-296 alias: k_int parent: 146 - id: 157 kind: Literal span: 1:308-312 alias: k_float parent: 146 - id: 161 kind: Ident span: 1:331-333 ident: !Ident - this - _literal_119 - id targets: - 119 parent: 254 - id: 162 kind: RqOperator span: 1:340-353 targets: - 164 - 165 parent: 254 - id: 164 kind: Ident span: 1:340-345 ident: !Ident - this - _literal_119 - x_int targets: - 119 - id: 165 kind: Ident span: 1:348-353 ident: !Ident - this - _literal_119 - k_int targets: - 119 - id: 166 kind: RqOperator span: 1:359-374 targets: - 168 - 169 parent: 254 - id: 168 kind: Ident span: 1:359-364 ident: !Ident - this - _literal_119 - x_int targets: - 119 - id: 169 kind: Ident span: 1:367-374 ident: !Ident - this - _literal_119 - k_float targets: - 119 - id: 170 kind: RqOperator span: 1:380-395 targets: - 172 - 173 parent: 254 - id: 172 kind: Ident span: 1:380-387 ident: !Ident - this - _literal_119 - x_float targets: - 119 - id: 173 kind: Ident span: 1:390-395 ident: !Ident - this - _literal_119 - k_int targets: - 119 - id: 174 kind: RqOperator span: 1:401-418 targets: - 176 - 177 parent: 254 - id: 176 kind: Ident span: 1:401-408 ident: !Ident - this - _literal_119 - x_float targets: - 119 - id: 177 kind: Ident span: 1:411-418 ident: !Ident - this - _literal_119 - k_float targets: - 119 - id: 178 kind: RqOperator span: 1:432-446 alias: q_ii targets: - 180 - 181 parent: 254 - id: 180 kind: Ident span: 1:432-437 ident: !Ident - this - _literal_119 - x_int targets: - 119 - id: 181 kind: Ident span: 1:441-446 ident: !Ident - this - _literal_119 - k_int targets: - 119 - id: 182 kind: RqOperator span: 1:459-475 alias: q_if targets: - 184 - 185 parent: 254 - id: 184 kind: Ident span: 1:459-464 ident: !Ident - this - _literal_119 - x_int targets: - 119 - id: 185 kind: Ident span: 1:468-475 ident: !Ident - this - _literal_119 - k_float targets: - 119 - id: 186 kind: RqOperator span: 1:488-504 alias: q_fi targets: - 188 - 189 parent: 254 - id: 188 kind: Ident span: 1:488-495 ident: !Ident - this - _literal_119 - x_float targets: - 119 - id: 189 kind: Ident span: 1:499-504 ident: !Ident - this - _literal_119 - k_int targets: - 119 - id: 190 kind: RqOperator span: 1:517-535 alias: q_ff targets: - 192 - 193 parent: 254 - id: 192 kind: Ident span: 1:517-524 ident: !Ident - this - _literal_119 - x_float targets: - 119 - id: 193 kind: Ident span: 1:528-535 ident: !Ident - this - _literal_119 - k_float targets: - 119 - id: 194 kind: RqOperator span: 1:549-562 alias: r_ii targets: - 196 - 197 parent: 254 - id: 196 kind: Ident span: 1:549-554 ident: !Ident - this - _literal_119 - x_int targets: - 119 - id: 197 kind: Ident span: 1:557-562 ident: !Ident - this - _literal_119 - k_int targets: - 119 - id: 198 kind: RqOperator span: 1:575-590 alias: r_if targets: - 200 - 201 parent: 254 - id: 200 kind: Ident span: 1:575-580 ident: !Ident - this - _literal_119 - x_int targets: - 119 - id: 201 kind: Ident span: 1:583-590 ident: !Ident - this - _literal_119 - k_float targets: - 119 - id: 202 kind: RqOperator span: 1:603-618 alias: r_fi targets: - 204 - 205 parent: 254 - id: 204 kind: Ident span: 1:603-610 ident: !Ident - this - _literal_119 - x_float targets: - 119 - id: 205 kind: Ident span: 1:613-618 ident: !Ident - this - _literal_119 - k_int targets: - 119 - id: 206 kind: RqOperator span: 1:631-648 alias: r_ff targets: - 208 - 209 parent: 254 - id: 208 kind: Ident span: 1:631-638 ident: !Ident - this - _literal_119 - x_float targets: - 119 - id: 209 kind: Ident span: 1:641-648 ident: !Ident - this - _literal_119 - k_float targets: - 119 - id: 210 kind: RqOperator span: 1:678-690 targets: - 213 - 214 parent: 254 - id: 213 kind: Literal span: 1:689-690 - id: 214 kind: RqOperator span: 1:656-675 targets: - 216 - 220 - id: 216 kind: RqOperator span: 1:656-668 targets: - 218 - 219 - id: 218 kind: Ident span: 1:656-660 ident: !Ident - this - q_ii targets: - 178 - id: 219 kind: Ident span: 1:663-668 ident: !Ident - this - _literal_119 - k_int targets: - 119 - id: 220 kind: Ident span: 1:671-675 ident: !Ident - this - r_ii targets: - 194 - id: 221 kind: RqOperator span: 1:722-734 targets: - 224 - 225 parent: 254 - id: 224 kind: Literal span: 1:733-734 - id: 225 kind: RqOperator span: 1:698-719 targets: - 227 - 231 - id: 227 kind: RqOperator span: 1:698-712 targets: - 229 - 230 - id: 229 kind: Ident span: 1:698-702 ident: !Ident - this - q_if targets: - 182 - id: 230 kind: Ident span: 1:705-712 ident: !Ident - this - _literal_119 - k_float targets: - 119 - id: 231 kind: Ident span: 1:715-719 ident: !Ident - this - r_if targets: - 198 - id: 232 kind: RqOperator span: 1:764-776 targets: - 235 - 236 parent: 254 - id: 235 kind: Literal span: 1:775-776 - id: 236 kind: RqOperator span: 1:742-761 targets: - 238 - 242 - id: 238 kind: RqOperator span: 1:742-754 targets: - 240 - 241 - id: 240 kind: Ident span: 1:742-746 ident: !Ident - this - q_fi targets: - 186 - id: 241 kind: Ident span: 1:749-754 ident: !Ident - this - _literal_119 - k_int targets: - 119 - id: 242 kind: Ident span: 1:757-761 ident: !Ident - this - r_fi targets: - 202 - id: 243 kind: RqOperator span: 1:808-820 targets: - 246 - 247 parent: 254 - id: 246 kind: Literal span: 1:819-820 - id: 247 kind: RqOperator span: 1:784-805 targets: - 249 - 253 - id: 249 kind: RqOperator span: 1:784-798 targets: - 251 - 252 - id: 251 kind: Ident span: 1:784-788 ident: !Ident - this - q_ff targets: - 190 - id: 252 kind: Ident span: 1:791-798 ident: !Ident - this - _literal_119 - k_float targets: - 119 - id: 253 kind: Ident span: 1:801-805 ident: !Ident - this - r_ff targets: - 206 - id: 254 kind: Tuple span: 1:325-824 children: - 161 - 162 - 166 - 170 - 174 - 178 - 182 - 186 - 190 - 194 - 198 - 202 - 206 - 210 - 221 - 232 - 243 parent: 255 - id: 255 kind: 'TransformCall: Select' span: 1:318-824 children: - 119 - 254 parent: 258 - id: 256 kind: Ident span: 1:830-832 ident: !Ident - this - _literal_119 - id targets: - 161 parent: 258 - id: 258 kind: 'TransformCall: Sort' span: 1:825-832 children: - 255 - 256 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:13-17 args: - Array: - Tuple: - Literal: Integer: 1 span: 1:31-32 alias: id - Literal: Integer: 13 span: 1:43-45 alias: x_int - Literal: Float: 13.0 span: 1:58-62 alias: x_float - Literal: Integer: 5 span: 1:73-74 alias: k_int - Literal: Float: 5.0 span: 1:87-90 alias: k_float span: 1:24-92 - Tuple: - Literal: Integer: 2 span: 1:105-106 alias: id - Unary: op: Neg expr: Literal: Integer: 13 span: 1:117-119 span: 1:116-119 alias: x_int - Unary: op: Neg expr: Literal: Float: 13.0 span: 1:132-136 span: 1:131-136 alias: x_float - Literal: Integer: 5 span: 1:147-148 alias: k_int - Literal: Float: 5.0 span: 1:161-164 alias: k_float span: 1:98-166 - Tuple: - Literal: Integer: 3 span: 1:179-180 alias: id - Literal: Integer: 13 span: 1:191-193 alias: x_int - Literal: Float: 13.0 span: 1:206-210 alias: x_float - Unary: op: Neg expr: Literal: Integer: 5 span: 1:221-222 span: 1:220-222 alias: k_int - Unary: op: Neg expr: Literal: Float: 5.0 span: 1:235-238 span: 1:234-238 alias: k_float span: 1:172-240 - Tuple: - Literal: Integer: 4 span: 1:253-254 alias: id - Unary: op: Neg expr: Literal: Integer: 13 span: 1:265-267 span: 1:264-267 alias: x_int - Unary: op: Neg expr: Literal: Float: 13.0 span: 1:280-284 span: 1:279-284 alias: x_float - Unary: op: Neg expr: Literal: Integer: 5 span: 1:295-296 span: 1:294-296 alias: k_int - Unary: op: Neg expr: Literal: Float: 5.0 span: 1:309-312 span: 1:308-312 alias: k_float span: 1:246-314 span: 1:18-317 span: 1:13-317 - FuncCall: name: Ident: - select span: 1:318-324 args: - Tuple: - Ident: - id span: 1:331-333 - Binary: left: Ident: - x_int span: 1:340-345 op: DivFloat right: Ident: - k_int span: 1:348-353 span: 1:340-353 - Binary: left: Ident: - x_int span: 1:359-364 op: DivFloat right: Ident: - k_float span: 1:367-374 span: 1:359-374 - Binary: left: Ident: - x_float span: 1:380-387 op: DivFloat right: Ident: - k_int span: 1:390-395 span: 1:380-395 - Binary: left: Ident: - x_float span: 1:401-408 op: DivFloat right: Ident: - k_float span: 1:411-418 span: 1:401-418 - Binary: left: Ident: - x_int span: 1:432-437 op: DivInt right: Ident: - k_int span: 1:441-446 span: 1:432-446 alias: q_ii - Binary: left: Ident: - x_int span: 1:459-464 op: DivInt right: Ident: - k_float span: 1:468-475 span: 1:459-475 alias: q_if - Binary: left: Ident: - x_float span: 1:488-495 op: DivInt right: Ident: - k_int span: 1:499-504 span: 1:488-504 alias: q_fi - Binary: left: Ident: - x_float span: 1:517-524 op: DivInt right: Ident: - k_float span: 1:528-535 span: 1:517-535 alias: q_ff - Binary: left: Ident: - x_int span: 1:549-554 op: Mod right: Ident: - k_int span: 1:557-562 span: 1:549-562 alias: r_ii - Binary: left: Ident: - x_int span: 1:575-580 op: Mod right: Ident: - k_float span: 1:583-590 span: 1:575-590 alias: r_if - Binary: left: Ident: - x_float span: 1:603-610 op: Mod right: Ident: - k_int span: 1:613-618 span: 1:603-618 alias: r_fi - Binary: left: Ident: - x_float span: 1:631-638 op: Mod right: Ident: - k_float span: 1:641-648 span: 1:631-648 alias: r_ff - Pipeline: exprs: - Binary: left: Binary: left: Ident: - q_ii span: 1:656-660 op: Mul right: Ident: - k_int span: 1:663-668 span: 1:656-668 op: Add right: Ident: - r_ii span: 1:671-675 span: 1:656-675 - FuncCall: name: Ident: - math - round span: 1:678-688 args: - Literal: Integer: 0 span: 1:689-690 span: 1:678-690 span: 1:655-691 - Pipeline: exprs: - Binary: left: Binary: left: Ident: - q_if span: 1:698-702 op: Mul right: Ident: - k_float span: 1:705-712 span: 1:698-712 op: Add right: Ident: - r_if span: 1:715-719 span: 1:698-719 - FuncCall: name: Ident: - math - round span: 1:722-732 args: - Literal: Integer: 0 span: 1:733-734 span: 1:722-734 span: 1:697-735 - Pipeline: exprs: - Binary: left: Binary: left: Ident: - q_fi span: 1:742-746 op: Mul right: Ident: - k_int span: 1:749-754 span: 1:742-754 op: Add right: Ident: - r_fi span: 1:757-761 span: 1:742-761 - FuncCall: name: Ident: - math - round span: 1:764-774 args: - Literal: Integer: 0 span: 1:775-776 span: 1:764-776 span: 1:741-777 - Pipeline: exprs: - Binary: left: Binary: left: Ident: - q_ff span: 1:784-788 op: Mul right: Ident: - k_float span: 1:791-798 span: 1:784-798 op: Add right: Ident: - r_ff span: 1:801-805 span: 1:784-805 - FuncCall: name: Ident: - math - round span: 1:808-818 args: - Literal: Integer: 0 span: 1:819-820 span: 1:808-820 span: 1:783-821 span: 1:325-824 span: 1:318-824 - FuncCall: name: Ident: - sort span: 1:825-829 args: - Ident: - id span: 1:830-832 span: 1:825-832 span: 1:13-832 span: 1:0-832 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__cast.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nsort {-bytes}\nselect {\n name,\n bin = ((album_id | as REAL) * 99)\n}\ntake 20\n" input_file: prqlc/prqlc/tests/integration/queries/cast.prql --- frames: - - 1:25-38 - columns: - !All input_id: 122 except: [] inputs: - id: 122 name: tracks table: - default_db - tracks - - 1:39-97 - columns: - !Single name: - tracks - name target_id: 129 target_name: null - !Single name: - bin target_id: 130 target_name: null inputs: - id: 122 name: tracks table: - default_db - tracks - - 1:98-105 - columns: - !Single name: - tracks - name target_id: 129 target_name: null - !Single name: - bin target_id: 130 target_name: null inputs: - id: 122 name: tracks table: - default_db - tracks nodes: - id: 122 kind: Ident span: 1:13-24 ident: !Ident - default_db - tracks parent: 128 - id: 126 kind: Ident span: 1:32-37 ident: !Ident - this - tracks - bytes targets: - 122 parent: 128 - id: 128 kind: 'TransformCall: Sort' span: 1:25-38 children: - 122 - 126 parent: 138 - id: 129 kind: Ident span: 1:52-56 ident: !Ident - this - tracks - name targets: - 122 parent: 137 - id: 130 kind: RqOperator span: 1:68-95 alias: bin targets: - 132 - 136 parent: 137 - id: 132 kind: RqOperator span: 1:81-88 targets: - 135 - id: 135 kind: Ident span: 1:70-78 ident: !Ident - this - tracks - album_id targets: - 122 - id: 136 kind: Literal span: 1:92-94 - id: 137 kind: Tuple span: 1:46-97 children: - 129 - 130 parent: 138 - id: 138 kind: 'TransformCall: Select' span: 1:39-97 children: - 128 - 137 parent: 140 - id: 140 kind: 'TransformCall: Take' span: 1:98-105 children: - 138 - 141 - id: 141 kind: Literal parent: 140 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:13-17 args: - Ident: - tracks span: 1:18-24 span: 1:13-24 - FuncCall: name: Ident: - sort span: 1:25-29 args: - Tuple: - Unary: op: Neg expr: Ident: - bytes span: 1:32-37 span: 1:31-37 span: 1:30-38 span: 1:25-38 - FuncCall: name: Ident: - select span: 1:39-45 args: - Tuple: - Ident: - name span: 1:52-56 - Binary: left: Pipeline: exprs: - Ident: - album_id span: 1:70-78 - FuncCall: name: Ident: - as span: 1:81-83 args: - Ident: - REAL span: 1:84-88 span: 1:81-88 span: 1:70-88 op: Mul right: Literal: Integer: 99 span: 1:92-94 span: 1:68-95 alias: bin span: 1:46-97 span: 1:39-97 - FuncCall: name: Ident: - take span: 1:98-102 args: - Literal: Integer: 20 span: 1:103-105 span: 1:98-105 span: 1:13-105 span: 1:0-105 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__constants_only.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from genres\ntake 10\nfilter true\ntake 20\nfilter true\nselect d = 10\n" input_file: prqlc/prqlc/tests/integration/queries/constants_only.prql --- frames: - - 1:12-19 - columns: - !All input_id: 128 except: [] inputs: - id: 128 name: genres table: - default_db - genres - - 1:20-31 - columns: - !All input_id: 128 except: [] inputs: - id: 128 name: genres table: - default_db - genres - - 1:32-39 - columns: - !All input_id: 128 except: [] inputs: - id: 128 name: genres table: - default_db - genres - - 1:40-51 - columns: - !All input_id: 128 except: [] inputs: - id: 128 name: genres table: - default_db - genres - - 1:52-65 - columns: - !Single name: - d target_id: 140 target_name: null inputs: - id: 128 name: genres table: - default_db - genres nodes: - id: 128 kind: Ident span: 1:0-11 ident: !Ident - default_db - genres parent: 131 - id: 131 kind: 'TransformCall: Take' span: 1:12-19 children: - 128 - 132 parent: 134 - id: 132 kind: Literal parent: 131 - id: 133 kind: Literal span: 1:27-31 parent: 134 - id: 134 kind: 'TransformCall: Filter' span: 1:20-31 children: - 131 - 133 parent: 136 - id: 136 kind: 'TransformCall: Take' span: 1:32-39 children: - 134 - 137 parent: 139 - id: 137 kind: Literal parent: 136 - id: 138 kind: Literal span: 1:47-51 parent: 139 - id: 139 kind: 'TransformCall: Filter' span: 1:40-51 children: - 136 - 138 parent: 142 - id: 140 kind: Literal span: 1:63-65 alias: d parent: 141 - id: 141 kind: Tuple span: 1:63-65 children: - 140 parent: 142 - id: 142 kind: 'TransformCall: Select' span: 1:52-65 children: - 139 - 141 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:0-4 args: - Ident: - genres span: 1:5-11 span: 1:0-11 - FuncCall: name: Ident: - take span: 1:12-16 args: - Literal: Integer: 10 span: 1:17-19 span: 1:12-19 - FuncCall: name: Ident: - filter span: 1:20-26 args: - Literal: Boolean: true span: 1:27-31 span: 1:20-31 - FuncCall: name: Ident: - take span: 1:32-36 args: - Literal: Integer: 20 span: 1:37-39 span: 1:32-39 - FuncCall: name: Ident: - filter span: 1:40-46 args: - Literal: Boolean: true span: 1:47-51 span: 1:40-51 - FuncCall: name: Ident: - select span: 1:52-58 args: - Literal: Integer: 10 span: 1:63-65 alias: d span: 1:52-65 span: 1:0-65 span: 1:0-65 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__date_to_text.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# generic:skip\n# glaredb:skip\n# sqlite:skip\n# mssql:test\nfrom invoices\ntake 20\nselect {\n d1 = (invoice_date | date.to_text \"%Y/%m/%d\"),\n d2 = (invoice_date | date.to_text \"%F\"),\n d3 = (invoice_date | date.to_text \"%D\"),\n d4 = (invoice_date | date.to_text \"%H:%M:%S.%f\"),\n d5 = (invoice_date | date.to_text \"%r\"),\n d6 = (invoice_date | date.to_text \"%A %B %-d %Y\"),\n d7 = (invoice_date | date.to_text \"%a, %-d %b %Y at %I:%M:%S %p\"),\n d8 = (invoice_date | date.to_text \"%+\"),\n d9 = (invoice_date | date.to_text \"%-d/%-m/%y\"),\n d10 = (invoice_date | date.to_text \"%-Hh %Mmin\"),\n d11 = (invoice_date | date.to_text \"%M'%S\\\"\"),\n d12 = (invoice_date | date.to_text \"100%% in %d days\"),\n}\n" input_file: prqlc/prqlc/tests/integration/queries/date_to_text.prql --- frames: - - 1:71-78 - columns: - !All input_id: 119 except: [] inputs: - id: 119 name: invoices table: - default_db - invoices - - 1:79-718 - columns: - !Single name: - d1 target_id: 124 target_name: null - !Single name: - d2 target_id: 129 target_name: null - !Single name: - d3 target_id: 134 target_name: null - !Single name: - d4 target_id: 139 target_name: null - !Single name: - d5 target_id: 144 target_name: null - !Single name: - d6 target_id: 149 target_name: null - !Single name: - d7 target_id: 154 target_name: null - !Single name: - d8 target_id: 159 target_name: null - !Single name: - d9 target_id: 164 target_name: null - !Single name: - d10 target_id: 169 target_name: null - !Single name: - d11 target_id: 174 target_name: null - !Single name: - d12 target_id: 179 target_name: null inputs: - id: 119 name: invoices table: - default_db - invoices nodes: - id: 119 kind: Ident span: 1:57-70 ident: !Ident - default_db - invoices parent: 122 - id: 122 kind: 'TransformCall: Take' span: 1:71-78 children: - 119 - 123 parent: 185 - id: 123 kind: Literal parent: 122 - id: 124 kind: RqOperator span: 1:113-136 alias: d1 targets: - 127 - 128 parent: 184 - id: 127 kind: Literal span: 1:126-136 - id: 128 kind: Ident span: 1:98-110 ident: !Ident - this - invoices - invoice_date targets: - 119 - id: 129 kind: RqOperator span: 1:164-181 alias: d2 targets: - 132 - 133 parent: 184 - id: 132 kind: Literal span: 1:177-181 - id: 133 kind: Ident span: 1:149-161 ident: !Ident - this - invoices - invoice_date targets: - 119 - id: 134 kind: RqOperator span: 1:209-226 alias: d3 targets: - 137 - 138 parent: 184 - id: 137 kind: Literal span: 1:222-226 - id: 138 kind: Ident span: 1:194-206 ident: !Ident - this - invoices - invoice_date targets: - 119 - id: 139 kind: RqOperator span: 1:254-280 alias: d4 targets: - 142 - 143 parent: 184 - id: 142 kind: Literal span: 1:267-280 - id: 143 kind: Ident span: 1:239-251 ident: !Ident - this - invoices - invoice_date targets: - 119 - id: 144 kind: RqOperator span: 1:308-325 alias: d5 targets: - 147 - 148 parent: 184 - id: 147 kind: Literal span: 1:321-325 - id: 148 kind: Ident span: 1:293-305 ident: !Ident - this - invoices - invoice_date targets: - 119 - id: 149 kind: RqOperator span: 1:353-380 alias: d6 targets: - 152 - 153 parent: 184 - id: 152 kind: Literal span: 1:366-380 - id: 153 kind: Ident span: 1:338-350 ident: !Ident - this - invoices - invoice_date targets: - 119 - id: 154 kind: RqOperator span: 1:408-451 alias: d7 targets: - 157 - 158 parent: 184 - id: 157 kind: Literal span: 1:421-451 - id: 158 kind: Ident span: 1:393-405 ident: !Ident - this - invoices - invoice_date targets: - 119 - id: 159 kind: RqOperator span: 1:479-496 alias: d8 targets: - 162 - 163 parent: 184 - id: 162 kind: Literal span: 1:492-496 - id: 163 kind: Ident span: 1:464-476 ident: !Ident - this - invoices - invoice_date targets: - 119 - id: 164 kind: RqOperator span: 1:524-549 alias: d9 targets: - 167 - 168 parent: 184 - id: 167 kind: Literal span: 1:537-549 - id: 168 kind: Ident span: 1:509-521 ident: !Ident - this - invoices - invoice_date targets: - 119 - id: 169 kind: RqOperator span: 1:578-603 alias: d10 targets: - 172 - 173 parent: 184 - id: 172 kind: Literal span: 1:591-603 - id: 173 kind: Ident span: 1:563-575 ident: !Ident - this - invoices - invoice_date targets: - 119 - id: 174 kind: RqOperator span: 1:632-654 alias: d11 targets: - 177 - 178 parent: 184 - id: 177 kind: Literal span: 1:645-654 - id: 178 kind: Ident span: 1:617-629 ident: !Ident - this - invoices - invoice_date targets: - 119 - id: 179 kind: RqOperator span: 1:683-714 alias: d12 targets: - 182 - 183 parent: 184 - id: 182 kind: Literal span: 1:696-714 - id: 183 kind: Ident span: 1:668-680 ident: !Ident - this - invoices - invoice_date targets: - 119 - id: 184 kind: Tuple span: 1:86-718 children: - 124 - 129 - 134 - 139 - 144 - 149 - 154 - 159 - 164 - 169 - 174 - 179 parent: 185 - id: 185 kind: 'TransformCall: Select' span: 1:79-718 children: - 122 - 184 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:57-61 args: - Ident: - invoices span: 1:62-70 span: 1:57-70 - FuncCall: name: Ident: - take span: 1:71-75 args: - Literal: Integer: 20 span: 1:76-78 span: 1:71-78 - FuncCall: name: Ident: - select span: 1:79-85 args: - Tuple: - Pipeline: exprs: - Ident: - invoice_date span: 1:98-110 - FuncCall: name: Ident: - date - to_text span: 1:113-125 args: - Literal: String: '%Y/%m/%d' span: 1:126-136 span: 1:113-136 span: 1:97-137 alias: d1 - Pipeline: exprs: - Ident: - invoice_date span: 1:149-161 - FuncCall: name: Ident: - date - to_text span: 1:164-176 args: - Literal: String: '%F' span: 1:177-181 span: 1:164-181 span: 1:148-182 alias: d2 - Pipeline: exprs: - Ident: - invoice_date span: 1:194-206 - FuncCall: name: Ident: - date - to_text span: 1:209-221 args: - Literal: String: '%D' span: 1:222-226 span: 1:209-226 span: 1:193-227 alias: d3 - Pipeline: exprs: - Ident: - invoice_date span: 1:239-251 - FuncCall: name: Ident: - date - to_text span: 1:254-266 args: - Literal: String: '%H:%M:%S.%f' span: 1:267-280 span: 1:254-280 span: 1:238-281 alias: d4 - Pipeline: exprs: - Ident: - invoice_date span: 1:293-305 - FuncCall: name: Ident: - date - to_text span: 1:308-320 args: - Literal: String: '%r' span: 1:321-325 span: 1:308-325 span: 1:292-326 alias: d5 - Pipeline: exprs: - Ident: - invoice_date span: 1:338-350 - FuncCall: name: Ident: - date - to_text span: 1:353-365 args: - Literal: String: '%A %B %-d %Y' span: 1:366-380 span: 1:353-380 span: 1:337-381 alias: d6 - Pipeline: exprs: - Ident: - invoice_date span: 1:393-405 - FuncCall: name: Ident: - date - to_text span: 1:408-420 args: - Literal: String: '%a, %-d %b %Y at %I:%M:%S %p' span: 1:421-451 span: 1:408-451 span: 1:392-452 alias: d7 - Pipeline: exprs: - Ident: - invoice_date span: 1:464-476 - FuncCall: name: Ident: - date - to_text span: 1:479-491 args: - Literal: String: '%+' span: 1:492-496 span: 1:479-496 span: 1:463-497 alias: d8 - Pipeline: exprs: - Ident: - invoice_date span: 1:509-521 - FuncCall: name: Ident: - date - to_text span: 1:524-536 args: - Literal: String: '%-d/%-m/%y' span: 1:537-549 span: 1:524-549 span: 1:508-550 alias: d9 - Pipeline: exprs: - Ident: - invoice_date span: 1:563-575 - FuncCall: name: Ident: - date - to_text span: 1:578-590 args: - Literal: String: '%-Hh %Mmin' span: 1:591-603 span: 1:578-603 span: 1:562-604 alias: d10 - Pipeline: exprs: - Ident: - invoice_date span: 1:617-629 - FuncCall: name: Ident: - date - to_text span: 1:632-644 args: - Literal: String: '%M''%S"' span: 1:645-654 span: 1:632-654 span: 1:616-655 alias: d11 - Pipeline: exprs: - Ident: - invoice_date span: 1:668-680 - FuncCall: name: Ident: - date - to_text span: 1:683-695 args: - Literal: String: 100%% in %d days span: 1:696-714 span: 1:683-714 span: 1:667-715 alias: d12 span: 1:86-718 span: 1:79-718 span: 1:57-718 span: 1:0-718 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__distinct.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nselect {album_id, genre_id}\ngroup tracks.* (take 1)\nsort tracks.*\n" input_file: prqlc/prqlc/tests/integration/queries/distinct.prql --- frames: - - 1:25-52 - columns: - !Single name: - tracks - album_id target_id: 124 target_name: null - !Single name: - tracks - genre_id target_id: 125 target_name: null inputs: - id: 122 name: tracks table: - default_db - tracks - - 1:69-75 - columns: - !Single name: - tracks - album_id target_id: 129 target_name: null - !Single name: - tracks - genre_id target_id: 130 target_name: null inputs: - id: 122 name: tracks table: - default_db - tracks - - 1:77-90 - columns: - !Single name: - tracks - album_id target_id: 129 target_name: null - !Single name: - tracks - genre_id target_id: 130 target_name: null inputs: - id: 122 name: tracks table: - default_db - tracks nodes: - id: 122 kind: Ident span: 1:13-24 ident: !Ident - default_db - tracks parent: 127 - id: 124 kind: Ident span: 1:33-41 ident: !Ident - this - tracks - album_id targets: - 122 parent: 126 - id: 125 kind: Ident span: 1:43-51 ident: !Ident - this - tracks - genre_id targets: - 122 parent: 126 - id: 126 kind: Tuple span: 1:32-52 children: - 124 - 125 parent: 127 - id: 127 kind: 'TransformCall: Select' span: 1:25-52 children: - 122 - 126 parent: 148 - id: 129 kind: Ident ident: !Ident - this - tracks - album_id targets: - 124 parent: 131 - id: 130 kind: Ident ident: !Ident - this - tracks - genre_id targets: - 125 parent: 131 - id: 131 kind: Tuple span: 1:59-67 children: - 129 - 130 - id: 148 kind: 'TransformCall: Take' span: 1:69-75 children: - 127 - 149 parent: 156 - id: 149 kind: Literal parent: 148 - id: 153 kind: Ident ident: !Ident - this - tracks - album_id targets: - 129 parent: 156 - id: 154 kind: Ident ident: !Ident - this - tracks - genre_id targets: - 130 parent: 156 - id: 156 kind: 'TransformCall: Sort' span: 1:77-90 children: - 148 - 153 - 154 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:13-17 args: - Ident: - tracks span: 1:18-24 span: 1:13-24 - FuncCall: name: Ident: - select span: 1:25-31 args: - Tuple: - Ident: - album_id span: 1:33-41 - Ident: - genre_id span: 1:43-51 span: 1:32-52 span: 1:25-52 - FuncCall: name: Ident: - group span: 1:53-58 args: - Ident: - tracks - '*' span: 1:59-67 - FuncCall: name: Ident: - take span: 1:69-73 args: - Literal: Integer: 1 span: 1:74-75 span: 1:69-75 span: 1:53-76 - FuncCall: name: Ident: - sort span: 1:77-81 args: - Ident: - tracks - '*' span: 1:82-90 span: 1:77-90 span: 1:13-90 span: 1:0-90 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__distinct_on.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nselect {genre_id, media_type_id, album_id}\ngroup {genre_id, media_type_id} (sort {-album_id} | take 1)\nsort {-genre_id, media_type_id}\n" input_file: prqlc/prqlc/tests/integration/queries/distinct_on.prql --- frames: - - 1:25-67 - columns: - !Single name: - tracks - genre_id target_id: 124 target_name: null - !Single name: - tracks - media_type_id target_id: 125 target_name: null - !Single name: - tracks - album_id target_id: 126 target_name: null inputs: - id: 122 name: tracks table: - default_db - tracks - - 1:120-126 - columns: - !Single name: - tracks - genre_id target_id: 129 target_name: null - !Single name: - tracks - media_type_id target_id: 130 target_name: null - !Single name: - tracks - album_id target_id: 126 target_name: null inputs: - id: 122 name: tracks table: - default_db - tracks - - 1:128-159 - columns: - !Single name: - tracks - genre_id target_id: 129 target_name: null - !Single name: - tracks - media_type_id target_id: 130 target_name: null - !Single name: - tracks - album_id target_id: 126 target_name: null inputs: - id: 122 name: tracks table: - default_db - tracks nodes: - id: 122 kind: Ident span: 1:13-24 ident: !Ident - default_db - tracks parent: 128 - id: 124 kind: Ident span: 1:33-41 ident: !Ident - this - tracks - genre_id targets: - 122 parent: 127 - id: 125 kind: Ident span: 1:43-56 ident: !Ident - this - tracks - media_type_id targets: - 122 parent: 127 - id: 126 kind: Ident span: 1:58-66 ident: !Ident - this - tracks - album_id targets: - 122 parent: 127 - id: 127 kind: Tuple span: 1:32-67 children: - 124 - 125 - 126 parent: 128 - id: 128 kind: 'TransformCall: Select' span: 1:25-67 children: - 122 - 127 parent: 160 - id: 129 kind: Ident span: 1:75-83 ident: !Ident - this - tracks - genre_id targets: - 124 parent: 131 - id: 130 kind: Ident span: 1:85-98 ident: !Ident - this - tracks - media_type_id targets: - 125 parent: 131 - id: 131 kind: Tuple span: 1:74-99 children: - 129 - 130 - id: 156 kind: Ident span: 1:108-116 ident: !Ident - this - tracks - album_id targets: - 126 - id: 160 kind: 'TransformCall: Take' span: 1:120-126 children: - 128 - 161 parent: 169 - id: 161 kind: Literal parent: 160 - id: 166 kind: Ident span: 1:135-143 ident: !Ident - this - tracks - genre_id targets: - 129 parent: 169 - id: 167 kind: Ident span: 1:145-158 ident: !Ident - this - tracks - media_type_id targets: - 130 parent: 169 - id: 169 kind: 'TransformCall: Sort' span: 1:128-159 children: - 160 - 166 - 167 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:13-17 args: - Ident: - tracks span: 1:18-24 span: 1:13-24 - FuncCall: name: Ident: - select span: 1:25-31 args: - Tuple: - Ident: - genre_id span: 1:33-41 - Ident: - media_type_id span: 1:43-56 - Ident: - album_id span: 1:58-66 span: 1:32-67 span: 1:25-67 - FuncCall: name: Ident: - group span: 1:68-73 args: - Tuple: - Ident: - genre_id span: 1:75-83 - Ident: - media_type_id span: 1:85-98 span: 1:74-99 - Pipeline: exprs: - FuncCall: name: Ident: - sort span: 1:101-105 args: - Tuple: - Unary: op: Neg expr: Ident: - album_id span: 1:108-116 span: 1:107-116 span: 1:106-117 span: 1:101-117 - FuncCall: name: Ident: - take span: 1:120-124 args: - Literal: Integer: 1 span: 1:125-126 span: 1:120-126 span: 1:101-126 span: 1:68-127 - FuncCall: name: Ident: - sort span: 1:128-132 args: - Tuple: - Unary: op: Neg expr: Ident: - genre_id span: 1:135-143 span: 1:134-143 - Ident: - media_type_id span: 1:145-158 span: 1:133-159 span: 1:128-159 span: 1:13-159 span: 1:0-159 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__genre_counts.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip (ClickHouse prefers aliases to column names https://github.com/PRQL/prql/issues/2827)\n# mssql:test\nlet genre_count = (\n from genres\n aggregate {a = count name}\n)\n\nfrom genre_count\nfilter a > 0\nselect a = -a\n" input_file: prqlc/prqlc/tests/integration/queries/genre_counts.prql --- frames: - - 1:204-216 - columns: - !Single name: - genre_count - a target_id: 134 target_name: a inputs: - id: 134 name: genre_count table: - default_db - genres - - 1:217-230 - columns: - !Single name: - a target_id: 141 target_name: null inputs: - id: 134 name: genre_count table: - default_db - genres nodes: - id: 134 kind: Ident span: 1:187-203 ident: !Ident - genre_count parent: 140 - id: 136 kind: RqOperator span: 1:211-216 targets: - 138 - 139 parent: 140 - id: 138 kind: Ident span: 1:211-212 ident: !Ident - this - genre_count - a targets: - 134 - id: 139 kind: Literal span: 1:215-216 - id: 140 kind: 'TransformCall: Filter' span: 1:204-216 children: - 134 - 136 parent: 145 - id: 141 kind: RqOperator span: 1:228-230 alias: a targets: - 143 parent: 144 - id: 143 kind: Ident span: 1:229-230 ident: !Ident - this - genre_count - a targets: - 134 - id: 144 kind: Tuple span: 1:228-230 children: - 141 parent: 145 - id: 145 kind: 'TransformCall: Select' span: 1:217-230 children: - 140 - 144 ast: name: Project stmts: - VarDef: kind: Let name: genre_count value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:141-145 args: - Ident: - genres span: 1:146-152 span: 1:141-152 - FuncCall: name: Ident: - aggregate span: 1:157-166 args: - Tuple: - FuncCall: name: Ident: - count span: 1:172-177 args: - Ident: - name span: 1:178-182 span: 1:172-182 alias: a span: 1:167-183 span: 1:157-183 span: 1:135-185 span: 1:0-185 - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:187-191 args: - Ident: - genre_count span: 1:192-203 span: 1:187-203 - FuncCall: name: Ident: - filter span: 1:204-210 args: - Binary: left: Ident: - a span: 1:211-212 op: Gt right: Literal: Integer: 0 span: 1:215-216 span: 1:211-216 span: 1:204-216 - FuncCall: name: Ident: - select span: 1:217-223 args: - Unary: op: Neg expr: Ident: - a span: 1:229-230 span: 1:228-230 alias: a span: 1:217-230 span: 1:187-230 span: 1:185-230 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__group_all.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom a=albums\ntake 10\njoin tracks (==album_id)\ngroup {a.album_id, a.title} (aggregate price = (sum tracks.unit_price | math.round 2))\nsort album_id\n" input_file: prqlc/prqlc/tests/integration/queries/group_all.prql --- frames: - - 1:27-34 - columns: - !All input_id: 126 except: [] inputs: - id: 126 name: a table: - default_db - albums - - 1:35-59 - columns: - !All input_id: 126 except: [] - !All input_id: 120 except: [] inputs: - id: 126 name: a table: - default_db - albums - id: 120 name: tracks table: - default_db - tracks - - 1:89-145 - columns: - !Single name: - a - album_id target_id: 136 target_name: null - !Single name: - a - title target_id: 137 target_name: null - !Single name: - price target_id: 155 target_name: null inputs: - id: 126 name: a table: - default_db - albums - id: 120 name: tracks table: - default_db - tracks - - 1:147-160 - columns: - !Single name: - a - album_id target_id: 136 target_name: null - !Single name: - a - title target_id: 137 target_name: null - !Single name: - price target_id: 155 target_name: null inputs: - id: 126 name: a table: - default_db - albums - id: 120 name: tracks table: - default_db - tracks nodes: - id: 120 kind: Ident span: 1:40-46 ident: !Ident - default_db - tracks parent: 135 - id: 126 kind: Ident span: 1:13-26 ident: !Ident - default_db - albums parent: 129 - id: 129 kind: 'TransformCall: Take' span: 1:27-34 children: - 126 - 130 parent: 135 - id: 130 kind: Literal parent: 129 - id: 131 kind: RqOperator span: 1:48-58 targets: - 133 - 134 parent: 135 - id: 133 kind: Ident span: 1:50-58 ident: !Ident - this - a - album_id targets: - 126 - id: 134 kind: Ident span: 1:50-58 ident: !Ident - that - tracks - album_id targets: - 120 - id: 135 kind: 'TransformCall: Join' span: 1:35-59 children: - 129 - 120 - 131 parent: 163 - id: 136 kind: Ident span: 1:67-77 ident: !Ident - this - a - album_id targets: - 126 parent: 138 - id: 137 kind: Ident span: 1:79-86 ident: !Ident - this - a - title targets: - 126 parent: 138 - id: 138 kind: Tuple span: 1:66-87 children: - 136 - 137 parent: 163 - id: 155 kind: RqOperator span: 1:132-144 alias: price targets: - 158 - 159 parent: 162 - id: 158 kind: Literal span: 1:143-144 - id: 159 kind: RqOperator span: 1:108-129 targets: - 161 - id: 161 kind: Ident span: 1:112-129 ident: !Ident - this - tracks - unit_price targets: - 120 - id: 162 kind: Tuple span: 1:132-144 children: - 155 parent: 163 - id: 163 kind: 'TransformCall: Aggregate' span: 1:89-145 children: - 135 - 162 - 138 parent: 168 - id: 166 kind: Ident span: 1:152-160 ident: !Ident - this - a - album_id targets: - 136 parent: 168 - id: 168 kind: 'TransformCall: Sort' span: 1:147-160 children: - 163 - 166 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:13-17 args: - Ident: - albums span: 1:20-26 alias: a span: 1:13-26 - FuncCall: name: Ident: - take span: 1:27-31 args: - Literal: Integer: 10 span: 1:32-34 span: 1:27-34 - FuncCall: name: Ident: - join span: 1:35-39 args: - Ident: - tracks span: 1:40-46 - Unary: op: EqSelf expr: Ident: - album_id span: 1:50-58 span: 1:48-58 span: 1:35-59 - FuncCall: name: Ident: - group span: 1:60-65 args: - Tuple: - Ident: - a - album_id span: 1:67-77 - Ident: - a - title span: 1:79-86 span: 1:66-87 - FuncCall: name: Ident: - aggregate span: 1:89-98 args: - Pipeline: exprs: - FuncCall: name: Ident: - sum span: 1:108-111 args: - Ident: - tracks - unit_price span: 1:112-129 span: 1:108-129 - FuncCall: name: Ident: - math - round span: 1:132-142 args: - Literal: Integer: 2 span: 1:143-144 span: 1:132-144 span: 1:108-144 alias: price span: 1:89-145 span: 1:60-146 - FuncCall: name: Ident: - sort span: 1:147-151 args: - Ident: - album_id span: 1:152-160 span: 1:147-160 span: 1:13-160 span: 1:0-160 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__group_sort.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nderive d = album_id + 1\ngroup d (\n aggregate {\n n1 = (track_id | sum),\n }\n)\nsort d\ntake 10\nselect { d1 = d, n1 }\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort.prql --- frames: - - 1:25-48 - columns: - !All input_id: 128 except: [] - !Single name: - d target_id: 130 target_name: null inputs: - id: 128 name: tracks table: - default_db - tracks - - 1:63-111 - columns: - !Single name: - d target_id: 136 target_name: null - !Single name: - n1 target_id: 153 target_name: null inputs: - id: 128 name: tracks table: - default_db - tracks - - 1:114-120 - columns: - !Single name: - d target_id: 136 target_name: null - !Single name: - n1 target_id: 153 target_name: null inputs: - id: 128 name: tracks table: - default_db - tracks - - 1:121-128 - columns: - !Single name: - d target_id: 136 target_name: null - !Single name: - n1 target_id: 153 target_name: null inputs: - id: 128 name: tracks table: - default_db - tracks - - 1:129-150 - columns: - !Single name: - d1 target_id: 166 target_name: null - !Single name: - n1 target_id: 167 target_name: null inputs: - id: 128 name: tracks table: - default_db - tracks nodes: - id: 128 kind: Ident span: 1:13-24 ident: !Ident - default_db - tracks parent: 135 - id: 130 kind: RqOperator span: 1:36-48 alias: d targets: - 132 - 133 parent: 134 - id: 132 kind: Ident span: 1:36-44 ident: !Ident - this - tracks - album_id targets: - 128 - id: 133 kind: Literal span: 1:47-48 - id: 134 kind: Tuple span: 1:36-48 children: - 130 parent: 135 - id: 135 kind: 'TransformCall: Derive' span: 1:25-48 children: - 128 - 134 parent: 157 - id: 136 kind: Ident span: 1:55-56 ident: !Ident - this - d targets: - 130 parent: 139 - id: 139 kind: Tuple span: 1:55-56 children: - 136 parent: 157 - id: 153 kind: RqOperator span: 1:100-103 alias: n1 targets: - 155 parent: 156 - id: 155 kind: Ident span: 1:89-97 ident: !Ident - this - tracks - track_id targets: - 128 - id: 156 kind: Tuple span: 1:73-111 children: - 153 parent: 157 - id: 157 kind: 'TransformCall: Aggregate' span: 1:63-111 children: - 135 - 156 - 139 parent: 162 - id: 160 kind: Ident span: 1:119-120 ident: !Ident - this - d targets: - 136 parent: 162 - id: 162 kind: 'TransformCall: Sort' span: 1:114-120 children: - 157 - 160 parent: 164 - id: 164 kind: 'TransformCall: Take' span: 1:121-128 children: - 162 - 165 parent: 169 - id: 165 kind: Literal parent: 164 - id: 166 kind: Ident span: 1:143-144 alias: d1 ident: !Ident - this - d targets: - 136 parent: 168 - id: 167 kind: Ident span: 1:146-148 ident: !Ident - this - n1 targets: - 153 parent: 168 - id: 168 kind: Tuple span: 1:136-150 children: - 166 - 167 parent: 169 - id: 169 kind: 'TransformCall: Select' span: 1:129-150 children: - 164 - 168 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:13-17 args: - Ident: - tracks span: 1:18-24 span: 1:13-24 - FuncCall: name: Ident: - derive span: 1:25-31 args: - Binary: left: Ident: - album_id span: 1:36-44 op: Add right: Literal: Integer: 1 span: 1:47-48 span: 1:36-48 alias: d span: 1:25-48 - FuncCall: name: Ident: - group span: 1:49-54 args: - Ident: - d span: 1:55-56 - FuncCall: name: Ident: - aggregate span: 1:63-72 args: - Tuple: - Pipeline: exprs: - Ident: - track_id span: 1:89-97 - Ident: - sum span: 1:100-103 span: 1:88-104 alias: n1 span: 1:73-111 span: 1:63-111 span: 1:49-113 - FuncCall: name: Ident: - sort span: 1:114-118 args: - Ident: - d span: 1:119-120 span: 1:114-120 - FuncCall: name: Ident: - take span: 1:121-125 args: - Literal: Integer: 10 span: 1:126-128 span: 1:121-128 - FuncCall: name: Ident: - select span: 1:129-135 args: - Tuple: - Ident: - d span: 1:143-144 alias: d1 - Ident: - n1 span: 1:146-148 span: 1:136-150 span: 1:129-150 span: 1:13-150 span: 1:0-150 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__group_sort_derive_select_join.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "s\"SELECT album_id,title,artist_id FROM albums\"\ngroup {artist_id} (aggregate { album_title_count = count this.`title`})\nsort {this.artist_id, this.album_title_count}\nderive {new_album_count = this.album_title_count}\nselect {this.artist_id, this.new_album_count}\njoin side:left ( s\"SELECT artist_id,name as artist_name FROM artists\" ) (this.artist_id == that.artist_id)\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort_derive_select_join.prql --- frames: - - 1:66-117 - columns: - !Single name: - _literal_127 - artist_id target_id: 128 target_name: null - !Single name: - album_title_count target_id: 147 target_name: null inputs: - id: 127 name: _literal_127 table: - default_db - _literal_127 - - 1:119-164 - columns: - !Single name: - _literal_127 - artist_id target_id: 128 target_name: null - !Single name: - album_title_count target_id: 147 target_name: null inputs: - id: 127 name: _literal_127 table: - default_db - _literal_127 - - 1:165-214 - columns: - !Single name: - _literal_127 - artist_id target_id: 128 target_name: null - !Single name: - album_title_count target_id: 147 target_name: null - !Single name: - new_album_count target_id: 157 target_name: null inputs: - id: 127 name: _literal_127 table: - default_db - _literal_127 - - 1:215-260 - columns: - !Single name: - _literal_127 - artist_id target_id: 160 target_name: null - !Single name: - new_album_count target_id: 161 target_name: null inputs: - id: 127 name: _literal_127 table: - default_db - _literal_127 - - 1:261-367 - columns: - !Single name: - _literal_127 - artist_id target_id: 160 target_name: null - !Single name: - new_album_count target_id: 161 target_name: null - !All input_id: 114 except: [] inputs: - id: 127 name: _literal_127 table: - default_db - _literal_127 - id: 114 name: _literal_114 table: - default_db - _literal_114 nodes: - id: 114 kind: SString span: 1:278-330 parent: 168 - id: 127 kind: SString span: 1:0-46 parent: 150 - id: 128 kind: Ident span: 1:54-63 ident: !Ident - this - _literal_127 - artist_id targets: - 127 parent: 129 - id: 129 kind: Tuple span: 1:53-64 children: - 128 parent: 150 - id: 147 kind: RqOperator span: 1:98-116 alias: album_title_count targets: - 148 parent: 149 - id: 148 kind: Literal - id: 149 kind: Tuple span: 1:76-117 children: - 147 parent: 150 - id: 150 kind: 'TransformCall: Aggregate' span: 1:66-117 children: - 127 - 149 - 129 parent: 156 - id: 153 kind: Ident span: 1:125-139 ident: !Ident - this - _literal_127 - artist_id targets: - 128 parent: 156 - id: 154 kind: Ident span: 1:141-163 ident: !Ident - this - album_title_count targets: - 147 parent: 156 - id: 156 kind: 'TransformCall: Sort' span: 1:119-164 children: - 150 - 153 - 154 parent: 159 - id: 157 kind: Ident span: 1:191-213 alias: new_album_count ident: !Ident - this - album_title_count targets: - 147 parent: 158 - id: 158 kind: Tuple span: 1:172-214 children: - 157 parent: 159 - id: 159 kind: 'TransformCall: Derive' span: 1:165-214 children: - 156 - 158 parent: 163 - id: 160 kind: Ident span: 1:223-237 ident: !Ident - this - _literal_127 - artist_id targets: - 128 parent: 162 - id: 161 kind: Ident span: 1:239-259 ident: !Ident - this - new_album_count targets: - 157 parent: 162 - id: 162 kind: Tuple span: 1:222-260 children: - 160 - 161 parent: 163 - id: 163 kind: 'TransformCall: Select' span: 1:215-260 children: - 159 - 162 parent: 168 - id: 164 kind: RqOperator span: 1:334-366 targets: - 166 - 167 parent: 168 - id: 166 kind: Ident span: 1:334-348 ident: !Ident - this - _literal_127 - artist_id targets: - 160 - id: 167 kind: Ident span: 1:352-366 ident: !Ident - that - _literal_114 - artist_id targets: - 114 - id: 168 kind: 'TransformCall: Join' span: 1:261-367 children: - 163 - 114 - 164 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - SString: - !String SELECT album_id,title,artist_id FROM albums span: 1:0-46 - FuncCall: name: Ident: - group span: 1:47-52 args: - Tuple: - Ident: - artist_id span: 1:54-63 span: 1:53-64 - FuncCall: name: Ident: - aggregate span: 1:66-75 args: - Tuple: - FuncCall: name: Ident: - count span: 1:98-103 args: - Ident: - this - title span: 1:104-116 span: 1:98-116 alias: album_title_count span: 1:76-117 span: 1:66-117 span: 1:47-118 - FuncCall: name: Ident: - sort span: 1:119-123 args: - Tuple: - Ident: - this - artist_id span: 1:125-139 - Ident: - this - album_title_count span: 1:141-163 span: 1:124-164 span: 1:119-164 - FuncCall: name: Ident: - derive span: 1:165-171 args: - Tuple: - Ident: - this - album_title_count span: 1:191-213 alias: new_album_count span: 1:172-214 span: 1:165-214 - FuncCall: name: Ident: - select span: 1:215-221 args: - Tuple: - Ident: - this - artist_id span: 1:223-237 - Ident: - this - new_album_count span: 1:239-259 span: 1:222-260 span: 1:215-260 - FuncCall: name: Ident: - join span: 1:261-265 args: - SString: - !String SELECT artist_id,name as artist_name FROM artists span: 1:278-330 - Binary: left: Ident: - this - artist_id span: 1:334-348 op: Eq right: Ident: - that - artist_id span: 1:352-366 span: 1:334-366 named_args: side: Ident: - left span: 1:271-275 span: 1:261-367 span: 1:0-367 span: 1:0-367 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__group_sort_filter_derive_select_join.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "s\"SELECT album_id,title,artist_id FROM albums\"\ngroup {artist_id} (aggregate { album_title_count = count this.`title`})\nsort {this.artist_id, this.album_title_count}\nfilter (this.album_title_count) > 10\nderive {new_album_count = this.album_title_count}\nselect {this.artist_id, this.new_album_count}\njoin side:left ( s\"SELECT artist_id,name as artist_name FROM artists\" ) (this.artist_id == that.artist_id)\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort_filter_derive_select_join.prql --- frames: - - 1:66-117 - columns: - !Single name: - _literal_130 - artist_id target_id: 131 target_name: null - !Single name: - album_title_count target_id: 150 target_name: null inputs: - id: 130 name: _literal_130 table: - default_db - _literal_130 - - 1:119-164 - columns: - !Single name: - _literal_130 - artist_id target_id: 131 target_name: null - !Single name: - album_title_count target_id: 150 target_name: null inputs: - id: 130 name: _literal_130 table: - default_db - _literal_130 - - 1:165-201 - columns: - !Single name: - _literal_130 - artist_id target_id: 131 target_name: null - !Single name: - album_title_count target_id: 150 target_name: null inputs: - id: 130 name: _literal_130 table: - default_db - _literal_130 - - 1:202-251 - columns: - !Single name: - _literal_130 - artist_id target_id: 131 target_name: null - !Single name: - album_title_count target_id: 150 target_name: null - !Single name: - new_album_count target_id: 165 target_name: null inputs: - id: 130 name: _literal_130 table: - default_db - _literal_130 - - 1:252-297 - columns: - !Single name: - _literal_130 - artist_id target_id: 168 target_name: null - !Single name: - new_album_count target_id: 169 target_name: null inputs: - id: 130 name: _literal_130 table: - default_db - _literal_130 - - 1:298-404 - columns: - !Single name: - _literal_130 - artist_id target_id: 168 target_name: null - !Single name: - new_album_count target_id: 169 target_name: null - !All input_id: 114 except: [] inputs: - id: 130 name: _literal_130 table: - default_db - _literal_130 - id: 114 name: _literal_114 table: - default_db - _literal_114 nodes: - id: 114 kind: SString span: 1:315-367 parent: 176 - id: 130 kind: SString span: 1:0-46 parent: 153 - id: 131 kind: Ident span: 1:54-63 ident: !Ident - this - _literal_130 - artist_id targets: - 130 parent: 132 - id: 132 kind: Tuple span: 1:53-64 children: - 131 parent: 153 - id: 150 kind: RqOperator span: 1:98-116 alias: album_title_count targets: - 151 parent: 152 - id: 151 kind: Literal - id: 152 kind: Tuple span: 1:76-117 children: - 150 parent: 153 - id: 153 kind: 'TransformCall: Aggregate' span: 1:66-117 children: - 130 - 152 - 132 parent: 159 - id: 156 kind: Ident span: 1:125-139 ident: !Ident - this - _literal_130 - artist_id targets: - 131 parent: 159 - id: 157 kind: Ident span: 1:141-163 ident: !Ident - this - album_title_count targets: - 150 parent: 159 - id: 159 kind: 'TransformCall: Sort' span: 1:119-164 children: - 153 - 156 - 157 parent: 164 - id: 160 kind: RqOperator span: 1:172-201 targets: - 162 - 163 parent: 164 - id: 162 kind: Ident span: 1:173-195 ident: !Ident - this - album_title_count targets: - 150 - id: 163 kind: Literal span: 1:199-201 - id: 164 kind: 'TransformCall: Filter' span: 1:165-201 children: - 159 - 160 parent: 167 - id: 165 kind: Ident span: 1:228-250 alias: new_album_count ident: !Ident - this - album_title_count targets: - 150 parent: 166 - id: 166 kind: Tuple span: 1:209-251 children: - 165 parent: 167 - id: 167 kind: 'TransformCall: Derive' span: 1:202-251 children: - 164 - 166 parent: 171 - id: 168 kind: Ident span: 1:260-274 ident: !Ident - this - _literal_130 - artist_id targets: - 131 parent: 170 - id: 169 kind: Ident span: 1:276-296 ident: !Ident - this - new_album_count targets: - 165 parent: 170 - id: 170 kind: Tuple span: 1:259-297 children: - 168 - 169 parent: 171 - id: 171 kind: 'TransformCall: Select' span: 1:252-297 children: - 167 - 170 parent: 176 - id: 172 kind: RqOperator span: 1:371-403 targets: - 174 - 175 parent: 176 - id: 174 kind: Ident span: 1:371-385 ident: !Ident - this - _literal_130 - artist_id targets: - 168 - id: 175 kind: Ident span: 1:389-403 ident: !Ident - that - _literal_114 - artist_id targets: - 114 - id: 176 kind: 'TransformCall: Join' span: 1:298-404 children: - 171 - 114 - 172 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - SString: - !String SELECT album_id,title,artist_id FROM albums span: 1:0-46 - FuncCall: name: Ident: - group span: 1:47-52 args: - Tuple: - Ident: - artist_id span: 1:54-63 span: 1:53-64 - FuncCall: name: Ident: - aggregate span: 1:66-75 args: - Tuple: - FuncCall: name: Ident: - count span: 1:98-103 args: - Ident: - this - title span: 1:104-116 span: 1:98-116 alias: album_title_count span: 1:76-117 span: 1:66-117 span: 1:47-118 - FuncCall: name: Ident: - sort span: 1:119-123 args: - Tuple: - Ident: - this - artist_id span: 1:125-139 - Ident: - this - album_title_count span: 1:141-163 span: 1:124-164 span: 1:119-164 - FuncCall: name: Ident: - filter span: 1:165-171 args: - Binary: left: Ident: - this - album_title_count span: 1:173-195 op: Gt right: Literal: Integer: 10 span: 1:199-201 span: 1:172-201 span: 1:165-201 - FuncCall: name: Ident: - derive span: 1:202-208 args: - Tuple: - Ident: - this - album_title_count span: 1:228-250 alias: new_album_count span: 1:209-251 span: 1:202-251 - FuncCall: name: Ident: - select span: 1:252-258 args: - Tuple: - Ident: - this - artist_id span: 1:260-274 - Ident: - this - new_album_count span: 1:276-296 span: 1:259-297 span: 1:252-297 - FuncCall: name: Ident: - join span: 1:298-302 args: - SString: - !String SELECT artist_id,name as artist_name FROM artists span: 1:315-367 - Binary: left: Ident: - this - artist_id span: 1:371-385 op: Eq right: Ident: - that - artist_id span: 1:389-403 span: 1:371-403 named_args: side: Ident: - left span: 1:308-312 span: 1:298-404 span: 1:0-404 span: 1:0-404 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__group_sort_limit_take.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# Compute the 3 longest songs for each genre and sort by genre\n# mssql:test\nfrom tracks\nselect {genre_id,milliseconds}\ngroup {genre_id} (\n sort {-milliseconds}\n take 3\n)\njoin genres (==genre_id)\nselect {name, milliseconds}\nsort {+name,-milliseconds}\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort_limit_take.prql --- frames: - - 1:88-118 - columns: - !Single name: - tracks - genre_id target_id: 131 target_name: null - !Single name: - tracks - milliseconds target_id: 132 target_name: null inputs: - id: 129 name: tracks table: - default_db - tracks - - 1:163-169 - columns: - !Single name: - tracks - genre_id target_id: 135 target_name: null - !Single name: - tracks - milliseconds target_id: 132 target_name: null inputs: - id: 129 name: tracks table: - default_db - tracks - - 1:172-196 - columns: - !Single name: - tracks - genre_id target_id: 135 target_name: null - !Single name: - tracks - milliseconds target_id: 132 target_name: null - !All input_id: 120 except: [] inputs: - id: 129 name: tracks table: - default_db - tracks - id: 120 name: genres table: - default_db - genres - - 1:197-224 - columns: - !Single name: - genres - name target_id: 173 target_name: null - !Single name: - tracks - milliseconds target_id: 174 target_name: null inputs: - id: 129 name: tracks table: - default_db - tracks - id: 120 name: genres table: - default_db - genres - - 1:225-251 - columns: - !Single name: - genres - name target_id: 173 target_name: null - !Single name: - tracks - milliseconds target_id: 174 target_name: null inputs: - id: 129 name: tracks table: - default_db - tracks - id: 120 name: genres table: - default_db - genres nodes: - id: 120 kind: Ident span: 1:177-183 ident: !Ident - default_db - genres parent: 172 - id: 129 kind: Ident span: 1:76-87 ident: !Ident - default_db - tracks parent: 134 - id: 131 kind: Ident span: 1:96-104 ident: !Ident - this - tracks - genre_id targets: - 129 parent: 133 - id: 132 kind: Ident span: 1:105-117 ident: !Ident - this - tracks - milliseconds targets: - 129 parent: 133 - id: 133 kind: Tuple span: 1:95-118 children: - 131 - 132 parent: 134 - id: 134 kind: 'TransformCall: Select' span: 1:88-118 children: - 129 - 133 parent: 164 - id: 135 kind: Ident span: 1:126-134 ident: !Ident - this - tracks - genre_id targets: - 131 parent: 136 - id: 136 kind: Tuple span: 1:125-135 children: - 135 - id: 160 kind: Ident span: 1:147-159 ident: !Ident - this - tracks - milliseconds targets: - 132 - id: 164 kind: 'TransformCall: Take' span: 1:163-169 children: - 134 - 165 parent: 172 - id: 165 kind: Literal parent: 164 - id: 168 kind: RqOperator span: 1:185-195 targets: - 170 - 171 parent: 172 - id: 170 kind: Ident span: 1:187-195 ident: !Ident - this - tracks - genre_id targets: - 135 - id: 171 kind: Ident span: 1:187-195 ident: !Ident - that - genres - genre_id targets: - 120 - id: 172 kind: 'TransformCall: Join' span: 1:172-196 children: - 164 - 120 - 168 parent: 176 - id: 173 kind: Ident span: 1:205-209 ident: !Ident - this - genres - name targets: - 120 parent: 175 - id: 174 kind: Ident span: 1:211-223 ident: !Ident - this - tracks - milliseconds targets: - 132 parent: 175 - id: 175 kind: Tuple span: 1:204-224 children: - 173 - 174 parent: 176 - id: 176 kind: 'TransformCall: Select' span: 1:197-224 children: - 172 - 175 parent: 182 - id: 177 kind: Ident span: 1:231-236 ident: !Ident - this - genres - name targets: - 173 parent: 182 - id: 180 kind: Ident span: 1:238-250 ident: !Ident - this - tracks - milliseconds targets: - 174 parent: 182 - id: 182 kind: 'TransformCall: Sort' span: 1:225-251 children: - 176 - 177 - 180 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:76-80 args: - Ident: - tracks span: 1:81-87 span: 1:76-87 - FuncCall: name: Ident: - select span: 1:88-94 args: - Tuple: - Ident: - genre_id span: 1:96-104 - Ident: - milliseconds span: 1:105-117 span: 1:95-118 span: 1:88-118 - FuncCall: name: Ident: - group span: 1:119-124 args: - Tuple: - Ident: - genre_id span: 1:126-134 span: 1:125-135 - Pipeline: exprs: - FuncCall: name: Ident: - sort span: 1:140-144 args: - Tuple: - Unary: op: Neg expr: Ident: - milliseconds span: 1:147-159 span: 1:146-159 span: 1:145-160 span: 1:140-160 - FuncCall: name: Ident: - take span: 1:163-167 args: - Literal: Integer: 3 span: 1:168-169 span: 1:163-169 span: 1:140-169 span: 1:119-171 - FuncCall: name: Ident: - join span: 1:172-176 args: - Ident: - genres span: 1:177-183 - Unary: op: EqSelf expr: Ident: - genre_id span: 1:187-195 span: 1:185-195 span: 1:172-196 - FuncCall: name: Ident: - select span: 1:197-203 args: - Tuple: - Ident: - name span: 1:205-209 - Ident: - milliseconds span: 1:211-223 span: 1:204-224 span: 1:197-224 - FuncCall: name: Ident: - sort span: 1:225-229 args: - Tuple: - Unary: op: Add expr: Ident: - name span: 1:232-236 span: 1:231-236 - Unary: op: Neg expr: Ident: - milliseconds span: 1:238-250 span: 1:237-250 span: 1:230-251 span: 1:225-251 span: 1:76-251 span: 1:0-251 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__invoice_totals.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip (clickhouse doesn't have lag function)\n\n#! Calculate a number of metrics about the sales of tracks in each city.\nfrom i=invoices\njoin ii=invoice_items (==invoice_id)\nderive {\n city = i.billing_city,\n street = i.billing_address,\n}\ngroup {city, street} (\n derive total = ii.unit_price * ii.quantity\n aggregate {\n num_orders = count_distinct i.invoice_id,\n num_tracks = sum ii.quantity,\n total_price = sum total,\n }\n)\ngroup {city} (\n sort street\n window expanding:true (\n derive {running_total_num_tracks = sum num_tracks}\n )\n)\nsort {city, street}\nderive {num_tracks_last_week = lag 7 num_tracks}\nselect {\n city,\n street,\n num_orders,\n num_tracks,\n running_total_num_tracks,\n num_tracks_last_week\n}\ntake 20\n" input_file: prqlc/prqlc/tests/integration/queries/invoice_totals.prql --- frames: - - 1:147-183 - columns: - !All input_id: 138 except: [] - !All input_id: 135 except: [] inputs: - id: 138 name: i table: - default_db - invoices - id: 135 name: ii table: - default_db - invoice_items - - 1:184-253 - columns: - !All input_id: 138 except: [] - !All input_id: 135 except: [] - !Single name: - city target_id: 145 target_name: null - !Single name: - street target_id: 146 target_name: null inputs: - id: 138 name: i table: - default_db - invoices - id: 135 name: ii table: - default_db - invoice_items - - 1:281-323 - columns: - !All input_id: 138 except: [] - !All input_id: 135 except: [] - !Single name: - total target_id: 176 target_name: null inputs: - id: 138 name: i table: - default_db - invoices - id: 135 name: ii table: - default_db - invoice_items - - 1:328-466 - columns: - !Single name: - city target_id: 149 target_name: null - !Single name: - street target_id: 150 target_name: null - !Single name: - num_orders target_id: 182 target_name: null - !Single name: - num_tracks target_id: 185 target_name: null - !Single name: - total_price target_id: 188 target_name: null inputs: - id: 138 name: i table: - default_db - invoices - id: 135 name: ii table: - default_db - invoice_items - - 1:536-586 - columns: - !Single name: - city target_id: 195 target_name: null - !Single name: - street target_id: 150 target_name: null - !Single name: - num_orders target_id: 182 target_name: null - !Single name: - num_tracks target_id: 185 target_name: null - !Single name: - total_price target_id: 188 target_name: null - !Single name: - running_total_num_tracks target_id: 241 target_name: null inputs: - id: 138 name: i table: - default_db - invoices - id: 135 name: ii table: - default_db - invoice_items - - 1:595-614 - columns: - !Single name: - city target_id: 195 target_name: null - !Single name: - street target_id: 150 target_name: null - !Single name: - num_orders target_id: 182 target_name: null - !Single name: - num_tracks target_id: 185 target_name: null - !Single name: - total_price target_id: 188 target_name: null - !Single name: - running_total_num_tracks target_id: 241 target_name: null inputs: - id: 138 name: i table: - default_db - invoices - id: 135 name: ii table: - default_db - invoice_items - - 1:615-663 - columns: - !Single name: - city target_id: 195 target_name: null - !Single name: - street target_id: 150 target_name: null - !Single name: - num_orders target_id: 182 target_name: null - !Single name: - num_tracks target_id: 185 target_name: null - !Single name: - total_price target_id: 188 target_name: null - !Single name: - running_total_num_tracks target_id: 241 target_name: null - !Single name: - num_tracks_last_week target_id: 255 target_name: null inputs: - id: 138 name: i table: - default_db - invoices - id: 135 name: ii table: - default_db - invoice_items - - 1:664-783 - columns: - !Single name: - city target_id: 261 target_name: null - !Single name: - street target_id: 262 target_name: null - !Single name: - num_orders target_id: 263 target_name: null - !Single name: - num_tracks target_id: 264 target_name: null - !Single name: - running_total_num_tracks target_id: 265 target_name: null - !Single name: - num_tracks_last_week target_id: 266 target_name: null inputs: - id: 138 name: i table: - default_db - invoices - id: 135 name: ii table: - default_db - invoice_items - - 1:784-791 - columns: - !Single name: - city target_id: 261 target_name: null - !Single name: - street target_id: 262 target_name: null - !Single name: - num_orders target_id: 263 target_name: null - !Single name: - num_tracks target_id: 264 target_name: null - !Single name: - running_total_num_tracks target_id: 265 target_name: null - !Single name: - num_tracks_last_week target_id: 266 target_name: null inputs: - id: 138 name: i table: - default_db - invoices - id: 135 name: ii table: - default_db - invoice_items nodes: - id: 135 kind: Ident span: 1:155-168 ident: !Ident - default_db - invoice_items parent: 144 - id: 138 kind: Ident span: 1:131-146 ident: !Ident - default_db - invoices parent: 144 - id: 140 kind: RqOperator span: 1:170-182 targets: - 142 - 143 parent: 144 - id: 142 kind: Ident span: 1:172-182 ident: !Ident - this - i - invoice_id targets: - 138 - id: 143 kind: Ident span: 1:172-182 ident: !Ident - that - ii - invoice_id targets: - 135 - id: 144 kind: 'TransformCall: Join' span: 1:147-183 children: - 138 - 135 - 140 parent: 148 - id: 145 kind: Ident span: 1:204-218 alias: city ident: !Ident - this - i - billing_city targets: - 138 parent: 147 - id: 146 kind: Ident span: 1:233-250 alias: street ident: !Ident - this - i - billing_address targets: - 138 parent: 147 - id: 147 kind: Tuple span: 1:191-253 children: - 145 - 146 parent: 148 - id: 148 kind: 'TransformCall: Derive' span: 1:184-253 children: - 144 - 147 parent: 181 - id: 149 kind: Ident span: 1:261-265 ident: !Ident - this - city targets: - 145 parent: 151 - id: 150 kind: Ident span: 1:267-273 ident: !Ident - this - street targets: - 146 parent: 151 - id: 151 kind: Tuple span: 1:260-274 children: - 149 - 150 parent: 192 - id: 176 kind: RqOperator span: 1:296-323 alias: total targets: - 178 - 179 parent: 180 - id: 178 kind: Ident span: 1:296-309 ident: !Ident - this - ii - unit_price targets: - 135 - id: 179 kind: Ident span: 1:312-323 ident: !Ident - this - ii - quantity targets: - 135 - id: 180 kind: Tuple span: 1:296-323 children: - 176 parent: 181 - id: 181 kind: 'TransformCall: Derive' span: 1:281-323 children: - 148 - 180 parent: 192 - id: 182 kind: RqOperator span: 1:361-388 alias: num_orders targets: - 184 parent: 191 - id: 184 kind: Ident span: 1:376-388 ident: !Ident - this - i - invoice_id targets: - 138 - id: 185 kind: RqOperator span: 1:411-426 alias: num_tracks targets: - 187 parent: 191 - id: 187 kind: Ident span: 1:415-426 ident: !Ident - this - ii - quantity targets: - 135 - id: 188 kind: RqOperator span: 1:450-459 alias: total_price targets: - 190 parent: 191 - id: 190 kind: Ident span: 1:454-459 ident: !Ident - this - total targets: - 176 - id: 191 kind: Tuple span: 1:338-466 children: - 182 - 185 - 188 parent: 192 - id: 192 kind: 'TransformCall: Aggregate' span: 1:328-466 children: - 181 - 191 - 151 parent: 245 - id: 195 kind: Ident span: 1:476-480 ident: !Ident - this - city targets: - 149 parent: 196 - id: 196 kind: Tuple span: 1:475-481 children: - 195 - id: 220 kind: Ident span: 1:493-499 ident: !Ident - this - street targets: - 150 - id: 241 kind: RqOperator span: 1:571-585 alias: running_total_num_tracks targets: - 243 parent: 244 - id: 243 kind: Ident span: 1:575-585 ident: !Ident - this - num_tracks targets: - 185 - id: 244 kind: Tuple span: 1:543-586 children: - 241 parent: 245 - id: 245 kind: 'TransformCall: Derive' span: 1:536-586 children: - 192 - 244 parent: 254 - id: 247 kind: Literal - id: 251 kind: Ident span: 1:601-605 ident: !Ident - this - city targets: - 195 parent: 254 - id: 252 kind: Ident span: 1:607-613 ident: !Ident - this - street targets: - 150 parent: 254 - id: 254 kind: 'TransformCall: Sort' span: 1:595-614 children: - 245 - 251 - 252 parent: 260 - id: 255 kind: RqOperator span: 1:646-662 alias: num_tracks_last_week targets: - 257 - 258 parent: 259 - id: 257 kind: Literal span: 1:650-651 - id: 258 kind: Ident span: 1:652-662 ident: !Ident - this - num_tracks targets: - 185 - id: 259 kind: Tuple span: 1:622-663 children: - 255 parent: 260 - id: 260 kind: 'TransformCall: Derive' span: 1:615-663 children: - 254 - 259 parent: 268 - id: 261 kind: Ident span: 1:677-681 ident: !Ident - this - city targets: - 195 parent: 267 - id: 262 kind: Ident span: 1:687-693 ident: !Ident - this - street targets: - 150 parent: 267 - id: 263 kind: Ident span: 1:699-709 ident: !Ident - this - num_orders targets: - 182 parent: 267 - id: 264 kind: Ident span: 1:715-725 ident: !Ident - this - num_tracks targets: - 185 parent: 267 - id: 265 kind: Ident span: 1:731-755 ident: !Ident - this - running_total_num_tracks targets: - 241 parent: 267 - id: 266 kind: Ident span: 1:761-781 ident: !Ident - this - num_tracks_last_week targets: - 255 parent: 267 - id: 267 kind: Tuple span: 1:671-783 children: - 261 - 262 - 263 - 264 - 265 - 266 parent: 268 - id: 268 kind: 'TransformCall: Select' span: 1:664-783 children: - 260 - 267 parent: 270 - id: 270 kind: 'TransformCall: Take' span: 1:784-791 children: - 268 - 271 - id: 271 kind: Literal parent: 270 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:131-135 args: - Ident: - invoices span: 1:138-146 alias: i span: 1:131-146 - FuncCall: name: Ident: - join span: 1:147-151 args: - Ident: - invoice_items span: 1:155-168 alias: ii - Unary: op: EqSelf expr: Ident: - invoice_id span: 1:172-182 span: 1:170-182 span: 1:147-183 - FuncCall: name: Ident: - derive span: 1:184-190 args: - Tuple: - Ident: - i - billing_city span: 1:204-218 alias: city - Ident: - i - billing_address span: 1:233-250 alias: street span: 1:191-253 span: 1:184-253 - FuncCall: name: Ident: - group span: 1:254-259 args: - Tuple: - Ident: - city span: 1:261-265 - Ident: - street span: 1:267-273 span: 1:260-274 - Pipeline: exprs: - FuncCall: name: Ident: - derive span: 1:281-287 args: - Binary: left: Ident: - ii - unit_price span: 1:296-309 op: Mul right: Ident: - ii - quantity span: 1:312-323 span: 1:296-323 alias: total span: 1:281-323 - FuncCall: name: Ident: - aggregate span: 1:328-337 args: - Tuple: - FuncCall: name: Ident: - count_distinct span: 1:361-375 args: - Ident: - i - invoice_id span: 1:376-388 span: 1:361-388 alias: num_orders - FuncCall: name: Ident: - sum span: 1:411-414 args: - Ident: - ii - quantity span: 1:415-426 span: 1:411-426 alias: num_tracks - FuncCall: name: Ident: - sum span: 1:450-453 args: - Ident: - total span: 1:454-459 span: 1:450-459 alias: total_price span: 1:338-466 span: 1:328-466 span: 1:281-466 span: 1:254-468 - FuncCall: name: Ident: - group span: 1:469-474 args: - Tuple: - Ident: - city span: 1:476-480 span: 1:475-481 - Pipeline: exprs: - FuncCall: name: Ident: - sort span: 1:488-492 args: - Ident: - street span: 1:493-499 span: 1:488-499 - FuncCall: name: Ident: - window span: 1:504-510 args: - FuncCall: name: Ident: - derive span: 1:536-542 args: - Tuple: - FuncCall: name: Ident: - sum span: 1:571-574 args: - Ident: - num_tracks span: 1:575-585 span: 1:571-585 alias: running_total_num_tracks span: 1:543-586 span: 1:536-586 named_args: expanding: Literal: Boolean: true span: 1:521-525 span: 1:504-592 span: 1:488-592 span: 1:469-594 - FuncCall: name: Ident: - sort span: 1:595-599 args: - Tuple: - Ident: - city span: 1:601-605 - Ident: - street span: 1:607-613 span: 1:600-614 span: 1:595-614 - FuncCall: name: Ident: - derive span: 1:615-621 args: - Tuple: - FuncCall: name: Ident: - lag span: 1:646-649 args: - Literal: Integer: 7 span: 1:650-651 - Ident: - num_tracks span: 1:652-662 span: 1:646-662 alias: num_tracks_last_week span: 1:622-663 span: 1:615-663 - FuncCall: name: Ident: - select span: 1:664-670 args: - Tuple: - Ident: - city span: 1:677-681 - Ident: - street span: 1:687-693 - Ident: - num_orders span: 1:699-709 - Ident: - num_tracks span: 1:715-725 - Ident: - running_total_num_tracks span: 1:731-755 - Ident: - num_tracks_last_week span: 1:761-781 span: 1:671-783 span: 1:664-783 - FuncCall: name: Ident: - take span: 1:784-788 args: - Literal: Integer: 20 span: 1:789-791 span: 1:784-791 span: 1:131-791 span: 1:130-791 doc_comment: ' Calculate a number of metrics about the sales of tracks in each city.' ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__loop_01.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip (DB::Exception: Syntax error)\n# glaredb:skip (DataFusion does not support recursive CTEs https://github.com/apache/arrow-datafusion/issues/462)\nfrom [{n = 1}]\nselect n = n - 2\nloop (filter n < 4 | select n = n + 1)\nselect n = n * 2\nsort n\n" input_file: prqlc/prqlc/tests/integration/queries/loop_01.prql --- frames: - - 1:177-193 - columns: - !Single name: - n target_id: 129 target_name: null inputs: - id: 125 name: _literal_125 table: - default_db - _literal_125 - - 1:200-212 - columns: - !Single name: - n target_id: 129 target_name: null inputs: - id: 125 name: _literal_125 table: - default_db - _literal_125 - - 1:215-231 - columns: - !Single name: - n target_id: 152 target_name: null inputs: - id: 125 name: _literal_125 table: - default_db - _literal_125 - - 1:194-232 - columns: - !Single name: - n target_id: 129 target_name: null inputs: - id: 125 name: _literal_125 table: - default_db - _literal_125 - - 1:233-249 - columns: - !Single name: - n target_id: 160 target_name: null inputs: - id: 125 name: _literal_125 table: - default_db - _literal_125 - - 1:250-256 - columns: - !Single name: - n target_id: 160 target_name: null inputs: - id: 125 name: _literal_125 table: - default_db - _literal_125 nodes: - id: 125 kind: Array span: 1:162-176 children: - 126 parent: 134 - id: 126 kind: Tuple span: 1:168-175 children: - 127 parent: 125 - id: 127 kind: Literal span: 1:173-174 alias: n parent: 126 - id: 129 kind: RqOperator span: 1:188-193 alias: n targets: - 131 - 132 parent: 133 - id: 131 kind: Ident span: 1:188-189 ident: !Ident - this - _literal_125 - n targets: - 125 - id: 132 kind: Literal span: 1:192-193 - id: 133 kind: Tuple span: 1:188-193 children: - 129 parent: 134 - id: 134 kind: 'TransformCall: Select' span: 1:177-193 children: - 125 - 133 parent: 158 - id: 143 kind: Ident ident: !Ident - _param - _tbl targets: - 140 parent: 151 - id: 147 kind: RqOperator span: 1:207-212 targets: - 149 - 150 parent: 151 - id: 149 kind: Ident span: 1:207-208 ident: !Ident - this - n targets: - 129 - id: 150 kind: Literal span: 1:211-212 - id: 151 kind: 'TransformCall: Filter' span: 1:200-212 children: - 143 - 147 parent: 157 - id: 152 kind: RqOperator span: 1:226-231 alias: n targets: - 154 - 155 parent: 156 - id: 154 kind: Ident span: 1:226-227 ident: !Ident - this - n targets: - 129 - id: 155 kind: Literal span: 1:230-231 - id: 156 kind: Tuple span: 1:226-231 children: - 152 parent: 157 - id: 157 kind: 'TransformCall: Select' span: 1:215-231 children: - 151 - 156 - id: 158 kind: 'TransformCall: Loop' span: 1:194-232 children: - 134 - 159 parent: 165 - id: 159 kind: Func span: 1:215-231 parent: 158 - id: 160 kind: RqOperator span: 1:244-249 alias: n targets: - 162 - 163 parent: 164 - id: 162 kind: Ident span: 1:244-245 ident: !Ident - this - n targets: - 129 - id: 163 kind: Literal span: 1:248-249 - id: 164 kind: Tuple span: 1:244-249 children: - 160 parent: 165 - id: 165 kind: 'TransformCall: Select' span: 1:233-249 children: - 158 - 164 parent: 168 - id: 166 kind: Ident span: 1:255-256 ident: !Ident - this - n targets: - 160 parent: 168 - id: 168 kind: 'TransformCall: Sort' span: 1:250-256 children: - 165 - 166 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:162-166 args: - Array: - Tuple: - Literal: Integer: 1 span: 1:173-174 alias: n span: 1:168-175 span: 1:167-176 span: 1:162-176 - FuncCall: name: Ident: - select span: 1:177-183 args: - Binary: left: Ident: - n span: 1:188-189 op: Sub right: Literal: Integer: 2 span: 1:192-193 span: 1:188-193 alias: n span: 1:177-193 - FuncCall: name: Ident: - loop span: 1:194-198 args: - Pipeline: exprs: - FuncCall: name: Ident: - filter span: 1:200-206 args: - Binary: left: Ident: - n span: 1:207-208 op: Lt right: Literal: Integer: 4 span: 1:211-212 span: 1:207-212 span: 1:200-212 - FuncCall: name: Ident: - select span: 1:215-221 args: - Binary: left: Ident: - n span: 1:226-227 op: Add right: Literal: Integer: 1 span: 1:230-231 span: 1:226-231 alias: n span: 1:215-231 span: 1:200-231 span: 1:194-232 - FuncCall: name: Ident: - select span: 1:233-239 args: - Binary: left: Ident: - n span: 1:244-245 op: Mul right: Literal: Integer: 2 span: 1:248-249 span: 1:244-249 alias: n span: 1:233-249 - FuncCall: name: Ident: - sort span: 1:250-254 args: - Ident: - n span: 1:255-256 span: 1:250-256 span: 1:162-256 span: 1:0-256 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__math_module.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\n# sqlite:skip (see https://github.com/rusqlite/rusqlite/issues/1211)\nfrom invoices\ntake 5\nselect {\n total_original = (total | math.round 2),\n total_x = (math.pi - total | math.round 2 | math.abs),\n total_floor = (math.floor total),\n total_ceil = (math.ceil total),\n total_log10 = (math.log10 total | math.round 3),\n total_log2 = (math.log 2 total | math.round 3),\n total_sqrt = (math.sqrt total | math.round 3),\n total_ln = (math.ln total | math.exp | math.round 2),\n total_cos = (math.cos total | math.acos | math.round 2),\n total_sin = (math.sin total | math.asin | math.round 2),\n total_tan = (math.tan total | math.atan | math.round 2),\n total_deg = (total | math.degrees | math.radians | math.round 2),\n total_square = (total | math.pow 2 | math.round 2),\n total_square_op = ((total ** 2) | math.round 2),\n}\n" input_file: prqlc/prqlc/tests/integration/queries/math_module.prql --- frames: - - 1:96-102 - columns: - !All input_id: 119 except: [] inputs: - id: 119 name: invoices table: - default_db - invoices - - 1:103-867 - columns: - !Single name: - total_original target_id: 124 target_name: null - !Single name: - total_x target_id: 129 target_name: null - !Single name: - total_floor target_id: 140 target_name: null - !Single name: - total_ceil target_id: 143 target_name: null - !Single name: - total_log10 target_id: 146 target_name: null - !Single name: - total_log2 target_id: 153 target_name: null - !Single name: - total_sqrt target_id: 161 target_name: null - !Single name: - total_ln target_id: 168 target_name: null - !Single name: - total_cos target_id: 177 target_name: null - !Single name: - total_sin target_id: 186 target_name: null - !Single name: - total_tan target_id: 195 target_name: null - !Single name: - total_deg target_id: 204 target_name: null - !Single name: - total_square target_id: 213 target_name: null - !Single name: - total_square_op target_id: 222 target_name: null inputs: - id: 119 name: invoices table: - default_db - invoices nodes: - id: 119 kind: Ident span: 1:82-95 ident: !Ident - default_db - invoices parent: 122 - id: 122 kind: 'TransformCall: Take' span: 1:96-102 children: - 119 - 123 parent: 231 - id: 123 kind: Literal parent: 122 - id: 124 kind: RqOperator span: 1:142-154 alias: total_original targets: - 127 - 128 parent: 230 - id: 127 kind: Literal span: 1:153-154 - id: 128 kind: Ident span: 1:134-139 ident: !Ident - this - invoices - total targets: - 119 - id: 129 kind: RqOperator span: 1:205-213 alias: total_x targets: - 131 parent: 230 - id: 131 kind: RqOperator span: 1:190-202 targets: - 134 - 135 - id: 134 kind: Literal span: 1:201-202 - id: 135 kind: RqOperator span: 1:172-187 targets: - 138 - 139 - id: 138 kind: RqOperator span: 1:172-179 - id: 139 kind: Ident span: 1:182-187 ident: !Ident - this - invoices - total targets: - 119 - id: 140 kind: RqOperator span: 1:234-252 alias: total_floor targets: - 142 parent: 230 - id: 142 kind: Ident span: 1:246-251 ident: !Ident - this - invoices - total targets: - 119 - id: 143 kind: RqOperator span: 1:271-288 alias: total_ceil targets: - 145 parent: 230 - id: 145 kind: Ident span: 1:282-287 ident: !Ident - this - invoices - total targets: - 119 - id: 146 kind: RqOperator span: 1:328-340 alias: total_log10 targets: - 149 - 150 parent: 230 - id: 149 kind: Literal span: 1:339-340 - id: 150 kind: RqOperator span: 1:309-325 targets: - 152 - id: 152 kind: Ident span: 1:320-325 ident: !Ident - this - invoices - total targets: - 119 - id: 153 kind: RqOperator span: 1:380-392 alias: total_log2 targets: - 156 - 157 parent: 230 - id: 156 kind: Literal span: 1:391-392 - id: 157 kind: RqOperator span: 1:361-377 targets: - 159 - 160 - id: 159 kind: Literal span: 1:370-371 - id: 160 kind: Ident span: 1:372-377 ident: !Ident - this - invoices - total targets: - 119 - id: 161 kind: RqOperator span: 1:431-443 alias: total_sqrt targets: - 164 - 165 parent: 230 - id: 164 kind: Literal span: 1:442-443 - id: 165 kind: RqOperator span: 1:413-428 targets: - 167 - id: 167 kind: Ident span: 1:423-428 ident: !Ident - this - invoices - total targets: - 119 - id: 168 kind: RqOperator span: 1:489-501 alias: total_ln targets: - 171 - 172 parent: 230 - id: 171 kind: Literal span: 1:500-501 - id: 172 kind: RqOperator span: 1:478-486 targets: - 174 - id: 174 kind: RqOperator span: 1:462-475 targets: - 176 - id: 176 kind: Ident span: 1:470-475 ident: !Ident - this - invoices - total targets: - 119 - id: 177 kind: RqOperator span: 1:550-562 alias: total_cos targets: - 180 - 181 parent: 230 - id: 180 kind: Literal span: 1:561-562 - id: 181 kind: RqOperator span: 1:538-547 targets: - 183 - id: 183 kind: RqOperator span: 1:521-535 targets: - 185 - id: 185 kind: Ident span: 1:530-535 ident: !Ident - this - invoices - total targets: - 119 - id: 186 kind: RqOperator span: 1:611-623 alias: total_sin targets: - 189 - 190 parent: 230 - id: 189 kind: Literal span: 1:622-623 - id: 190 kind: RqOperator span: 1:599-608 targets: - 192 - id: 192 kind: RqOperator span: 1:582-596 targets: - 194 - id: 194 kind: Ident span: 1:591-596 ident: !Ident - this - invoices - total targets: - 119 - id: 195 kind: RqOperator span: 1:672-684 alias: total_tan targets: - 198 - 199 parent: 230 - id: 198 kind: Literal span: 1:683-684 - id: 199 kind: RqOperator span: 1:660-669 targets: - 201 - id: 201 kind: RqOperator span: 1:643-657 targets: - 203 - id: 203 kind: Ident span: 1:652-657 ident: !Ident - this - invoices - total targets: - 119 - id: 204 kind: RqOperator span: 1:742-754 alias: total_deg targets: - 207 - 208 parent: 230 - id: 207 kind: Literal span: 1:753-754 - id: 208 kind: RqOperator span: 1:727-739 targets: - 210 - id: 210 kind: RqOperator span: 1:712-724 targets: - 212 - id: 212 kind: Ident span: 1:704-709 ident: !Ident - this - invoices - total targets: - 119 - id: 213 kind: RqOperator span: 1:798-810 alias: total_square targets: - 216 - 217 parent: 230 - id: 216 kind: Literal span: 1:809-810 - id: 217 kind: RqOperator span: 1:785-795 targets: - 220 - 221 - id: 220 kind: Literal span: 1:794-795 - id: 221 kind: Ident span: 1:777-782 ident: !Ident - this - invoices - total targets: - 119 - id: 222 kind: RqOperator span: 1:851-863 alias: total_square_op targets: - 225 - 226 parent: 230 - id: 225 kind: Literal span: 1:862-863 - id: 226 kind: RqOperator span: 1:836-848 targets: - 228 - 229 - id: 228 kind: Literal span: 1:846-847 - id: 229 kind: Ident span: 1:837-842 ident: !Ident - this - invoices - total targets: - 119 - id: 230 kind: Tuple span: 1:110-867 children: - 124 - 129 - 140 - 143 - 146 - 153 - 161 - 168 - 177 - 186 - 195 - 204 - 213 - 222 parent: 231 - id: 231 kind: 'TransformCall: Select' span: 1:103-867 children: - 122 - 230 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:82-86 args: - Ident: - invoices span: 1:87-95 span: 1:82-95 - FuncCall: name: Ident: - take span: 1:96-100 args: - Literal: Integer: 5 span: 1:101-102 span: 1:96-102 - FuncCall: name: Ident: - select span: 1:103-109 args: - Tuple: - Pipeline: exprs: - Ident: - total span: 1:134-139 - FuncCall: name: Ident: - math - round span: 1:142-152 args: - Literal: Integer: 2 span: 1:153-154 span: 1:142-154 span: 1:133-155 alias: total_original - Pipeline: exprs: - Binary: left: Ident: - math - pi span: 1:172-179 op: Sub right: Ident: - total span: 1:182-187 span: 1:172-187 - FuncCall: name: Ident: - math - round span: 1:190-200 args: - Literal: Integer: 2 span: 1:201-202 span: 1:190-202 - Ident: - math - abs span: 1:205-213 span: 1:171-214 alias: total_x - FuncCall: name: Ident: - math - floor span: 1:235-245 args: - Ident: - total span: 1:246-251 span: 1:234-252 alias: total_floor - FuncCall: name: Ident: - math - ceil span: 1:272-281 args: - Ident: - total span: 1:282-287 span: 1:271-288 alias: total_ceil - Pipeline: exprs: - FuncCall: name: Ident: - math - log10 span: 1:309-319 args: - Ident: - total span: 1:320-325 span: 1:309-325 - FuncCall: name: Ident: - math - round span: 1:328-338 args: - Literal: Integer: 3 span: 1:339-340 span: 1:328-340 span: 1:308-341 alias: total_log10 - Pipeline: exprs: - FuncCall: name: Ident: - math - log span: 1:361-369 args: - Literal: Integer: 2 span: 1:370-371 - Ident: - total span: 1:372-377 span: 1:361-377 - FuncCall: name: Ident: - math - round span: 1:380-390 args: - Literal: Integer: 3 span: 1:391-392 span: 1:380-392 span: 1:360-393 alias: total_log2 - Pipeline: exprs: - FuncCall: name: Ident: - math - sqrt span: 1:413-422 args: - Ident: - total span: 1:423-428 span: 1:413-428 - FuncCall: name: Ident: - math - round span: 1:431-441 args: - Literal: Integer: 3 span: 1:442-443 span: 1:431-443 span: 1:412-444 alias: total_sqrt - Pipeline: exprs: - FuncCall: name: Ident: - math - ln span: 1:462-469 args: - Ident: - total span: 1:470-475 span: 1:462-475 - Ident: - math - exp span: 1:478-486 - FuncCall: name: Ident: - math - round span: 1:489-499 args: - Literal: Integer: 2 span: 1:500-501 span: 1:489-501 span: 1:461-502 alias: total_ln - Pipeline: exprs: - FuncCall: name: Ident: - math - cos span: 1:521-529 args: - Ident: - total span: 1:530-535 span: 1:521-535 - Ident: - math - acos span: 1:538-547 - FuncCall: name: Ident: - math - round span: 1:550-560 args: - Literal: Integer: 2 span: 1:561-562 span: 1:550-562 span: 1:520-563 alias: total_cos - Pipeline: exprs: - FuncCall: name: Ident: - math - sin span: 1:582-590 args: - Ident: - total span: 1:591-596 span: 1:582-596 - Ident: - math - asin span: 1:599-608 - FuncCall: name: Ident: - math - round span: 1:611-621 args: - Literal: Integer: 2 span: 1:622-623 span: 1:611-623 span: 1:581-624 alias: total_sin - Pipeline: exprs: - FuncCall: name: Ident: - math - tan span: 1:643-651 args: - Ident: - total span: 1:652-657 span: 1:643-657 - Ident: - math - atan span: 1:660-669 - FuncCall: name: Ident: - math - round span: 1:672-682 args: - Literal: Integer: 2 span: 1:683-684 span: 1:672-684 span: 1:642-685 alias: total_tan - Pipeline: exprs: - Ident: - total span: 1:704-709 - Ident: - math - degrees span: 1:712-724 - Ident: - math - radians span: 1:727-739 - FuncCall: name: Ident: - math - round span: 1:742-752 args: - Literal: Integer: 2 span: 1:753-754 span: 1:742-754 span: 1:703-755 alias: total_deg - Pipeline: exprs: - Ident: - total span: 1:777-782 - FuncCall: name: Ident: - math - pow span: 1:785-793 args: - Literal: Integer: 2 span: 1:794-795 span: 1:785-795 - FuncCall: name: Ident: - math - round span: 1:798-808 args: - Literal: Integer: 2 span: 1:809-810 span: 1:798-810 span: 1:776-811 alias: total_square - Pipeline: exprs: - Binary: left: Ident: - total span: 1:837-842 op: Pow right: Literal: Integer: 2 span: 1:846-847 span: 1:836-848 - FuncCall: name: Ident: - math - round span: 1:851-861 args: - Literal: Integer: 2 span: 1:862-863 span: 1:851-863 span: 1:835-864 alias: total_square_op span: 1:110-867 span: 1:103-867 span: 1:82-867 span: 1:0-867 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__pipelines.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# sqlite:skip (Only works on Sqlite implementations which have the extension\n# installed\n# https://stackoverflow.com/questions/24037982/how-to-use-regexp-in-sqlite)\n\nfrom tracks\n\nfilter (name ~= \"Love\")\nfilter ((milliseconds / 1000 / 60) | in 3..4)\nsort track_id\ntake 1..15\nselect {name, composer}\n" input_file: prqlc/prqlc/tests/integration/queries/pipelines.prql --- frames: - - 1:179-202 - columns: - !All input_id: 128 except: [] inputs: - id: 128 name: tracks table: - default_db - tracks - - 1:203-248 - columns: - !All input_id: 128 except: [] inputs: - id: 128 name: tracks table: - default_db - tracks - - 1:249-262 - columns: - !All input_id: 128 except: [] inputs: - id: 128 name: tracks table: - default_db - tracks - - 1:263-273 - columns: - !All input_id: 128 except: [] inputs: - id: 128 name: tracks table: - default_db - tracks - - 1:274-297 - columns: - !Single name: - tracks - name target_id: 162 target_name: null - !Single name: - tracks - composer target_id: 163 target_name: null inputs: - id: 128 name: tracks table: - default_db - tracks nodes: - id: 128 kind: Ident span: 1:166-177 ident: !Ident - default_db - tracks parent: 134 - id: 130 kind: RqOperator span: 1:187-201 targets: - 132 - 133 parent: 134 - id: 132 kind: Ident span: 1:187-191 ident: !Ident - this - tracks - name targets: - 128 - id: 133 kind: Literal span: 1:195-201 - id: 134 kind: 'TransformCall: Filter' span: 1:179-202 children: - 128 - 130 parent: 154 - id: 138 kind: Literal span: 1:243-244 alias: start - id: 139 kind: Literal span: 1:246-247 alias: end - id: 141 kind: RqOperator span: 1:211-237 targets: - 143 - 147 - id: 143 kind: RqOperator span: 1:212-231 targets: - 145 - 146 - id: 145 kind: Ident span: 1:212-224 ident: !Ident - this - tracks - milliseconds targets: - 128 - id: 146 kind: Literal span: 1:227-231 - id: 147 kind: Literal span: 1:234-236 - id: 148 kind: RqOperator span: 1:240-247 targets: - 150 - 152 parent: 154 - id: 150 kind: RqOperator targets: - 141 - 138 - id: 152 kind: RqOperator targets: - 141 - 139 - id: 154 kind: 'TransformCall: Filter' span: 1:203-248 children: - 134 - 148 parent: 157 - id: 155 kind: Ident span: 1:254-262 ident: !Ident - this - tracks - track_id targets: - 128 parent: 157 - id: 157 kind: 'TransformCall: Sort' span: 1:249-262 children: - 154 - 155 parent: 161 - id: 158 kind: Literal span: 1:268-269 alias: start parent: 161 - id: 159 kind: Literal span: 1:271-273 alias: end parent: 161 - id: 161 kind: 'TransformCall: Take' span: 1:263-273 children: - 157 - 158 - 159 parent: 165 - id: 162 kind: Ident span: 1:282-286 ident: !Ident - this - tracks - name targets: - 128 parent: 164 - id: 163 kind: Ident span: 1:288-296 ident: !Ident - this - tracks - composer targets: - 128 parent: 164 - id: 164 kind: Tuple span: 1:281-297 children: - 162 - 163 parent: 165 - id: 165 kind: 'TransformCall: Select' span: 1:274-297 children: - 161 - 164 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:166-170 args: - Ident: - tracks span: 1:171-177 span: 1:166-177 - FuncCall: name: Ident: - filter span: 1:179-185 args: - Binary: left: Ident: - name span: 1:187-191 op: RegexSearch right: Literal: String: Love span: 1:195-201 span: 1:187-201 span: 1:179-202 - FuncCall: name: Ident: - filter span: 1:203-209 args: - Pipeline: exprs: - Binary: left: Binary: left: Ident: - milliseconds span: 1:212-224 op: DivFloat right: Literal: Integer: 1000 span: 1:227-231 span: 1:212-231 op: DivFloat right: Literal: Integer: 60 span: 1:234-236 span: 1:211-237 - FuncCall: name: Ident: - in span: 1:240-242 args: - Range: start: Literal: Integer: 3 span: 1:243-244 end: Literal: Integer: 4 span: 1:246-247 span: 1:243-247 span: 1:240-247 span: 1:211-247 span: 1:203-248 - FuncCall: name: Ident: - sort span: 1:249-253 args: - Ident: - track_id span: 1:254-262 span: 1:249-262 - FuncCall: name: Ident: - take span: 1:263-267 args: - Range: start: Literal: Integer: 1 span: 1:268-269 end: Literal: Integer: 15 span: 1:271-273 span: 1:268-273 span: 1:263-273 - FuncCall: name: Ident: - select span: 1:274-280 args: - Tuple: - Ident: - name span: 1:282-286 - Ident: - composer span: 1:288-296 span: 1:281-297 span: 1:274-297 span: 1:166-297 span: 1:0-297 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__read_csv.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# sqlite:skip\n# postgres:skip\n# mysql:skip\nfrom (read_csv \"data_file_root/media_types.csv\")\nappend (read_json \"data_file_root/media_types.json\")\nsort media_type_id\n" input_file: prqlc/prqlc/tests/integration/queries/read_csv.prql --- frames: - - 1:92-144 - columns: - !All input_id: 122 except: [] inputs: - id: 122 name: _literal_122 table: - default_db - _literal_122 - id: 117 name: _literal_117 table: - default_db - _literal_117 - - 1:145-163 - columns: - !All input_id: 122 except: [] inputs: - id: 122 name: _literal_122 table: - default_db - _literal_122 - id: 117 name: _literal_117 table: - default_db - _literal_117 nodes: - id: 117 kind: RqOperator span: 1:100-143 targets: - 119 parent: 126 - id: 119 kind: Literal span: 1:110-143 - id: 122 kind: RqOperator span: 1:43-91 targets: - 124 parent: 126 - id: 124 kind: Literal span: 1:58-90 - id: 126 kind: 'TransformCall: Append' span: 1:92-144 children: - 122 - 117 parent: 129 - id: 127 kind: Ident span: 1:150-163 ident: !Ident - this - _literal_122 - media_type_id targets: - 122 parent: 129 - id: 129 kind: 'TransformCall: Sort' span: 1:145-163 children: - 126 - 127 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:43-47 args: - FuncCall: name: Ident: - read_csv span: 1:49-57 args: - Literal: String: data_file_root/media_types.csv span: 1:58-90 span: 1:49-90 span: 1:43-91 - FuncCall: name: Ident: - append span: 1:92-98 args: - FuncCall: name: Ident: - read_json span: 1:100-109 args: - Literal: String: data_file_root/media_types.json span: 1:110-143 span: 1:100-143 span: 1:92-144 - FuncCall: name: Ident: - sort span: 1:145-149 args: - Ident: - media_type_id span: 1:150-163 span: 1:145-163 span: 1:43-163 span: 1:0-163 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__set_ops_remove.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nlet distinct = rel -> (from t = _param.rel | group {t.*} (take 1))\n\nfrom_text format:json '{ \"columns\": [\"a\"], \"data\": [[1], [2], [2], [3]] }'\ndistinct\nremove (from_text format:json '{ \"columns\": [\"a\"], \"data\": [[1], [2]] }')\nsort a\n" input_file: prqlc/prqlc/tests/integration/queries/set_ops_remove.prql --- frames: - - 1:71-77 - columns: - !Single name: - t - a target_id: 134 target_name: null inputs: - id: 125 name: t table: - default_db - _literal_125 - - 0:3163-3240 - columns: - !Single name: - t - a target_id: 134 target_name: null - !Single name: - b - a target_id: 120 target_name: a inputs: - id: 125 name: t table: - default_db - _literal_125 - id: 120 name: b table: - default_db - _literal_120 - - 0:3243-3288 - columns: - !Single name: - t - a target_id: 134 target_name: null - !Single name: - b - a target_id: 120 target_name: a inputs: - id: 125 name: t table: - default_db - _literal_125 - id: 120 name: b table: - default_db - _literal_120 - - 1:165-238 - columns: - !Single name: - t - a target_id: 205 target_name: null inputs: - id: 125 name: t table: - default_db - _literal_125 - id: 120 name: b table: - default_db - _literal_120 - - 1:239-245 - columns: - !Single name: - t - a target_id: 205 target_name: null inputs: - id: 125 name: t table: - default_db - _literal_125 - id: 120 name: b table: - default_db - _literal_120 nodes: - id: 120 kind: Array span: 1:173-237 parent: 187 - id: 125 kind: Array span: 1:36-55 parent: 152 - id: 134 kind: Ident ident: !Ident - this - t - a targets: - 125 parent: 136 - id: 136 kind: Tuple span: 1:64-69 children: - 134 - id: 152 kind: 'TransformCall: Take' span: 1:71-77 children: - 125 - 153 parent: 187 - id: 153 kind: Literal parent: 152 - id: 176 kind: Ident ident: !Ident - this - t - a targets: - 134 - id: 179 kind: Ident ident: !Ident - that - b - a targets: - 120 - id: 185 kind: RqOperator span: 0:3192-3239 targets: - 176 - 179 parent: 187 - id: 187 kind: 'TransformCall: Join' span: 0:3163-3240 children: - 152 - 120 - 185 parent: 203 - id: 195 kind: Ident span: 0:5981-5989 ident: !Ident - this - b - a targets: - 120 - id: 199 kind: RqOperator span: 0:3251-3287 targets: - 195 - 202 parent: 203 - id: 202 kind: Literal span: 0:5993-5997 - id: 203 kind: 'TransformCall: Filter' span: 0:3243-3288 children: - 187 - 199 parent: 207 - id: 205 kind: Ident ident: !Ident - this - t - a targets: - 134 parent: 206 - id: 206 kind: Tuple span: 0:3298-3301 children: - 205 parent: 207 - id: 207 kind: 'TransformCall: Select' span: 1:165-238 children: - 203 - 206 parent: 210 - id: 208 kind: Ident span: 1:244-245 ident: !Ident - this - t - a targets: - 205 parent: 210 - id: 210 kind: 'TransformCall: Sort' span: 1:239-245 children: - 207 - 208 ast: name: Project stmts: - VarDef: kind: Let name: distinct value: Func: return_ty: null body: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:36-40 args: - Ident: - _param - rel span: 1:45-55 alias: t span: 1:36-55 - FuncCall: name: Ident: - group span: 1:58-63 args: - Tuple: - Ident: - t - '*' span: 1:65-68 span: 1:64-69 - FuncCall: name: Ident: - take span: 1:71-75 args: - Literal: Integer: 1 span: 1:76-77 span: 1:71-77 span: 1:58-78 span: 1:35-79 params: - name: rel default_value: null named_params: [] span: 1:28-79 span: 1:0-79 - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from_text span: 1:81-90 args: - Literal: String: '{ "columns": ["a"], "data": [[1], [2], [2], [3]] }' span: 1:103-155 named_args: format: Ident: - json span: 1:98-102 span: 1:81-155 - Ident: - distinct span: 1:156-164 - FuncCall: name: Ident: - remove span: 1:165-171 args: - FuncCall: name: Ident: - from_text span: 1:173-182 args: - Literal: String: '{ "columns": ["a"], "data": [[1], [2]] }' span: 1:195-237 named_args: format: Ident: - json span: 1:190-194 span: 1:173-237 span: 1:165-238 - FuncCall: name: Ident: - sort span: 1:239-243 args: - Ident: - a span: 1:244-245 span: 1:239-245 span: 1:81-245 span: 1:79-245 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__sort.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom e=employees\nfilter first_name != \"Mitchell\"\nsort {first_name, last_name}\n\n# joining may use HashMerge, which can undo ORDER BY\njoin manager=employees side:left (e.reports_to == manager.employee_id)\n\nselect {e.first_name, e.last_name, manager.first_name}\n" input_file: prqlc/prqlc/tests/integration/queries/sort.prql --- frames: - - 1:30-61 - columns: - !All input_id: 126 except: [] inputs: - id: 126 name: e table: - default_db - employees - - 1:62-90 - columns: - !All input_id: 126 except: [] inputs: - id: 126 name: e table: - default_db - employees - - 1:145-215 - columns: - !All input_id: 126 except: [] - !All input_id: 117 except: [] inputs: - id: 126 name: e table: - default_db - employees - id: 117 name: manager table: - default_db - employees - - 1:217-271 - columns: - !Single name: null target_id: 142 target_name: null - !Single name: - e - last_name target_id: 143 target_name: null - !Single name: - manager - first_name target_id: 144 target_name: null inputs: - id: 126 name: e table: - default_db - employees - id: 117 name: manager table: - default_db - employees nodes: - id: 117 kind: Ident span: 1:158-167 ident: !Ident - default_db - employees parent: 141 - id: 126 kind: Ident span: 1:13-29 ident: !Ident - default_db - employees parent: 132 - id: 128 kind: RqOperator span: 1:37-61 targets: - 130 - 131 parent: 132 - id: 130 kind: Ident span: 1:37-47 ident: !Ident - this - e - first_name targets: - 126 - id: 131 kind: Literal span: 1:51-61 - id: 132 kind: 'TransformCall: Filter' span: 1:30-61 children: - 126 - 128 parent: 136 - id: 133 kind: Ident span: 1:68-78 ident: !Ident - this - e - first_name targets: - 126 parent: 136 - id: 134 kind: Ident span: 1:80-89 ident: !Ident - this - e - last_name targets: - 126 parent: 136 - id: 136 kind: 'TransformCall: Sort' span: 1:62-90 children: - 132 - 133 - 134 parent: 141 - id: 137 kind: RqOperator span: 1:179-214 targets: - 139 - 140 parent: 141 - id: 139 kind: Ident span: 1:179-191 ident: !Ident - this - e - reports_to targets: - 126 - id: 140 kind: Ident span: 1:195-214 ident: !Ident - that - manager - employee_id targets: - 117 - id: 141 kind: 'TransformCall: Join' span: 1:145-215 children: - 136 - 117 - 137 parent: 146 - id: 142 kind: Ident span: 1:225-237 ident: !Ident - this - e - first_name targets: - 126 parent: 145 - id: 143 kind: Ident span: 1:239-250 ident: !Ident - this - e - last_name targets: - 126 parent: 145 - id: 144 kind: Ident span: 1:252-270 ident: !Ident - this - manager - first_name targets: - 117 parent: 145 - id: 145 kind: Tuple span: 1:224-271 children: - 142 - 143 - 144 parent: 146 - id: 146 kind: 'TransformCall: Select' span: 1:217-271 children: - 141 - 145 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:13-17 args: - Ident: - employees span: 1:20-29 alias: e span: 1:13-29 - FuncCall: name: Ident: - filter span: 1:30-36 args: - Binary: left: Ident: - first_name span: 1:37-47 op: Ne right: Literal: String: Mitchell span: 1:51-61 span: 1:37-61 span: 1:30-61 - FuncCall: name: Ident: - sort span: 1:62-66 args: - Tuple: - Ident: - first_name span: 1:68-78 - Ident: - last_name span: 1:80-89 span: 1:67-90 span: 1:62-90 - FuncCall: name: Ident: - join span: 1:145-149 args: - Ident: - employees span: 1:158-167 alias: manager - Binary: left: Ident: - e - reports_to span: 1:179-191 op: Eq right: Ident: - manager - employee_id span: 1:195-214 span: 1:179-214 named_args: side: Ident: - left span: 1:173-177 span: 1:145-215 - FuncCall: name: Ident: - select span: 1:217-223 args: - Tuple: - Ident: - e - first_name span: 1:225-237 - Ident: - e - last_name span: 1:239-250 - Ident: - manager - first_name span: 1:252-270 span: 1:224-271 span: 1:217-271 span: 1:13-271 span: 1:0-271 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__sort_2.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from albums\nselect { AA=album_id, artist_id }\nsort AA\nfilter AA >= 25\njoin artists (==artist_id)\n" input_file: prqlc/prqlc/tests/integration/queries/sort_2.prql --- frames: - - 1:12-45 - columns: - !Single name: - AA target_id: 128 target_name: null - !Single name: - albums - artist_id target_id: 129 target_name: null inputs: - id: 126 name: albums table: - default_db - albums - - 1:46-53 - columns: - !Single name: - AA target_id: 128 target_name: null - !Single name: - albums - artist_id target_id: 129 target_name: null inputs: - id: 126 name: albums table: - default_db - albums - - 1:54-69 - columns: - !Single name: - AA target_id: 128 target_name: null - !Single name: - albums - artist_id target_id: 129 target_name: null inputs: - id: 126 name: albums table: - default_db - albums - - 1:70-96 - columns: - !Single name: - AA target_id: 128 target_name: null - !Single name: - albums - artist_id target_id: 129 target_name: null - !All input_id: 114 except: [] inputs: - id: 126 name: albums table: - default_db - albums - id: 114 name: artists table: - default_db - artists nodes: - id: 114 kind: Ident span: 1:75-82 ident: !Ident - default_db - artists parent: 144 - id: 126 kind: Ident span: 1:0-11 ident: !Ident - default_db - albums parent: 131 - id: 128 kind: Ident span: 1:24-32 alias: AA ident: !Ident - this - albums - album_id targets: - 126 parent: 130 - id: 129 kind: Ident span: 1:34-43 ident: !Ident - this - albums - artist_id targets: - 126 parent: 130 - id: 130 kind: Tuple span: 1:19-45 children: - 128 - 129 parent: 131 - id: 131 kind: 'TransformCall: Select' span: 1:12-45 children: - 126 - 130 parent: 134 - id: 132 kind: Ident span: 1:51-53 ident: !Ident - this - AA targets: - 128 parent: 134 - id: 134 kind: 'TransformCall: Sort' span: 1:46-53 children: - 131 - 132 parent: 139 - id: 135 kind: RqOperator span: 1:61-69 targets: - 137 - 138 parent: 139 - id: 137 kind: Ident span: 1:61-63 ident: !Ident - this - AA targets: - 128 - id: 138 kind: Literal span: 1:67-69 - id: 139 kind: 'TransformCall: Filter' span: 1:54-69 children: - 134 - 135 parent: 144 - id: 140 kind: RqOperator span: 1:84-95 targets: - 142 - 143 parent: 144 - id: 142 kind: Ident span: 1:86-95 ident: !Ident - this - albums - artist_id targets: - 129 - id: 143 kind: Ident span: 1:86-95 ident: !Ident - that - artists - artist_id targets: - 114 - id: 144 kind: 'TransformCall: Join' span: 1:70-96 children: - 139 - 114 - 140 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:0-4 args: - Ident: - albums span: 1:5-11 span: 1:0-11 - FuncCall: name: Ident: - select span: 1:12-18 args: - Tuple: - Ident: - album_id span: 1:24-32 alias: AA - Ident: - artist_id span: 1:34-43 span: 1:19-45 span: 1:12-45 - FuncCall: name: Ident: - sort span: 1:46-50 args: - Ident: - AA span: 1:51-53 span: 1:46-53 - FuncCall: name: Ident: - filter span: 1:54-60 args: - Binary: left: Ident: - AA span: 1:61-63 op: Gte right: Literal: Integer: 25 span: 1:67-69 span: 1:61-69 span: 1:54-69 - FuncCall: name: Ident: - join span: 1:70-74 args: - Ident: - artists span: 1:75-82 - Unary: op: EqSelf expr: Ident: - artist_id span: 1:86-95 span: 1:84-95 span: 1:70-96 span: 1:0-96 span: 1:0-96 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__sort_3.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from [{track_id=0, album_id=1, genre_id=2}]\nselect { AA=track_id, album_id, genre_id }\nsort AA\njoin side:left [{album_id=1, album_title=\"Songs\"}] (==album_id)\nselect { AA, AT = album_title ?? \"unknown\", genre_id }\nfilter AA < 25\njoin side:left [{genre_id=1, genre_title=\"Rock\"}] (==genre_id)\nselect { AA, AT, GT = genre_title ?? \"unknown\" }\n" input_file: prqlc/prqlc/tests/integration/queries/sort_3.prql --- frames: - - 1:44-86 - columns: - !Single name: - AA target_id: 148 target_name: null - !Single name: - _literal_142 - album_id target_id: 149 target_name: null - !Single name: - _literal_142 - genre_id target_id: 150 target_name: null inputs: - id: 142 name: _literal_142 table: - default_db - _literal_142 - - 1:87-94 - columns: - !Single name: - AA target_id: 148 target_name: null - !Single name: - _literal_142 - album_id target_id: 149 target_name: null - !Single name: - _literal_142 - genre_id target_id: 150 target_name: null inputs: - id: 142 name: _literal_142 table: - default_db - _literal_142 - - 1:95-158 - columns: - !Single name: - AA target_id: 148 target_name: null - !Single name: - _literal_142 - album_id target_id: 149 target_name: null - !Single name: - _literal_142 - genre_id target_id: 150 target_name: null - !Single name: - _literal_130 - album_id target_id: 130 target_name: album_id - !Single name: - _literal_130 - album_title target_id: 130 target_name: album_title inputs: - id: 142 name: _literal_142 table: - default_db - _literal_142 - id: 130 name: _literal_130 table: - default_db - _literal_130 - - 1:159-213 - columns: - !Single name: - AA target_id: 161 target_name: null - !Single name: - AT target_id: 162 target_name: null - !Single name: - _literal_142 - genre_id target_id: 166 target_name: null inputs: - id: 142 name: _literal_142 table: - default_db - _literal_142 - id: 130 name: _literal_130 table: - default_db - _literal_130 - - 1:214-228 - columns: - !Single name: - AA target_id: 161 target_name: null - !Single name: - AT target_id: 162 target_name: null - !Single name: - _literal_142 - genre_id target_id: 166 target_name: null inputs: - id: 142 name: _literal_142 table: - default_db - _literal_142 - id: 130 name: _literal_130 table: - default_db - _literal_130 - - 1:229-291 - columns: - !Single name: - AA target_id: 161 target_name: null - !Single name: - AT target_id: 162 target_name: null - !Single name: - _literal_142 - genre_id target_id: 166 target_name: null - !Single name: - _literal_117 - genre_id target_id: 117 target_name: genre_id - !Single name: - _literal_117 - genre_title target_id: 117 target_name: genre_title inputs: - id: 142 name: _literal_142 table: - default_db - _literal_142 - id: 130 name: _literal_130 table: - default_db - _literal_130 - id: 117 name: _literal_117 table: - default_db - _literal_117 - - 1:292-340 - columns: - !Single name: - AA target_id: 179 target_name: null - !Single name: - AT target_id: 180 target_name: null - !Single name: - GT target_id: 181 target_name: null inputs: - id: 142 name: _literal_142 table: - default_db - _literal_142 - id: 130 name: _literal_130 table: - default_db - _literal_130 - id: 117 name: _literal_117 table: - default_db - _literal_117 nodes: - id: 117 kind: Array span: 1:244-278 children: - 118 parent: 178 - id: 118 kind: Tuple span: 1:245-277 children: - 119 - 120 parent: 117 - id: 119 kind: Literal span: 1:255-256 alias: genre_id parent: 118 - id: 120 kind: Literal span: 1:270-276 alias: genre_title parent: 118 - id: 130 kind: Array span: 1:110-145 children: - 131 parent: 160 - id: 131 kind: Tuple span: 1:111-144 children: - 132 - 133 parent: 130 - id: 132 kind: Literal span: 1:121-122 alias: album_id parent: 131 - id: 133 kind: Literal span: 1:136-143 alias: album_title parent: 131 - id: 142 kind: Array span: 1:0-43 children: - 143 parent: 152 - id: 143 kind: Tuple span: 1:6-42 children: - 144 - 145 - 146 parent: 142 - id: 144 kind: Literal span: 1:16-17 alias: track_id parent: 143 - id: 145 kind: Literal span: 1:28-29 alias: album_id parent: 143 - id: 146 kind: Literal span: 1:40-41 alias: genre_id parent: 143 - id: 148 kind: Ident span: 1:56-64 alias: AA ident: !Ident - this - _literal_142 - track_id targets: - 142 parent: 151 - id: 149 kind: Ident span: 1:66-74 ident: !Ident - this - _literal_142 - album_id targets: - 142 parent: 151 - id: 150 kind: Ident span: 1:76-84 ident: !Ident - this - _literal_142 - genre_id targets: - 142 parent: 151 - id: 151 kind: Tuple span: 1:51-86 children: - 148 - 149 - 150 parent: 152 - id: 152 kind: 'TransformCall: Select' span: 1:44-86 children: - 142 - 151 parent: 155 - id: 153 kind: Ident span: 1:92-94 ident: !Ident - this - AA targets: - 148 parent: 155 - id: 155 kind: 'TransformCall: Sort' span: 1:87-94 children: - 152 - 153 parent: 160 - id: 156 kind: RqOperator span: 1:147-157 targets: - 158 - 159 parent: 160 - id: 158 kind: Ident span: 1:149-157 ident: !Ident - this - _literal_142 - album_id targets: - 149 - id: 159 kind: Ident span: 1:149-157 ident: !Ident - that - _literal_130 - album_id targets: - 130 - id: 160 kind: 'TransformCall: Join' span: 1:95-158 children: - 155 - 130 - 156 parent: 168 - id: 161 kind: Ident span: 1:168-170 ident: !Ident - this - AA targets: - 148 parent: 167 - id: 162 kind: RqOperator span: 1:177-201 alias: AT targets: - 164 - 165 parent: 167 - id: 164 kind: Ident span: 1:177-188 ident: !Ident - this - _literal_130 - album_title targets: - 130 - id: 165 kind: Literal span: 1:192-201 - id: 166 kind: Ident span: 1:203-211 ident: !Ident - this - _literal_142 - genre_id targets: - 150 parent: 167 - id: 167 kind: Tuple span: 1:166-213 children: - 161 - 162 - 166 parent: 168 - id: 168 kind: 'TransformCall: Select' span: 1:159-213 children: - 160 - 167 parent: 173 - id: 169 kind: RqOperator span: 1:221-228 targets: - 171 - 172 parent: 173 - id: 171 kind: Ident span: 1:221-223 ident: !Ident - this - AA targets: - 161 - id: 172 kind: Literal span: 1:226-228 - id: 173 kind: 'TransformCall: Filter' span: 1:214-228 children: - 168 - 169 parent: 178 - id: 174 kind: RqOperator span: 1:280-290 targets: - 176 - 177 parent: 178 - id: 176 kind: Ident span: 1:282-290 ident: !Ident - this - _literal_142 - genre_id targets: - 166 - id: 177 kind: Ident span: 1:282-290 ident: !Ident - that - _literal_117 - genre_id targets: - 117 - id: 178 kind: 'TransformCall: Join' span: 1:229-291 children: - 173 - 117 - 174 parent: 186 - id: 179 kind: Ident span: 1:301-303 ident: !Ident - this - AA targets: - 161 parent: 185 - id: 180 kind: Ident span: 1:305-307 ident: !Ident - this - AT targets: - 162 parent: 185 - id: 181 kind: RqOperator span: 1:314-338 alias: GT targets: - 183 - 184 parent: 185 - id: 183 kind: Ident span: 1:314-325 ident: !Ident - this - _literal_117 - genre_title targets: - 117 - id: 184 kind: Literal span: 1:329-338 - id: 185 kind: Tuple span: 1:299-340 children: - 179 - 180 - 181 parent: 186 - id: 186 kind: 'TransformCall: Select' span: 1:292-340 children: - 178 - 185 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:0-4 args: - Array: - Tuple: - Literal: Integer: 0 span: 1:16-17 alias: track_id - Literal: Integer: 1 span: 1:28-29 alias: album_id - Literal: Integer: 2 span: 1:40-41 alias: genre_id span: 1:6-42 span: 1:5-43 span: 1:0-43 - FuncCall: name: Ident: - select span: 1:44-50 args: - Tuple: - Ident: - track_id span: 1:56-64 alias: AA - Ident: - album_id span: 1:66-74 - Ident: - genre_id span: 1:76-84 span: 1:51-86 span: 1:44-86 - FuncCall: name: Ident: - sort span: 1:87-91 args: - Ident: - AA span: 1:92-94 span: 1:87-94 - FuncCall: name: Ident: - join span: 1:95-99 args: - Array: - Tuple: - Literal: Integer: 1 span: 1:121-122 alias: album_id - Literal: String: Songs span: 1:136-143 alias: album_title span: 1:111-144 span: 1:110-145 - Unary: op: EqSelf expr: Ident: - album_id span: 1:149-157 span: 1:147-157 named_args: side: Ident: - left span: 1:105-109 span: 1:95-158 - FuncCall: name: Ident: - select span: 1:159-165 args: - Tuple: - Ident: - AA span: 1:168-170 - Binary: left: Ident: - album_title span: 1:177-188 op: Coalesce right: Literal: String: unknown span: 1:192-201 span: 1:177-201 alias: AT - Ident: - genre_id span: 1:203-211 span: 1:166-213 span: 1:159-213 - FuncCall: name: Ident: - filter span: 1:214-220 args: - Binary: left: Ident: - AA span: 1:221-223 op: Lt right: Literal: Integer: 25 span: 1:226-228 span: 1:221-228 span: 1:214-228 - FuncCall: name: Ident: - join span: 1:229-233 args: - Array: - Tuple: - Literal: Integer: 1 span: 1:255-256 alias: genre_id - Literal: String: Rock span: 1:270-276 alias: genre_title span: 1:245-277 span: 1:244-278 - Unary: op: EqSelf expr: Ident: - genre_id span: 1:282-290 span: 1:280-290 named_args: side: Ident: - left span: 1:239-243 span: 1:229-291 - FuncCall: name: Ident: - select span: 1:292-298 args: - Tuple: - Ident: - AA span: 1:301-303 - Ident: - AT span: 1:305-307 - Binary: left: Ident: - genre_title span: 1:314-325 op: Coalesce right: Literal: String: unknown span: 1:329-338 span: 1:314-338 alias: GT span: 1:299-340 span: 1:292-340 span: 1:0-340 span: 1:0-340 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__switch.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# glaredb:skip (May be a bag of String type conversion for Postgres Client)\n# mssql:test\nfrom tracks\nsort milliseconds\nselect display = case [\n composer != null => composer,\n genre_id < 17 => 'no composer',\n true => f'unknown composer'\n]\ntake 10\n" input_file: prqlc/prqlc/tests/integration/queries/switch.prql --- frames: - - 1:101-118 - columns: - !All input_id: 122 except: [] inputs: - id: 122 name: tracks table: - default_db - tracks - - 1:119-246 - columns: - !Single name: - display target_id: 127 target_name: null inputs: - id: 122 name: tracks table: - default_db - tracks - - 1:247-254 - columns: - !Single name: - display target_id: 127 target_name: null inputs: - id: 122 name: tracks table: - default_db - tracks nodes: - id: 122 kind: Ident span: 1:89-100 ident: !Ident - default_db - tracks parent: 126 - id: 124 kind: Ident span: 1:106-118 ident: !Ident - this - tracks - milliseconds targets: - 122 parent: 126 - id: 126 kind: 'TransformCall: Sort' span: 1:101-118 children: - 122 - 124 parent: 141 - id: 127 kind: Case span: 1:136-246 alias: display targets: - 128 - 132 - 133 - 137 - 138 - 139 parent: 140 - id: 128 kind: RqOperator span: 1:147-163 targets: - 130 - 131 - id: 130 kind: Ident span: 1:147-155 ident: !Ident - this - tracks - composer targets: - 122 - id: 131 kind: Literal span: 1:159-163 - id: 132 kind: Ident span: 1:167-175 ident: !Ident - this - tracks - composer targets: - 122 - id: 133 kind: RqOperator span: 1:181-194 targets: - 135 - 136 - id: 135 kind: Ident span: 1:181-189 ident: !Ident - this - tracks - genre_id targets: - 122 - id: 136 kind: Literal span: 1:192-194 - id: 137 kind: Literal span: 1:198-211 - id: 138 kind: Literal span: 1:217-221 - id: 139 kind: FString span: 1:225-244 - id: 140 kind: Tuple span: 1:136-246 children: - 127 parent: 141 - id: 141 kind: 'TransformCall: Select' span: 1:119-246 children: - 126 - 140 parent: 143 - id: 143 kind: 'TransformCall: Take' span: 1:247-254 children: - 141 - 144 - id: 144 kind: Literal parent: 143 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:89-93 args: - Ident: - tracks span: 1:94-100 span: 1:89-100 - FuncCall: name: Ident: - sort span: 1:101-105 args: - Ident: - milliseconds span: 1:106-118 span: 1:101-118 - FuncCall: name: Ident: - select span: 1:119-125 args: - Case: - condition: Binary: left: Ident: - composer span: 1:147-155 op: Ne right: Literal: 'Null' span: 1:159-163 span: 1:147-163 value: Ident: - composer span: 1:167-175 - condition: Binary: left: Ident: - genre_id span: 1:181-189 op: Lt right: Literal: Integer: 17 span: 1:192-194 span: 1:181-194 value: Literal: String: no composer span: 1:198-211 - condition: Literal: Boolean: true span: 1:217-221 value: FString: - !String unknown composer span: 1:225-244 span: 1:136-246 alias: display span: 1:119-246 - FuncCall: name: Ident: - take span: 1:247-251 args: - Literal: Integer: 10 span: 1:252-254 span: 1:247-254 span: 1:89-254 span: 1:0-254 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__take.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nsort {+track_id}\ntake 3..5\n" input_file: prqlc/prqlc/tests/integration/queries/take.prql --- frames: - - 1:25-41 - columns: - !All input_id: 119 except: [] inputs: - id: 119 name: tracks table: - default_db - tracks - - 1:42-51 - columns: - !All input_id: 119 except: [] inputs: - id: 119 name: tracks table: - default_db - tracks nodes: - id: 119 kind: Ident span: 1:13-24 ident: !Ident - default_db - tracks parent: 123 - id: 121 kind: Ident span: 1:31-40 ident: !Ident - this - tracks - track_id targets: - 119 parent: 123 - id: 123 kind: 'TransformCall: Sort' span: 1:25-41 children: - 119 - 121 parent: 127 - id: 124 kind: Literal span: 1:47-48 alias: start parent: 127 - id: 125 kind: Literal span: 1:50-51 alias: end parent: 127 - id: 127 kind: 'TransformCall: Take' span: 1:42-51 children: - 123 - 124 - 125 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:13-17 args: - Ident: - tracks span: 1:18-24 span: 1:13-24 - FuncCall: name: Ident: - sort span: 1:25-29 args: - Tuple: - Unary: op: Add expr: Ident: - track_id span: 1:32-40 span: 1:31-40 span: 1:30-41 span: 1:25-41 - FuncCall: name: Ident: - take span: 1:42-46 args: - Range: start: Literal: Integer: 3 span: 1:47-48 end: Literal: Integer: 5 span: 1:50-51 span: 1:47-51 span: 1:42-51 span: 1:13-51 span: 1:0-51 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__text_module.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\n# glaredb:skip — TODO: started raising an error on 2024-05-20; see `window.prql`\n# for more details\nfrom albums\nselect {\n title,\n title_and_spaces = f\" {title} \",\n low = (title | text.lower),\n up = (title | text.upper),\n ltrimmed = (title | text.ltrim),\n rtrimmed = (title | text.rtrim),\n trimmed = (title | text.trim),\n len = (title | text.length),\n subs = (title | text.extract 2 5),\n replace = (title | text.replace \"al\" \"PIKA\"),\n}\nsort {title}\nfilter (title | text.starts_with \"Black\") || (title | text.contains \"Sabbath\") || (title | text.ends_with \"os\")\n" input_file: prqlc/prqlc/tests/integration/queries/text_module.prql --- frames: - - 1:127-481 - columns: - !Single name: - albums - title target_id: 124 target_name: null - !Single name: - title_and_spaces target_id: 125 target_name: null - !Single name: - low target_id: 127 target_name: null - !Single name: - up target_id: 130 target_name: null - !Single name: - ltrimmed target_id: 133 target_name: null - !Single name: - rtrimmed target_id: 136 target_name: null - !Single name: - trimmed target_id: 139 target_name: null - !Single name: - len target_id: 142 target_name: null - !Single name: - subs target_id: 145 target_name: null - !Single name: - replace target_id: 151 target_name: null inputs: - id: 122 name: albums table: - default_db - albums - - 1:482-494 - columns: - !Single name: - albums - title target_id: 124 target_name: null - !Single name: - title_and_spaces target_id: 125 target_name: null - !Single name: - low target_id: 127 target_name: null - !Single name: - up target_id: 130 target_name: null - !Single name: - ltrimmed target_id: 133 target_name: null - !Single name: - rtrimmed target_id: 136 target_name: null - !Single name: - trimmed target_id: 139 target_name: null - !Single name: - len target_id: 142 target_name: null - !Single name: - subs target_id: 145 target_name: null - !Single name: - replace target_id: 151 target_name: null inputs: - id: 122 name: albums table: - default_db - albums - - 1:495-606 - columns: - !Single name: - albums - title target_id: 124 target_name: null - !Single name: - title_and_spaces target_id: 125 target_name: null - !Single name: - low target_id: 127 target_name: null - !Single name: - up target_id: 130 target_name: null - !Single name: - ltrimmed target_id: 133 target_name: null - !Single name: - rtrimmed target_id: 136 target_name: null - !Single name: - trimmed target_id: 139 target_name: null - !Single name: - len target_id: 142 target_name: null - !Single name: - subs target_id: 145 target_name: null - !Single name: - replace target_id: 151 target_name: null inputs: - id: 122 name: albums table: - default_db - albums nodes: - id: 122 kind: Ident span: 1:115-126 ident: !Ident - default_db - albums parent: 158 - id: 124 kind: Ident span: 1:140-145 ident: !Ident - this - albums - title targets: - 122 parent: 157 - id: 125 kind: FString span: 1:170-184 alias: title_and_spaces targets: - 126 parent: 157 - id: 126 kind: Ident span: 1:175-180 ident: !Ident - this - albums - title targets: - 122 - id: 127 kind: RqOperator span: 1:205-215 alias: low targets: - 129 parent: 157 - id: 129 kind: Ident span: 1:197-202 ident: !Ident - this - albums - title targets: - 122 - id: 130 kind: RqOperator span: 1:236-246 alias: up targets: - 132 parent: 157 - id: 132 kind: Ident span: 1:228-233 ident: !Ident - this - albums - title targets: - 122 - id: 133 kind: RqOperator span: 1:273-283 alias: ltrimmed targets: - 135 parent: 157 - id: 135 kind: Ident span: 1:265-270 ident: !Ident - this - albums - title targets: - 122 - id: 136 kind: RqOperator span: 1:310-320 alias: rtrimmed targets: - 138 parent: 157 - id: 138 kind: Ident span: 1:302-307 ident: !Ident - this - albums - title targets: - 122 - id: 139 kind: RqOperator span: 1:346-355 alias: trimmed targets: - 141 parent: 157 - id: 141 kind: Ident span: 1:338-343 ident: !Ident - this - albums - title targets: - 122 - id: 142 kind: RqOperator span: 1:377-388 alias: len targets: - 144 parent: 157 - id: 144 kind: Ident span: 1:369-374 ident: !Ident - this - albums - title targets: - 122 - id: 145 kind: RqOperator span: 1:411-427 alias: subs targets: - 148 - 149 - 150 parent: 157 - id: 148 kind: Literal span: 1:424-425 - id: 149 kind: Literal span: 1:426-427 - id: 150 kind: Ident span: 1:403-408 ident: !Ident - this - albums - title targets: - 122 - id: 151 kind: RqOperator span: 1:453-477 alias: replace targets: - 154 - 155 - 156 parent: 157 - id: 154 kind: Literal span: 1:466-470 - id: 155 kind: Literal span: 1:471-477 - id: 156 kind: Ident span: 1:445-450 ident: !Ident - this - albums - title targets: - 122 - id: 157 kind: Tuple span: 1:134-481 children: - 124 - 125 - 127 - 130 - 133 - 136 - 139 - 142 - 145 - 151 parent: 158 - id: 158 kind: 'TransformCall: Select' span: 1:127-481 children: - 122 - 157 parent: 161 - id: 159 kind: Ident span: 1:488-493 ident: !Ident - this - albums - title targets: - 124 parent: 161 - id: 161 kind: 'TransformCall: Sort' span: 1:482-494 children: - 158 - 159 parent: 181 - id: 162 kind: RqOperator span: 1:502-606 targets: - 164 - 176 parent: 181 - id: 164 kind: RqOperator span: 1:502-573 targets: - 166 - 171 - id: 166 kind: RqOperator span: 1:511-535 targets: - 169 - 170 - id: 169 kind: Literal span: 1:528-535 - id: 170 kind: Ident span: 1:503-508 ident: !Ident - this - albums - title targets: - 124 - id: 171 kind: RqOperator span: 1:549-572 targets: - 174 - 175 - id: 174 kind: Literal span: 1:563-572 - id: 175 kind: Ident span: 1:541-546 ident: !Ident - this - albums - title targets: - 124 - id: 176 kind: RqOperator span: 1:586-605 targets: - 179 - 180 - id: 179 kind: Literal span: 1:601-605 - id: 180 kind: Ident span: 1:578-583 ident: !Ident - this - albums - title targets: - 124 - id: 181 kind: 'TransformCall: Filter' span: 1:495-606 children: - 161 - 162 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:115-119 args: - Ident: - albums span: 1:120-126 span: 1:115-126 - FuncCall: name: Ident: - select span: 1:127-133 args: - Tuple: - Ident: - title span: 1:140-145 - FString: - !String ' ' - !Expr expr: Ident: - title span: 1:175-180 format: null - !String ' ' span: 1:170-184 alias: title_and_spaces - Pipeline: exprs: - Ident: - title span: 1:197-202 - Ident: - text - lower span: 1:205-215 span: 1:196-216 alias: low - Pipeline: exprs: - Ident: - title span: 1:228-233 - Ident: - text - upper span: 1:236-246 span: 1:227-247 alias: up - Pipeline: exprs: - Ident: - title span: 1:265-270 - Ident: - text - ltrim span: 1:273-283 span: 1:264-284 alias: ltrimmed - Pipeline: exprs: - Ident: - title span: 1:302-307 - Ident: - text - rtrim span: 1:310-320 span: 1:301-321 alias: rtrimmed - Pipeline: exprs: - Ident: - title span: 1:338-343 - Ident: - text - trim span: 1:346-355 span: 1:337-356 alias: trimmed - Pipeline: exprs: - Ident: - title span: 1:369-374 - Ident: - text - length span: 1:377-388 span: 1:368-389 alias: len - Pipeline: exprs: - Ident: - title span: 1:403-408 - FuncCall: name: Ident: - text - extract span: 1:411-423 args: - Literal: Integer: 2 span: 1:424-425 - Literal: Integer: 5 span: 1:426-427 span: 1:411-427 span: 1:402-428 alias: subs - Pipeline: exprs: - Ident: - title span: 1:445-450 - FuncCall: name: Ident: - text - replace span: 1:453-465 args: - Literal: String: al span: 1:466-470 - Literal: String: PIKA span: 1:471-477 span: 1:453-477 span: 1:444-478 alias: replace span: 1:134-481 span: 1:127-481 - FuncCall: name: Ident: - sort span: 1:482-486 args: - Tuple: - Ident: - title span: 1:488-493 span: 1:487-494 span: 1:482-494 - FuncCall: name: Ident: - filter span: 1:495-501 args: - Binary: left: Binary: left: Pipeline: exprs: - Ident: - title span: 1:503-508 - FuncCall: name: Ident: - text - starts_with span: 1:511-527 args: - Literal: String: Black span: 1:528-535 span: 1:511-535 span: 1:503-535 op: Or right: Pipeline: exprs: - Ident: - title span: 1:541-546 - FuncCall: name: Ident: - text - contains span: 1:549-562 args: - Literal: String: Sabbath span: 1:563-572 span: 1:549-572 span: 1:541-572 span: 1:502-573 op: Or right: Pipeline: exprs: - Ident: - title span: 1:578-583 - FuncCall: name: Ident: - text - ends_with span: 1:586-600 args: - Literal: String: os span: 1:601-605 span: 1:586-605 span: 1:578-605 span: 1:502-606 span: 1:495-606 span: 1:115-606 span: 1:0-606 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__debug_lineage__window.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip problems with DISTINCT ON\n# glaredb:skip — TODO: started raising an error on 2024-05-20, from https://github.com/PRQL/prql/actions/runs/9154902656/job/25198160283:\n # ERROR: This feature is not implemented: Unsupported ast node in sqltorel:\n # Substring { expr: Identifier(Ident { value: \"title\", quote_style: None }),\n # substring_from: Some(Value(Number(\"2\", false))), substring_for:\n # Some(Value(Number(\"5\", false))), special: true }\nfrom tracks\ngroup genre_id (\n sort milliseconds\n derive {\n num = row_number this,\n total = count this,\n last_val = last track_id,\n }\n take 10\n)\nsort {genre_id, milliseconds}\nselect {track_id, genre_id, num, total, last_val}\nfilter genre_id >= 22\n" input_file: prqlc/prqlc/tests/integration/queries/window.prql --- frames: - - 1:521-614 - columns: - !All input_id: 125 except: - genre_id - !Single name: - num target_id: 163 target_name: null - !Single name: - total target_id: 171 target_name: null - !Single name: - last_val target_id: 173 target_name: null inputs: - id: 125 name: tracks table: - default_db - tracks - - 1:617-624 - columns: - !Single name: - tracks - genre_id target_id: 127 target_name: null - !All input_id: 125 except: - genre_id - !Single name: - num target_id: 163 target_name: null - !Single name: - total target_id: 171 target_name: null - !Single name: - last_val target_id: 173 target_name: null inputs: - id: 125 name: tracks table: - default_db - tracks - - 1:627-656 - columns: - !Single name: - tracks - genre_id target_id: 127 target_name: null - !All input_id: 125 except: - genre_id - !Single name: - num target_id: 163 target_name: null - !Single name: - total target_id: 171 target_name: null - !Single name: - last_val target_id: 173 target_name: null inputs: - id: 125 name: tracks table: - default_db - tracks - - 1:657-706 - columns: - !Single name: - tracks - track_id target_id: 187 target_name: null - !Single name: - tracks - genre_id target_id: 188 target_name: null - !Single name: - num target_id: 189 target_name: null - !Single name: - total target_id: 190 target_name: null - !Single name: - last_val target_id: 191 target_name: null inputs: - id: 125 name: tracks table: - default_db - tracks - - 1:707-728 - columns: - !Single name: - tracks - track_id target_id: 187 target_name: null - !Single name: - tracks - genre_id target_id: 188 target_name: null - !Single name: - num target_id: 189 target_name: null - !Single name: - total target_id: 190 target_name: null - !Single name: - last_val target_id: 191 target_name: null inputs: - id: 125 name: tracks table: - default_db - tracks nodes: - id: 125 kind: Ident span: 1:470-481 ident: !Ident - default_db - tracks parent: 177 - id: 127 kind: Ident span: 1:488-496 ident: !Ident - this - tracks - genre_id targets: - 125 parent: 136 - id: 136 kind: Tuple span: 1:488-496 children: - 127 - id: 155 kind: Ident span: 1:506-518 ident: !Ident - this - tracks - milliseconds targets: - 125 - id: 163 kind: RqOperator span: 1:540-555 alias: num targets: - 164 parent: 176 - id: 164 kind: Literal - id: 171 kind: RqOperator span: 1:569-579 alias: total targets: - 172 parent: 176 - id: 172 kind: Literal - id: 173 kind: RqOperator span: 1:596-609 alias: last_val targets: - 175 parent: 176 - id: 175 kind: Ident span: 1:601-609 ident: !Ident - this - tracks - track_id targets: - 125 - id: 176 kind: Tuple span: 1:528-614 children: - 163 - 171 - 173 parent: 177 - id: 177 kind: 'TransformCall: Derive' span: 1:521-614 children: - 125 - 176 parent: 179 - id: 179 kind: 'TransformCall: Take' span: 1:617-624 children: - 177 - 180 parent: 186 - id: 180 kind: Literal parent: 179 - id: 183 kind: Ident span: 1:633-641 ident: !Ident - this - tracks - genre_id targets: - 127 parent: 186 - id: 184 kind: Ident span: 1:643-655 ident: !Ident - this - tracks - milliseconds targets: - 125 parent: 186 - id: 186 kind: 'TransformCall: Sort' span: 1:627-656 children: - 179 - 183 - 184 parent: 193 - id: 187 kind: Ident span: 1:665-673 ident: !Ident - this - tracks - track_id targets: - 125 parent: 192 - id: 188 kind: Ident span: 1:675-683 ident: !Ident - this - tracks - genre_id targets: - 127 parent: 192 - id: 189 kind: Ident span: 1:685-688 ident: !Ident - this - num targets: - 163 parent: 192 - id: 190 kind: Ident span: 1:690-695 ident: !Ident - this - total targets: - 171 parent: 192 - id: 191 kind: Ident span: 1:697-705 ident: !Ident - this - last_val targets: - 173 parent: 192 - id: 192 kind: Tuple span: 1:664-706 children: - 187 - 188 - 189 - 190 - 191 parent: 193 - id: 193 kind: 'TransformCall: Select' span: 1:657-706 children: - 186 - 192 parent: 198 - id: 194 kind: RqOperator span: 1:714-728 targets: - 196 - 197 parent: 198 - id: 196 kind: Ident span: 1:714-722 ident: !Ident - this - tracks - genre_id targets: - 188 - id: 197 kind: Literal span: 1:726-728 - id: 198 kind: 'TransformCall: Filter' span: 1:707-728 children: - 193 - 194 ast: name: Project stmts: - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: 1:470-474 args: - Ident: - tracks span: 1:475-481 span: 1:470-481 - FuncCall: name: Ident: - group span: 1:482-487 args: - Ident: - genre_id span: 1:488-496 - Pipeline: exprs: - FuncCall: name: Ident: - sort span: 1:501-505 args: - Ident: - milliseconds span: 1:506-518 span: 1:501-518 - FuncCall: name: Ident: - derive span: 1:521-527 args: - Tuple: - FuncCall: name: Ident: - row_number span: 1:540-550 args: - Ident: - this span: 1:551-555 span: 1:540-555 alias: num - FuncCall: name: Ident: - count span: 1:569-574 args: - Ident: - this span: 1:575-579 span: 1:569-579 alias: total - FuncCall: name: Ident: - last span: 1:596-600 args: - Ident: - track_id span: 1:601-609 span: 1:596-609 alias: last_val span: 1:528-614 span: 1:521-614 - FuncCall: name: Ident: - take span: 1:617-621 args: - Literal: Integer: 10 span: 1:622-624 span: 1:617-624 span: 1:501-624 span: 1:482-626 - FuncCall: name: Ident: - sort span: 1:627-631 args: - Tuple: - Ident: - genre_id span: 1:633-641 - Ident: - milliseconds span: 1:643-655 span: 1:632-656 span: 1:627-656 - FuncCall: name: Ident: - select span: 1:657-663 args: - Tuple: - Ident: - track_id span: 1:665-673 - Ident: - genre_id span: 1:675-683 - Ident: - num span: 1:685-688 - Ident: - total span: 1:690-695 - Ident: - last_val span: 1:697-705 span: 1:664-706 span: 1:657-706 - FuncCall: name: Ident: - filter span: 1:707-713 args: - Binary: left: Ident: - genre_id span: 1:714-722 op: Gte right: Literal: Integer: 22 span: 1:726-728 span: 1:714-728 span: 1:707-728 span: 1:470-728 span: 1:0-728 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__aggregation.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mysql:skip\n# clickhouse:skip\n# glaredb:skip (the string_agg function is not supported)\nfrom tracks\nfilter genre_id == 100\nderive empty_name = name == ''\naggregate {sum track_id, concat_array name, all empty_name, any empty_name}\n" input_file: prqlc/prqlc/tests/integration/queries/aggregation.prql --- from tracks filter genre_id == 100 derive empty_name = name == "" aggregate { sum track_id, concat_array name, all empty_name, any empty_name, } ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__append_select.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nselect { customer_id, invoice_id, billing_country }\ntake 10..15\nappend (\n from invoices\n select { customer_id, invoice_id, billing_country }\n take 40..45\n)\nselect { billing_country, invoice_id }\n" input_file: prqlc/prqlc/tests/integration/queries/append_select.prql --- from invoices select {customer_id, invoice_id, billing_country} take 10..15 append ( from invoices select {customer_id, invoice_id, billing_country} take 40..45 ) select {billing_country, invoice_id} ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__append_select_compute.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nderive total = case [total < 10 => total * 2, true => total]\nselect { customer_id, invoice_id, total }\ntake 5\nappend (\n from invoice_items\n derive unit_price = case [unit_price < 1 => unit_price * 2, true => unit_price]\n select { invoice_line_id, invoice_id, unit_price }\n take 5\n)\nselect { a = customer_id * 2, b = math.round 1 (invoice_id * total) }\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_compute.prql --- from invoices derive total = case [ total < 10 => total * 2, true => total, ] select {customer_id, invoice_id, total} take 5 append ( from invoice_items derive unit_price = case [ unit_price < 1 => unit_price * 2, true => unit_price, ] select {invoice_line_id, invoice_id, unit_price} take 5 ) select { a = customer_id * 2, b = math.round 1 invoice_id * total, } ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__append_select_multiple_with_null.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nselect { customer_id, invoice_id, billing_country }\ntake 5\nappend (\n from employees\n select { employee_id, employee_id, country }\n take 5\n)\nappend (\n from invoice_items\n select { invoice_line_id, invoice_id, null }\n take 5\n)\nselect { billing_country, invoice_id }\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_multiple_with_null.prql --- from invoices select {customer_id, invoice_id, billing_country} take 5 append ( from employees select {employee_id, employee_id, country} take 5 ) append ( from invoice_items select {invoice_line_id, invoice_id, null} take 5 ) select {billing_country, invoice_id} ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__append_select_nulls.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# duckdb:skip\n# postgres:skip\n\nfrom invoices\nselect {an_id = invoice_id, name = null}\ntake 2\nappend (\n from employees\n select {an_id = null, name = first_name}\n take 2\n)\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_nulls.prql --- from invoices select {an_id = invoice_id, name = null} take 2 append ( from employees select {an_id = null, name = first_name} take 2 ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__append_select_simple.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nselect { invoice_id, billing_country }\nappend (\n from invoices\n select { invoice_id = `invoice_id` + 100, billing_country }\n)\nfilter (billing_country | text.starts_with(\"I\"))\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_simple.prql --- from invoices select {invoice_id, billing_country} append ( from invoices select { invoice_id = invoice_id + 100, billing_country, } ) filter (billing_country | text.starts_with "I") ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__arithmetic.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom [\n { id = 1, x_int = 13, x_float = 13.0, k_int = 5, k_float = 5.0 },\n { id = 2, x_int = -13, x_float = -13.0, k_int = 5, k_float = 5.0 },\n { id = 3, x_int = 13, x_float = 13.0, k_int = -5, k_float = -5.0 },\n { id = 4, x_int = -13, x_float = -13.0, k_int = -5, k_float = -5.0 },\n]\nselect {\n id,\n\n x_int / k_int,\n x_int / k_float,\n x_float / k_int,\n x_float / k_float,\n\n q_ii = x_int // k_int,\n q_if = x_int // k_float,\n q_fi = x_float // k_int,\n q_ff = x_float // k_float,\n\n r_ii = x_int % k_int,\n r_if = x_int % k_float,\n r_fi = x_float % k_int,\n r_ff = x_float % k_float,\n\n (q_ii * k_int + r_ii | math.round 0),\n (q_if * k_float + r_if | math.round 0),\n (q_fi * k_int + r_fi | math.round 0),\n (q_ff * k_float + r_ff | math.round 0),\n}\nsort id\n" input_file: prqlc/prqlc/tests/integration/queries/arithmetic.prql --- from [ { id = 1, x_int = 13, x_float = 13, k_int = 5, k_float = 5, }, { id = 2, x_int = -13, x_float = -13, k_int = 5, k_float = 5, }, { id = 3, x_int = 13, x_float = 13, k_int = -5, k_float = -5, }, { id = 4, x_int = -13, x_float = -13, k_int = -5, k_float = -5, }, ] select { id, x_int / k_int, x_int / k_float, x_float / k_int, x_float / k_float, q_ii = x_int // k_int, q_if = x_int // k_float, q_fi = x_float // k_int, q_ff = x_float // k_float, r_ii = x_int % k_int, r_if = x_int % k_float, r_fi = x_float % k_int, r_ff = x_float % k_float, (q_ii * k_int + r_ii | math.round 0), (q_if * k_float + r_if | math.round 0), (q_fi * k_int + r_fi | math.round 0), (q_ff * k_float + r_ff | math.round 0), } sort id ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__cast.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nsort {-bytes}\nselect {\n name,\n bin = ((album_id | as REAL) * 99)\n}\ntake 20\n" input_file: prqlc/prqlc/tests/integration/queries/cast.prql --- from tracks sort {-bytes} select {name, bin = (album_id | as REAL) * 99} take 20 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__constants_only.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from genres\ntake 10\nfilter true\ntake 20\nfilter true\nselect d = 10\n" input_file: prqlc/prqlc/tests/integration/queries/constants_only.prql --- from genres take 10 filter true take 20 filter true select d = 10 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__date_to_text.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# generic:skip\n# glaredb:skip\n# sqlite:skip\n# mssql:test\nfrom invoices\ntake 20\nselect {\n d1 = (invoice_date | date.to_text \"%Y/%m/%d\"),\n d2 = (invoice_date | date.to_text \"%F\"),\n d3 = (invoice_date | date.to_text \"%D\"),\n d4 = (invoice_date | date.to_text \"%H:%M:%S.%f\"),\n d5 = (invoice_date | date.to_text \"%r\"),\n d6 = (invoice_date | date.to_text \"%A %B %-d %Y\"),\n d7 = (invoice_date | date.to_text \"%a, %-d %b %Y at %I:%M:%S %p\"),\n d8 = (invoice_date | date.to_text \"%+\"),\n d9 = (invoice_date | date.to_text \"%-d/%-m/%y\"),\n d10 = (invoice_date | date.to_text \"%-Hh %Mmin\"),\n d11 = (invoice_date | date.to_text \"%M'%S\\\"\"),\n d12 = (invoice_date | date.to_text \"100%% in %d days\"),\n}\n" input_file: prqlc/prqlc/tests/integration/queries/date_to_text.prql --- from invoices take 20 select { d1 = (invoice_date | date.to_text "%Y/%m/%d"), d2 = (invoice_date | date.to_text "%F"), d3 = (invoice_date | date.to_text "%D"), d4 = (invoice_date | date.to_text "%H:%M:%S.%f"), d5 = (invoice_date | date.to_text "%r"), d6 = (invoice_date | date.to_text "%A %B %-d %Y"), d7 = (invoice_date | date.to_text "%a, %-d %b %Y at %I:%M:%S %p"), d8 = (invoice_date | date.to_text "%+"), d9 = (invoice_date | date.to_text "%-d/%-m/%y"), d10 = (invoice_date | date.to_text "%-Hh %Mmin"), d11 = (invoice_date | date.to_text '''%M'%S"'''), d12 = (invoice_date | date.to_text "100%% in %d days"), } ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__distinct.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nselect {album_id, genre_id}\ngroup tracks.* (take 1)\nsort tracks.*\n" input_file: prqlc/prqlc/tests/integration/queries/distinct.prql --- from tracks select {album_id, genre_id} group tracks.`*` (take 1) sort tracks.`*` ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__distinct_on.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nselect {genre_id, media_type_id, album_id}\ngroup {genre_id, media_type_id} (sort {-album_id} | take 1)\nsort {-genre_id, media_type_id}\n" input_file: prqlc/prqlc/tests/integration/queries/distinct_on.prql --- from tracks select {genre_id, media_type_id, album_id} group {genre_id, media_type_id} ( sort {-album_id} take 1 ) sort {-genre_id, media_type_id} ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__genre_counts.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip (ClickHouse prefers aliases to column names https://github.com/PRQL/prql/issues/2827)\n# mssql:test\nlet genre_count = (\n from genres\n aggregate {a = count name}\n)\n\nfrom genre_count\nfilter a > 0\nselect a = -a\n" input_file: prqlc/prqlc/tests/integration/queries/genre_counts.prql --- let genre_count = ( from genres aggregate {a = count name} ) from genre_count filter a > 0 select a = -a ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__group_all.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom a=albums\ntake 10\njoin tracks (==album_id)\ngroup {a.album_id, a.title} (aggregate price = (sum tracks.unit_price | math.round 2))\nsort album_id\n" input_file: prqlc/prqlc/tests/integration/queries/group_all.prql --- from a = albums take 10 join tracks (==album_id) group {a.album_id, a.title} (aggregate price = ( sum tracks.unit_price math.round 2 )) sort album_id ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__group_sort.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nderive d = album_id + 1\ngroup d (\n aggregate {\n n1 = (track_id | sum),\n }\n)\nsort d\ntake 10\nselect { d1 = d, n1 }\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort.prql --- from tracks derive d = album_id + 1 group d (aggregate {n1 = (track_id | sum)}) sort d take 10 select {d1 = d, n1} ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__group_sort_derive_select_join.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "s\"SELECT album_id,title,artist_id FROM albums\"\ngroup {artist_id} (aggregate { album_title_count = count this.`title`})\nsort {this.artist_id, this.album_title_count}\nderive {new_album_count = this.album_title_count}\nselect {this.artist_id, this.new_album_count}\njoin side:left ( s\"SELECT artist_id,name as artist_name FROM artists\" ) (this.artist_id == that.artist_id)\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort_derive_select_join.prql --- s"SELECT album_id,title,artist_id FROM albums" group {artist_id} (aggregate {album_title_count = count this.title}) sort {this.artist_id, this.album_title_count} derive {new_album_count = this.album_title_count} select {this.artist_id, this.new_album_count} join side:left s"SELECT artist_id,name as artist_name FROM artists" this.artist_id == that.artist_id ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__group_sort_filter_derive_select_join.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "s\"SELECT album_id,title,artist_id FROM albums\"\ngroup {artist_id} (aggregate { album_title_count = count this.`title`})\nsort {this.artist_id, this.album_title_count}\nfilter (this.album_title_count) > 10\nderive {new_album_count = this.album_title_count}\nselect {this.artist_id, this.new_album_count}\njoin side:left ( s\"SELECT artist_id,name as artist_name FROM artists\" ) (this.artist_id == that.artist_id)\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort_filter_derive_select_join.prql --- s"SELECT album_id,title,artist_id FROM albums" group {artist_id} (aggregate {album_title_count = count this.title}) sort {this.artist_id, this.album_title_count} filter this.album_title_count > 10 derive {new_album_count = this.album_title_count} select {this.artist_id, this.new_album_count} join side:left s"SELECT artist_id,name as artist_name FROM artists" this.artist_id == that.artist_id ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__group_sort_limit_take.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# Compute the 3 longest songs for each genre and sort by genre\n# mssql:test\nfrom tracks\nselect {genre_id,milliseconds}\ngroup {genre_id} (\n sort {-milliseconds}\n take 3\n)\njoin genres (==genre_id)\nselect {name, milliseconds}\nsort {+name,-milliseconds}\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort_limit_take.prql --- from tracks select {genre_id, milliseconds} group {genre_id} (sort {-milliseconds} | take 3) join genres (==genre_id) select {name, milliseconds} sort {+name, -milliseconds} ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__invoice_totals.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip (clickhouse doesn't have lag function)\n\n#! Calculate a number of metrics about the sales of tracks in each city.\nfrom i=invoices\njoin ii=invoice_items (==invoice_id)\nderive {\n city = i.billing_city,\n street = i.billing_address,\n}\ngroup {city, street} (\n derive total = ii.unit_price * ii.quantity\n aggregate {\n num_orders = count_distinct i.invoice_id,\n num_tracks = sum ii.quantity,\n total_price = sum total,\n }\n)\ngroup {city} (\n sort street\n window expanding:true (\n derive {running_total_num_tracks = sum num_tracks}\n )\n)\nsort {city, street}\nderive {num_tracks_last_week = lag 7 num_tracks}\nselect {\n city,\n street,\n num_orders,\n num_tracks,\n running_total_num_tracks,\n num_tracks_last_week\n}\ntake 20\n" input_file: prqlc/prqlc/tests/integration/queries/invoice_totals.prql --- from i = invoices join ii = invoice_items (==invoice_id) derive { city = i.billing_city, street = i.billing_address, } group {city, street} ( derive total = ii.unit_price * ii.quantity aggregate { num_orders = count_distinct i.invoice_id, num_tracks = sum ii.quantity, total_price = sum total, } ) group {city} ( sort street window expanding:true (derive { running_total_num_tracks = sum num_tracks, }) ) sort {city, street} derive {num_tracks_last_week = lag 7 num_tracks} select { city, street, num_orders, num_tracks, running_total_num_tracks, num_tracks_last_week, } take 20 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__loop_01.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip (DB::Exception: Syntax error)\n# glaredb:skip (DataFusion does not support recursive CTEs https://github.com/apache/arrow-datafusion/issues/462)\nfrom [{n = 1}]\nselect n = n - 2\nloop (filter n < 4 | select n = n + 1)\nselect n = n * 2\nsort n\n" input_file: prqlc/prqlc/tests/integration/queries/loop_01.prql --- from [{n = 1}] select n = n - 2 loop (filter n < 4 | select n = n + 1) select n = n * 2 sort n ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__math_module.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\n# sqlite:skip (see https://github.com/rusqlite/rusqlite/issues/1211)\nfrom invoices\ntake 5\nselect {\n total_original = (total | math.round 2),\n total_x = (math.pi - total | math.round 2 | math.abs),\n total_floor = (math.floor total),\n total_ceil = (math.ceil total),\n total_log10 = (math.log10 total | math.round 3),\n total_log2 = (math.log 2 total | math.round 3),\n total_sqrt = (math.sqrt total | math.round 3),\n total_ln = (math.ln total | math.exp | math.round 2),\n total_cos = (math.cos total | math.acos | math.round 2),\n total_sin = (math.sin total | math.asin | math.round 2),\n total_tan = (math.tan total | math.atan | math.round 2),\n total_deg = (total | math.degrees | math.radians | math.round 2),\n total_square = (total | math.pow 2 | math.round 2),\n total_square_op = ((total ** 2) | math.round 2),\n}\n" input_file: prqlc/prqlc/tests/integration/queries/math_module.prql --- from invoices take 5 select { total_original = (total | math.round 2), total_x = ( math.pi - total math.round 2 math.abs ), total_floor = math.floor total, total_ceil = math.ceil total, total_log10 = (math.log10 total | math.round 3), total_log2 = (math.log 2 total | math.round 3), total_sqrt = (math.sqrt total | math.round 3), total_ln = ( math.ln total math.exp math.round 2 ), total_cos = ( math.cos total math.acos math.round 2 ), total_sin = ( math.sin total math.asin math.round 2 ), total_tan = ( math.tan total math.atan math.round 2 ), total_deg = ( total math.degrees math.radians math.round 2 ), total_square = ( total math.pow 2 math.round 2 ), total_square_op = (total ** 2 | math.round 2), } ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__pipelines.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# sqlite:skip (Only works on Sqlite implementations which have the extension\n# installed\n# https://stackoverflow.com/questions/24037982/how-to-use-regexp-in-sqlite)\n\nfrom tracks\n\nfilter (name ~= \"Love\")\nfilter ((milliseconds / 1000 / 60) | in 3..4)\nsort track_id\ntake 1..15\nselect {name, composer}\n" input_file: prqlc/prqlc/tests/integration/queries/pipelines.prql --- from tracks filter name ~= "Love" filter (milliseconds / 1000 / 60 | in 3..4) sort track_id take 1..15 select {name, composer} ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__read_csv.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# sqlite:skip\n# postgres:skip\n# mysql:skip\nfrom (read_csv \"data_file_root/media_types.csv\")\nappend (read_json \"data_file_root/media_types.json\")\nsort media_type_id\n" input_file: prqlc/prqlc/tests/integration/queries/read_csv.prql --- from (read_csv "data_file_root/media_types.csv") append ( read_json "data_file_root/media_types.json" ) sort media_type_id ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__set_ops_remove.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nlet distinct = rel -> (from t = _param.rel | group {t.*} (take 1))\n\nfrom_text format:json '{ \"columns\": [\"a\"], \"data\": [[1], [2], [2], [3]] }'\ndistinct\nremove (from_text format:json '{ \"columns\": [\"a\"], \"data\": [[1], [2]] }')\nsort a\n" input_file: prqlc/prqlc/tests/integration/queries/set_ops_remove.prql --- let distinct = func rel -> ( from t = _param.rel group {t.`*`} (take 1) ) from_text format:json '{ "columns": ["a"], "data": [[1], [2], [2], [3]] }' distinct remove (from_text format:json '{ "columns": ["a"], "data": [[1], [2]] }') sort a ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__sort.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom e=employees\nfilter first_name != \"Mitchell\"\nsort {first_name, last_name}\n\n# joining may use HashMerge, which can undo ORDER BY\njoin manager=employees side:left (e.reports_to == manager.employee_id)\n\nselect {e.first_name, e.last_name, manager.first_name}\n" input_file: prqlc/prqlc/tests/integration/queries/sort.prql --- from e = employees filter first_name != "Mitchell" sort {first_name, last_name} join side:left manager = employees e.reports_to == manager.employee_id select {e.first_name, e.last_name, manager.first_name} ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__sort_2.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from albums\nselect { AA=album_id, artist_id }\nsort AA\nfilter AA >= 25\njoin artists (==artist_id)\n" input_file: prqlc/prqlc/tests/integration/queries/sort_2.prql --- from albums select {AA = album_id, artist_id} sort AA filter AA >= 25 join artists (==artist_id) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__sort_3.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from [{track_id=0, album_id=1, genre_id=2}]\nselect { AA=track_id, album_id, genre_id }\nsort AA\njoin side:left [{album_id=1, album_title=\"Songs\"}] (==album_id)\nselect { AA, AT = album_title ?? \"unknown\", genre_id }\nfilter AA < 25\njoin side:left [{genre_id=1, genre_title=\"Rock\"}] (==genre_id)\nselect { AA, AT, GT = genre_title ?? \"unknown\" }\n" input_file: prqlc/prqlc/tests/integration/queries/sort_3.prql --- from [{track_id = 0, album_id = 1, genre_id = 2}] select {AA = track_id, album_id, genre_id} sort AA join side:left [ {album_id = 1, album_title = "Songs"}, ] (==album_id) select { AA, AT = album_title ?? "unknown", genre_id, } filter AA < 25 join side:left [ {genre_id = 1, genre_title = "Rock"}, ] (==genre_id) select {AA, AT, GT = genre_title ?? "unknown"} ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__switch.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# glaredb:skip (May be a bag of String type conversion for Postgres Client)\n# mssql:test\nfrom tracks\nsort milliseconds\nselect display = case [\n composer != null => composer,\n genre_id < 17 => 'no composer',\n true => f'unknown composer'\n]\ntake 10\n" input_file: prqlc/prqlc/tests/integration/queries/switch.prql --- from tracks sort milliseconds select display = case [ composer != null => composer, genre_id < 17 => "no composer", true => f"unknown composer", ] take 10 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__take.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nsort {+track_id}\ntake 3..5\n" input_file: prqlc/prqlc/tests/integration/queries/take.prql --- from tracks sort {+track_id} take 3..5 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__text_module.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\n# glaredb:skip — TODO: started raising an error on 2024-05-20; see `window.prql`\n# for more details\nfrom albums\nselect {\n title,\n title_and_spaces = f\" {title} \",\n low = (title | text.lower),\n up = (title | text.upper),\n ltrimmed = (title | text.ltrim),\n rtrimmed = (title | text.rtrim),\n trimmed = (title | text.trim),\n len = (title | text.length),\n subs = (title | text.extract 2 5),\n replace = (title | text.replace \"al\" \"PIKA\"),\n}\nsort {title}\nfilter (title | text.starts_with \"Black\") || (title | text.contains \"Sabbath\") || (title | text.ends_with \"os\")\n" input_file: prqlc/prqlc/tests/integration/queries/text_module.prql --- from albums select { title, title_and_spaces = f" {title} ", low = (title | text.lower), up = (title | text.upper), ltrimmed = (title | text.ltrim), rtrimmed = (title | text.rtrim), trimmed = (title | text.trim), len = (title | text.length), subs = (title | text.extract 2 5), replace = (title | text.replace "al" "PIKA"), } sort {title} filter (title | text.starts_with "Black") || ( title text.contains "Sabbath" ) || (title | text.ends_with "os") ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__fmt__window.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip problems with DISTINCT ON\n# glaredb:skip — TODO: started raising an error on 2024-05-20, from https://github.com/PRQL/prql/actions/runs/9154902656/job/25198160283:\n # ERROR: This feature is not implemented: Unsupported ast node in sqltorel:\n # Substring { expr: Identifier(Ident { value: \"title\", quote_style: None }),\n # substring_from: Some(Value(Number(\"2\", false))), substring_for:\n # Some(Value(Number(\"5\", false))), special: true }\nfrom tracks\ngroup genre_id (\n sort milliseconds\n derive {\n num = row_number this,\n total = count this,\n last_val = last track_id,\n }\n take 10\n)\nsort {genre_id, milliseconds}\nselect {track_id, genre_id, num, total, last_val}\nfilter genre_id >= 22\n" input_file: prqlc/prqlc/tests/integration/queries/window.prql --- from tracks group genre_id ( sort milliseconds derive { num = row_number this, total = count this, last_val = last track_id, } take 10 ) sort {genre_id, milliseconds} select {track_id, genre_id, num, total, last_val} filter genre_id >= 22 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__aggregation.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/aggregation.prql --- Tokens( [ 0..0: Start, 0..12: Comment(" mysql:skip"), 12..13: NewLine, 13..30: Comment(" clickhouse:skip"), 30..31: NewLine, 31..88: Comment(" glaredb:skip (the string_agg function is not supported)"), 88..89: NewLine, 89..93: Ident("from"), 94..100: Ident("tracks"), 100..101: NewLine, 101..107: Ident("filter"), 108..116: Ident("genre_id"), 117..119: Eq, 120..123: Literal(Integer(100)), 123..124: NewLine, 124..130: Ident("derive"), 131..141: Ident("empty_name"), 142..143: Control('='), 144..148: Ident("name"), 149..151: Eq, 152..154: Literal(String("")), 154..155: NewLine, 155..164: Ident("aggregate"), 165..166: Control('{'), 166..169: Ident("sum"), 170..178: Ident("track_id"), 178..179: Control(','), 180..192: Ident("concat_array"), 193..197: Ident("name"), 197..198: Control(','), 199..202: Ident("all"), 203..213: Ident("empty_name"), 213..214: Control(','), 215..218: Ident("any"), 219..229: Ident("empty_name"), 229..230: Control('}'), 230..231: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__append_select.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/append_select.prql --- Tokens( [ 0..0: Start, 0..4: Ident("from"), 5..13: Ident("invoices"), 13..14: NewLine, 14..20: Ident("select"), 21..22: Control('{'), 23..34: Ident("customer_id"), 34..35: Control(','), 36..46: Ident("invoice_id"), 46..47: Control(','), 48..63: Ident("billing_country"), 64..65: Control('}'), 65..66: NewLine, 66..70: Ident("take"), 71..73: Literal(Integer(10)), 73..75: Range { bind_left: true, bind_right: true }, 75..77: Literal(Integer(15)), 77..78: NewLine, 78..84: Ident("append"), 85..86: Control('('), 86..87: NewLine, 89..93: Ident("from"), 94..102: Ident("invoices"), 102..103: NewLine, 105..111: Ident("select"), 112..113: Control('{'), 114..125: Ident("customer_id"), 125..126: Control(','), 127..137: Ident("invoice_id"), 137..138: Control(','), 139..154: Ident("billing_country"), 155..156: Control('}'), 156..157: NewLine, 159..163: Ident("take"), 164..166: Literal(Integer(40)), 166..168: Range { bind_left: true, bind_right: true }, 168..170: Literal(Integer(45)), 170..171: NewLine, 171..172: Control(')'), 172..173: NewLine, 173..179: Ident("select"), 180..181: Control('{'), 182..197: Ident("billing_country"), 197..198: Control(','), 199..209: Ident("invoice_id"), 210..211: Control('}'), 211..212: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__append_select_compute.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/append_select_compute.prql --- Tokens( [ 0..0: Start, 0..4: Ident("from"), 5..13: Ident("invoices"), 13..14: NewLine, 14..20: Ident("derive"), 21..26: Ident("total"), 27..28: Control('='), 29..33: Keyword("case"), 34..35: Control('['), 35..40: Ident("total"), 41..42: Control('<'), 43..45: Literal(Integer(10)), 46..48: ArrowFat, 49..54: Ident("total"), 55..56: Control('*'), 57..58: Literal(Integer(2)), 58..59: Control(','), 60..64: Literal(Boolean(true)), 65..67: ArrowFat, 68..73: Ident("total"), 73..74: Control(']'), 74..75: NewLine, 75..81: Ident("select"), 82..83: Control('{'), 84..95: Ident("customer_id"), 95..96: Control(','), 97..107: Ident("invoice_id"), 107..108: Control(','), 109..114: Ident("total"), 115..116: Control('}'), 116..117: NewLine, 117..121: Ident("take"), 122..123: Literal(Integer(5)), 123..124: NewLine, 124..130: Ident("append"), 131..132: Control('('), 132..133: NewLine, 135..139: Ident("from"), 140..153: Ident("invoice_items"), 153..154: NewLine, 156..162: Ident("derive"), 163..173: Ident("unit_price"), 174..175: Control('='), 176..180: Keyword("case"), 181..182: Control('['), 182..192: Ident("unit_price"), 193..194: Control('<'), 195..196: Literal(Integer(1)), 197..199: ArrowFat, 200..210: Ident("unit_price"), 211..212: Control('*'), 213..214: Literal(Integer(2)), 214..215: Control(','), 216..220: Literal(Boolean(true)), 221..223: ArrowFat, 224..234: Ident("unit_price"), 234..235: Control(']'), 235..236: NewLine, 238..244: Ident("select"), 245..246: Control('{'), 247..262: Ident("invoice_line_id"), 262..263: Control(','), 264..274: Ident("invoice_id"), 274..275: Control(','), 276..286: Ident("unit_price"), 287..288: Control('}'), 288..289: NewLine, 291..295: Ident("take"), 296..297: Literal(Integer(5)), 297..298: NewLine, 298..299: Control(')'), 299..300: NewLine, 300..306: Ident("select"), 307..308: Control('{'), 309..310: Ident("a"), 311..312: Control('='), 313..324: Ident("customer_id"), 325..326: Control('*'), 327..328: Literal(Integer(2)), 328..329: Control(','), 330..331: Ident("b"), 332..333: Control('='), 334..338: Ident("math"), 338..339: Control('.'), 339..344: Ident("round"), 345..346: Literal(Integer(1)), 347..348: Control('('), 348..358: Ident("invoice_id"), 359..360: Control('*'), 361..366: Ident("total"), 366..367: Control(')'), 368..369: Control('}'), 369..370: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__append_select_multiple_with_null.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/append_select_multiple_with_null.prql --- Tokens( [ 0..0: Start, 0..4: Ident("from"), 5..13: Ident("invoices"), 13..14: NewLine, 14..20: Ident("select"), 21..22: Control('{'), 23..34: Ident("customer_id"), 34..35: Control(','), 36..46: Ident("invoice_id"), 46..47: Control(','), 48..63: Ident("billing_country"), 64..65: Control('}'), 65..66: NewLine, 66..70: Ident("take"), 71..72: Literal(Integer(5)), 72..73: NewLine, 73..79: Ident("append"), 80..81: Control('('), 81..82: NewLine, 84..88: Ident("from"), 89..98: Ident("employees"), 98..99: NewLine, 101..107: Ident("select"), 108..109: Control('{'), 110..121: Ident("employee_id"), 121..122: Control(','), 123..134: Ident("employee_id"), 134..135: Control(','), 136..143: Ident("country"), 144..145: Control('}'), 145..146: NewLine, 148..152: Ident("take"), 153..154: Literal(Integer(5)), 154..155: NewLine, 155..156: Control(')'), 156..157: NewLine, 157..163: Ident("append"), 164..165: Control('('), 165..166: NewLine, 168..172: Ident("from"), 173..186: Ident("invoice_items"), 186..187: NewLine, 189..195: Ident("select"), 196..197: Control('{'), 198..213: Ident("invoice_line_id"), 213..214: Control(','), 215..225: Ident("invoice_id"), 225..226: Control(','), 227..231: Literal(Null), 232..233: Control('}'), 233..234: NewLine, 236..240: Ident("take"), 241..242: Literal(Integer(5)), 242..243: NewLine, 243..244: Control(')'), 244..245: NewLine, 245..251: Ident("select"), 252..253: Control('{'), 254..269: Ident("billing_country"), 269..270: Control(','), 271..281: Ident("invoice_id"), 282..283: Control('}'), 283..284: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__append_select_nulls.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/append_select_nulls.prql --- Tokens( [ 0..0: Start, 0..4: Ident("from"), 5..13: Ident("invoices"), 13..14: NewLine, 14..20: Ident("select"), 21..22: Control('{'), 22..27: Ident("an_id"), 28..29: Control('='), 30..40: Ident("invoice_id"), 40..41: Control(','), 42..46: Ident("name"), 47..48: Control('='), 49..53: Literal(Null), 53..54: Control('}'), 54..55: NewLine, 55..59: Ident("take"), 60..61: Literal(Integer(2)), 61..62: NewLine, 62..68: Ident("append"), 69..70: Control('('), 70..71: NewLine, 73..77: Ident("from"), 78..87: Ident("employees"), 87..88: NewLine, 90..96: Ident("select"), 97..98: Control('{'), 98..103: Ident("an_id"), 104..105: Control('='), 106..110: Literal(Null), 110..111: Control(','), 112..116: Ident("name"), 117..118: Control('='), 119..129: Ident("first_name"), 129..130: Control('}'), 130..131: NewLine, 133..137: Ident("take"), 138..139: Literal(Integer(2)), 139..140: NewLine, 140..141: Control(')'), 141..142: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__append_select_simple.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/append_select_simple.prql --- Tokens( [ 0..0: Start, 0..4: Ident("from"), 5..13: Ident("invoices"), 13..14: NewLine, 14..20: Ident("select"), 21..22: Control('{'), 23..33: Ident("invoice_id"), 33..34: Control(','), 35..50: Ident("billing_country"), 51..52: Control('}'), 52..53: NewLine, 53..59: Ident("append"), 60..61: Control('('), 61..62: NewLine, 64..68: Ident("from"), 69..77: Ident("invoices"), 77..78: NewLine, 80..86: Ident("select"), 87..88: Control('{'), 89..99: Ident("invoice_id"), 100..101: Control('='), 102..114: Ident("invoice_id"), 115..116: Control('+'), 117..120: Literal(Integer(100)), 120..121: Control(','), 122..137: Ident("billing_country"), 138..139: Control('}'), 139..140: NewLine, 140..141: Control(')'), 141..142: NewLine, 142..148: Ident("filter"), 149..150: Control('('), 150..165: Ident("billing_country"), 166..167: Control('|'), 168..172: Ident("text"), 172..173: Control('.'), 173..184: Ident("starts_with"), 184..185: Control('('), 185..188: Literal(String("I")), 188..189: Control(')'), 189..190: Control(')'), 190..191: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__arithmetic.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/arithmetic.prql --- Tokens( [ 0..0: Start, 0..12: Comment(" mssql:test"), 12..13: NewLine, 13..17: Ident("from"), 18..19: Control('['), 19..20: NewLine, 24..25: Control('{'), 26..28: Ident("id"), 29..30: Control('='), 31..32: Literal(Integer(1)), 32..33: Control(','), 34..39: Ident("x_int"), 40..41: Control('='), 43..45: Literal(Integer(13)), 45..46: Control(','), 47..54: Ident("x_float"), 55..56: Control('='), 58..62: Literal(Float(13.0)), 62..63: Control(','), 64..69: Ident("k_int"), 70..71: Control('='), 73..74: Literal(Integer(5)), 74..75: Control(','), 76..83: Ident("k_float"), 84..85: Control('='), 87..90: Literal(Float(5.0)), 91..92: Control('}'), 92..93: Control(','), 93..94: NewLine, 98..99: Control('{'), 100..102: Ident("id"), 103..104: Control('='), 105..106: Literal(Integer(2)), 106..107: Control(','), 108..113: Ident("x_int"), 114..115: Control('='), 116..117: Control('-'), 117..119: Literal(Integer(13)), 119..120: Control(','), 121..128: Ident("x_float"), 129..130: Control('='), 131..132: Control('-'), 132..136: Literal(Float(13.0)), 136..137: Control(','), 138..143: Ident("k_int"), 144..145: Control('='), 147..148: Literal(Integer(5)), 148..149: Control(','), 150..157: Ident("k_float"), 158..159: Control('='), 161..164: Literal(Float(5.0)), 165..166: Control('}'), 166..167: Control(','), 167..168: NewLine, 172..173: Control('{'), 174..176: Ident("id"), 177..178: Control('='), 179..180: Literal(Integer(3)), 180..181: Control(','), 182..187: Ident("x_int"), 188..189: Control('='), 191..193: Literal(Integer(13)), 193..194: Control(','), 195..202: Ident("x_float"), 203..204: Control('='), 206..210: Literal(Float(13.0)), 210..211: Control(','), 212..217: Ident("k_int"), 218..219: Control('='), 220..221: Control('-'), 221..222: Literal(Integer(5)), 222..223: Control(','), 224..231: Ident("k_float"), 232..233: Control('='), 234..235: Control('-'), 235..238: Literal(Float(5.0)), 239..240: Control('}'), 240..241: Control(','), 241..242: NewLine, 246..247: Control('{'), 248..250: Ident("id"), 251..252: Control('='), 253..254: Literal(Integer(4)), 254..255: Control(','), 256..261: Ident("x_int"), 262..263: Control('='), 264..265: Control('-'), 265..267: Literal(Integer(13)), 267..268: Control(','), 269..276: Ident("x_float"), 277..278: Control('='), 279..280: Control('-'), 280..284: Literal(Float(13.0)), 284..285: Control(','), 286..291: Ident("k_int"), 292..293: Control('='), 294..295: Control('-'), 295..296: Literal(Integer(5)), 296..297: Control(','), 298..305: Ident("k_float"), 306..307: Control('='), 308..309: Control('-'), 309..312: Literal(Float(5.0)), 313..314: Control('}'), 314..315: Control(','), 315..316: NewLine, 316..317: Control(']'), 317..318: NewLine, 318..324: Ident("select"), 325..326: Control('{'), 326..327: NewLine, 331..333: Ident("id"), 333..334: Control(','), 334..335: NewLine, 335..336: NewLine, 340..345: Ident("x_int"), 346..347: Control('/'), 348..353: Ident("k_int"), 353..354: Control(','), 354..355: NewLine, 359..364: Ident("x_int"), 365..366: Control('/'), 367..374: Ident("k_float"), 374..375: Control(','), 375..376: NewLine, 380..387: Ident("x_float"), 388..389: Control('/'), 390..395: Ident("k_int"), 395..396: Control(','), 396..397: NewLine, 401..408: Ident("x_float"), 409..410: Control('/'), 411..418: Ident("k_float"), 418..419: Control(','), 419..420: NewLine, 420..421: NewLine, 425..429: Ident("q_ii"), 430..431: Control('='), 432..437: Ident("x_int"), 438..440: DivInt, 441..446: Ident("k_int"), 446..447: Control(','), 447..448: NewLine, 452..456: Ident("q_if"), 457..458: Control('='), 459..464: Ident("x_int"), 465..467: DivInt, 468..475: Ident("k_float"), 475..476: Control(','), 476..477: NewLine, 481..485: Ident("q_fi"), 486..487: Control('='), 488..495: Ident("x_float"), 496..498: DivInt, 499..504: Ident("k_int"), 504..505: Control(','), 505..506: NewLine, 510..514: Ident("q_ff"), 515..516: Control('='), 517..524: Ident("x_float"), 525..527: DivInt, 528..535: Ident("k_float"), 535..536: Control(','), 536..537: NewLine, 537..538: NewLine, 542..546: Ident("r_ii"), 547..548: Control('='), 549..554: Ident("x_int"), 555..556: Control('%'), 557..562: Ident("k_int"), 562..563: Control(','), 563..564: NewLine, 568..572: Ident("r_if"), 573..574: Control('='), 575..580: Ident("x_int"), 581..582: Control('%'), 583..590: Ident("k_float"), 590..591: Control(','), 591..592: NewLine, 596..600: Ident("r_fi"), 601..602: Control('='), 603..610: Ident("x_float"), 611..612: Control('%'), 613..618: Ident("k_int"), 618..619: Control(','), 619..620: NewLine, 624..628: Ident("r_ff"), 629..630: Control('='), 631..638: Ident("x_float"), 639..640: Control('%'), 641..648: Ident("k_float"), 648..649: Control(','), 649..650: NewLine, 650..651: NewLine, 655..656: Control('('), 656..660: Ident("q_ii"), 661..662: Control('*'), 663..668: Ident("k_int"), 669..670: Control('+'), 671..675: Ident("r_ii"), 676..677: Control('|'), 678..682: Ident("math"), 682..683: Control('.'), 683..688: Ident("round"), 689..690: Literal(Integer(0)), 690..691: Control(')'), 691..692: Control(','), 692..693: NewLine, 697..698: Control('('), 698..702: Ident("q_if"), 703..704: Control('*'), 705..712: Ident("k_float"), 713..714: Control('+'), 715..719: Ident("r_if"), 720..721: Control('|'), 722..726: Ident("math"), 726..727: Control('.'), 727..732: Ident("round"), 733..734: Literal(Integer(0)), 734..735: Control(')'), 735..736: Control(','), 736..737: NewLine, 741..742: Control('('), 742..746: Ident("q_fi"), 747..748: Control('*'), 749..754: Ident("k_int"), 755..756: Control('+'), 757..761: Ident("r_fi"), 762..763: Control('|'), 764..768: Ident("math"), 768..769: Control('.'), 769..774: Ident("round"), 775..776: Literal(Integer(0)), 776..777: Control(')'), 777..778: Control(','), 778..779: NewLine, 783..784: Control('('), 784..788: Ident("q_ff"), 789..790: Control('*'), 791..798: Ident("k_float"), 799..800: Control('+'), 801..805: Ident("r_ff"), 806..807: Control('|'), 808..812: Ident("math"), 812..813: Control('.'), 813..818: Ident("round"), 819..820: Literal(Integer(0)), 820..821: Control(')'), 821..822: Control(','), 822..823: NewLine, 823..824: Control('}'), 824..825: NewLine, 825..829: Ident("sort"), 830..832: Ident("id"), 832..833: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__cast.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/cast.prql --- Tokens( [ 0..0: Start, 0..12: Comment(" mssql:test"), 12..13: NewLine, 13..17: Ident("from"), 18..24: Ident("tracks"), 24..25: NewLine, 25..29: Ident("sort"), 30..31: Control('{'), 31..32: Control('-'), 32..37: Ident("bytes"), 37..38: Control('}'), 38..39: NewLine, 39..45: Ident("select"), 46..47: Control('{'), 47..48: NewLine, 52..56: Ident("name"), 56..57: Control(','), 57..58: NewLine, 62..65: Ident("bin"), 66..67: Control('='), 68..69: Control('('), 69..70: Control('('), 70..78: Ident("album_id"), 79..80: Control('|'), 81..83: Ident("as"), 84..88: Ident("REAL"), 88..89: Control(')'), 90..91: Control('*'), 92..94: Literal(Integer(99)), 94..95: Control(')'), 95..96: NewLine, 96..97: Control('}'), 97..98: NewLine, 98..102: Ident("take"), 103..105: Literal(Integer(20)), 105..106: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__constants_only.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/constants_only.prql --- Tokens( [ 0..0: Start, 0..4: Ident("from"), 5..11: Ident("genres"), 11..12: NewLine, 12..16: Ident("take"), 17..19: Literal(Integer(10)), 19..20: NewLine, 20..26: Ident("filter"), 27..31: Literal(Boolean(true)), 31..32: NewLine, 32..36: Ident("take"), 37..39: Literal(Integer(20)), 39..40: NewLine, 40..46: Ident("filter"), 47..51: Literal(Boolean(true)), 51..52: NewLine, 52..58: Ident("select"), 59..60: Ident("d"), 61..62: Control('='), 63..65: Literal(Integer(10)), 65..66: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__date_to_text.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/date_to_text.prql --- Tokens( [ 0..0: Start, 0..14: Comment(" generic:skip"), 14..15: NewLine, 15..29: Comment(" glaredb:skip"), 29..30: NewLine, 30..43: Comment(" sqlite:skip"), 43..44: NewLine, 44..56: Comment(" mssql:test"), 56..57: NewLine, 57..61: Ident("from"), 62..70: Ident("invoices"), 70..71: NewLine, 71..75: Ident("take"), 76..78: Literal(Integer(20)), 78..79: NewLine, 79..85: Ident("select"), 86..87: Control('{'), 87..88: NewLine, 92..94: Ident("d1"), 95..96: Control('='), 97..98: Control('('), 98..110: Ident("invoice_date"), 111..112: Control('|'), 113..117: Ident("date"), 117..118: Control('.'), 118..125: Ident("to_text"), 126..136: Literal(String("%Y/%m/%d")), 136..137: Control(')'), 137..138: Control(','), 138..139: NewLine, 143..145: Ident("d2"), 146..147: Control('='), 148..149: Control('('), 149..161: Ident("invoice_date"), 162..163: Control('|'), 164..168: Ident("date"), 168..169: Control('.'), 169..176: Ident("to_text"), 177..181: Literal(String("%F")), 181..182: Control(')'), 182..183: Control(','), 183..184: NewLine, 188..190: Ident("d3"), 191..192: Control('='), 193..194: Control('('), 194..206: Ident("invoice_date"), 207..208: Control('|'), 209..213: Ident("date"), 213..214: Control('.'), 214..221: Ident("to_text"), 222..226: Literal(String("%D")), 226..227: Control(')'), 227..228: Control(','), 228..229: NewLine, 233..235: Ident("d4"), 236..237: Control('='), 238..239: Control('('), 239..251: Ident("invoice_date"), 252..253: Control('|'), 254..258: Ident("date"), 258..259: Control('.'), 259..266: Ident("to_text"), 267..280: Literal(String("%H:%M:%S.%f")), 280..281: Control(')'), 281..282: Control(','), 282..283: NewLine, 287..289: Ident("d5"), 290..291: Control('='), 292..293: Control('('), 293..305: Ident("invoice_date"), 306..307: Control('|'), 308..312: Ident("date"), 312..313: Control('.'), 313..320: Ident("to_text"), 321..325: Literal(String("%r")), 325..326: Control(')'), 326..327: Control(','), 327..328: NewLine, 332..334: Ident("d6"), 335..336: Control('='), 337..338: Control('('), 338..350: Ident("invoice_date"), 351..352: Control('|'), 353..357: Ident("date"), 357..358: Control('.'), 358..365: Ident("to_text"), 366..380: Literal(String("%A %B %-d %Y")), 380..381: Control(')'), 381..382: Control(','), 382..383: NewLine, 387..389: Ident("d7"), 390..391: Control('='), 392..393: Control('('), 393..405: Ident("invoice_date"), 406..407: Control('|'), 408..412: Ident("date"), 412..413: Control('.'), 413..420: Ident("to_text"), 421..451: Literal(String("%a, %-d %b %Y at %I:%M:%S %p")), 451..452: Control(')'), 452..453: Control(','), 453..454: NewLine, 458..460: Ident("d8"), 461..462: Control('='), 463..464: Control('('), 464..476: Ident("invoice_date"), 477..478: Control('|'), 479..483: Ident("date"), 483..484: Control('.'), 484..491: Ident("to_text"), 492..496: Literal(String("%+")), 496..497: Control(')'), 497..498: Control(','), 498..499: NewLine, 503..505: Ident("d9"), 506..507: Control('='), 508..509: Control('('), 509..521: Ident("invoice_date"), 522..523: Control('|'), 524..528: Ident("date"), 528..529: Control('.'), 529..536: Ident("to_text"), 537..549: Literal(String("%-d/%-m/%y")), 549..550: Control(')'), 550..551: Control(','), 551..552: NewLine, 556..559: Ident("d10"), 560..561: Control('='), 562..563: Control('('), 563..575: Ident("invoice_date"), 576..577: Control('|'), 578..582: Ident("date"), 582..583: Control('.'), 583..590: Ident("to_text"), 591..603: Literal(String("%-Hh %Mmin")), 603..604: Control(')'), 604..605: Control(','), 605..606: NewLine, 610..613: Ident("d11"), 614..615: Control('='), 616..617: Control('('), 617..629: Ident("invoice_date"), 630..631: Control('|'), 632..636: Ident("date"), 636..637: Control('.'), 637..644: Ident("to_text"), 645..654: Literal(String("%M'%S\"")), 654..655: Control(')'), 655..656: Control(','), 656..657: NewLine, 661..664: Ident("d12"), 665..666: Control('='), 667..668: Control('('), 668..680: Ident("invoice_date"), 681..682: Control('|'), 683..687: Ident("date"), 687..688: Control('.'), 688..695: Ident("to_text"), 696..714: Literal(String("100%% in %d days")), 714..715: Control(')'), 715..716: Control(','), 716..717: NewLine, 717..718: Control('}'), 718..719: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__distinct.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/distinct.prql --- Tokens( [ 0..0: Start, 0..12: Comment(" mssql:test"), 12..13: NewLine, 13..17: Ident("from"), 18..24: Ident("tracks"), 24..25: NewLine, 25..31: Ident("select"), 32..33: Control('{'), 33..41: Ident("album_id"), 41..42: Control(','), 43..51: Ident("genre_id"), 51..52: Control('}'), 52..53: NewLine, 53..58: Ident("group"), 59..65: Ident("tracks"), 65..66: Control('.'), 66..67: Control('*'), 68..69: Control('('), 69..73: Ident("take"), 74..75: Literal(Integer(1)), 75..76: Control(')'), 76..77: NewLine, 77..81: Ident("sort"), 82..88: Ident("tracks"), 88..89: Control('.'), 89..90: Control('*'), 90..91: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__distinct_on.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/distinct_on.prql --- Tokens( [ 0..0: Start, 0..12: Comment(" mssql:test"), 12..13: NewLine, 13..17: Ident("from"), 18..24: Ident("tracks"), 24..25: NewLine, 25..31: Ident("select"), 32..33: Control('{'), 33..41: Ident("genre_id"), 41..42: Control(','), 43..56: Ident("media_type_id"), 56..57: Control(','), 58..66: Ident("album_id"), 66..67: Control('}'), 67..68: NewLine, 68..73: Ident("group"), 74..75: Control('{'), 75..83: Ident("genre_id"), 83..84: Control(','), 85..98: Ident("media_type_id"), 98..99: Control('}'), 100..101: Control('('), 101..105: Ident("sort"), 106..107: Control('{'), 107..108: Control('-'), 108..116: Ident("album_id"), 116..117: Control('}'), 118..119: Control('|'), 120..124: Ident("take"), 125..126: Literal(Integer(1)), 126..127: Control(')'), 127..128: NewLine, 128..132: Ident("sort"), 133..134: Control('{'), 134..135: Control('-'), 135..143: Ident("genre_id"), 143..144: Control(','), 145..158: Ident("media_type_id"), 158..159: Control('}'), 159..160: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__genre_counts.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/genre_counts.prql --- Tokens( [ 0..0: Start, 0..103: Comment(" clickhouse:skip (ClickHouse prefers aliases to column names https://github.com/PRQL/prql/issues/2827)"), 103..104: NewLine, 104..116: Comment(" mssql:test"), 116..117: NewLine, 117..120: Keyword("let"), 121..132: Ident("genre_count"), 133..134: Control('='), 135..136: Control('('), 136..137: NewLine, 141..145: Ident("from"), 146..152: Ident("genres"), 152..153: NewLine, 157..166: Ident("aggregate"), 167..168: Control('{'), 168..169: Ident("a"), 170..171: Control('='), 172..177: Ident("count"), 178..182: Ident("name"), 182..183: Control('}'), 183..184: NewLine, 184..185: Control(')'), 185..186: NewLine, 186..187: NewLine, 187..191: Ident("from"), 192..203: Ident("genre_count"), 203..204: NewLine, 204..210: Ident("filter"), 211..212: Ident("a"), 213..214: Control('>'), 215..216: Literal(Integer(0)), 216..217: NewLine, 217..223: Ident("select"), 224..225: Ident("a"), 226..227: Control('='), 228..229: Control('-'), 229..230: Ident("a"), 230..231: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__group_all.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/group_all.prql --- Tokens( [ 0..0: Start, 0..12: Comment(" mssql:test"), 12..13: NewLine, 13..17: Ident("from"), 18..19: Ident("a"), 19..20: Control('='), 20..26: Ident("albums"), 26..27: NewLine, 27..31: Ident("take"), 32..34: Literal(Integer(10)), 34..35: NewLine, 35..39: Ident("join"), 40..46: Ident("tracks"), 47..48: Control('('), 48..50: Eq, 50..58: Ident("album_id"), 58..59: Control(')'), 59..60: NewLine, 60..65: Ident("group"), 66..67: Control('{'), 67..68: Ident("a"), 68..69: Control('.'), 69..77: Ident("album_id"), 77..78: Control(','), 79..80: Ident("a"), 80..81: Control('.'), 81..86: Ident("title"), 86..87: Control('}'), 88..89: Control('('), 89..98: Ident("aggregate"), 99..104: Ident("price"), 105..106: Control('='), 107..108: Control('('), 108..111: Ident("sum"), 112..118: Ident("tracks"), 118..119: Control('.'), 119..129: Ident("unit_price"), 130..131: Control('|'), 132..136: Ident("math"), 136..137: Control('.'), 137..142: Ident("round"), 143..144: Literal(Integer(2)), 144..145: Control(')'), 145..146: Control(')'), 146..147: NewLine, 147..151: Ident("sort"), 152..160: Ident("album_id"), 160..161: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__group_sort.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/group_sort.prql --- Tokens( [ 0..0: Start, 0..12: Comment(" mssql:test"), 12..13: NewLine, 13..17: Ident("from"), 18..24: Ident("tracks"), 24..25: NewLine, 25..31: Ident("derive"), 32..33: Ident("d"), 34..35: Control('='), 36..44: Ident("album_id"), 45..46: Control('+'), 47..48: Literal(Integer(1)), 48..49: NewLine, 49..54: Ident("group"), 55..56: Ident("d"), 57..58: Control('('), 58..59: NewLine, 63..72: Ident("aggregate"), 73..74: Control('{'), 74..75: NewLine, 83..85: Ident("n1"), 86..87: Control('='), 88..89: Control('('), 89..97: Ident("track_id"), 98..99: Control('|'), 100..103: Ident("sum"), 103..104: Control(')'), 104..105: Control(','), 105..106: NewLine, 110..111: Control('}'), 111..112: NewLine, 112..113: Control(')'), 113..114: NewLine, 114..118: Ident("sort"), 119..120: Ident("d"), 120..121: NewLine, 121..125: Ident("take"), 126..128: Literal(Integer(10)), 128..129: NewLine, 129..135: Ident("select"), 136..137: Control('{'), 138..140: Ident("d1"), 141..142: Control('='), 143..144: Ident("d"), 144..145: Control(','), 146..148: Ident("n1"), 149..150: Control('}'), 150..151: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__group_sort_derive_select_join.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/group_sort_derive_select_join.prql --- Tokens( [ 0..0: Start, 0..46: Interpolation('s', "SELECT album_id,title,artist_id FROM albums"), 46..47: NewLine, 47..52: Ident("group"), 53..54: Control('{'), 54..63: Ident("artist_id"), 63..64: Control('}'), 65..66: Control('('), 66..75: Ident("aggregate"), 76..77: Control('{'), 78..95: Ident("album_title_count"), 96..97: Control('='), 98..103: Ident("count"), 104..108: Ident("this"), 108..109: Control('.'), 109..116: Ident("title"), 116..117: Control('}'), 117..118: Control(')'), 118..119: NewLine, 119..123: Ident("sort"), 124..125: Control('{'), 125..129: Ident("this"), 129..130: Control('.'), 130..139: Ident("artist_id"), 139..140: Control(','), 141..145: Ident("this"), 145..146: Control('.'), 146..163: Ident("album_title_count"), 163..164: Control('}'), 164..165: NewLine, 165..171: Ident("derive"), 172..173: Control('{'), 173..188: Ident("new_album_count"), 189..190: Control('='), 191..195: Ident("this"), 195..196: Control('.'), 196..213: Ident("album_title_count"), 213..214: Control('}'), 214..215: NewLine, 215..221: Ident("select"), 222..223: Control('{'), 223..227: Ident("this"), 227..228: Control('.'), 228..237: Ident("artist_id"), 237..238: Control(','), 239..243: Ident("this"), 243..244: Control('.'), 244..259: Ident("new_album_count"), 259..260: Control('}'), 260..261: NewLine, 261..265: Ident("join"), 266..270: Ident("side"), 270..271: Control(':'), 271..275: Ident("left"), 276..277: Control('('), 278..330: Interpolation('s', "SELECT artist_id,name as artist_name FROM artists"), 331..332: Control(')'), 333..334: Control('('), 334..338: Ident("this"), 338..339: Control('.'), 339..348: Ident("artist_id"), 349..351: Eq, 352..356: Ident("that"), 356..357: Control('.'), 357..366: Ident("artist_id"), 366..367: Control(')'), 367..368: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__group_sort_filter_derive_select_join.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/group_sort_filter_derive_select_join.prql --- Tokens( [ 0..0: Start, 0..46: Interpolation('s', "SELECT album_id,title,artist_id FROM albums"), 46..47: NewLine, 47..52: Ident("group"), 53..54: Control('{'), 54..63: Ident("artist_id"), 63..64: Control('}'), 65..66: Control('('), 66..75: Ident("aggregate"), 76..77: Control('{'), 78..95: Ident("album_title_count"), 96..97: Control('='), 98..103: Ident("count"), 104..108: Ident("this"), 108..109: Control('.'), 109..116: Ident("title"), 116..117: Control('}'), 117..118: Control(')'), 118..119: NewLine, 119..123: Ident("sort"), 124..125: Control('{'), 125..129: Ident("this"), 129..130: Control('.'), 130..139: Ident("artist_id"), 139..140: Control(','), 141..145: Ident("this"), 145..146: Control('.'), 146..163: Ident("album_title_count"), 163..164: Control('}'), 164..165: NewLine, 165..171: Ident("filter"), 172..173: Control('('), 173..177: Ident("this"), 177..178: Control('.'), 178..195: Ident("album_title_count"), 195..196: Control(')'), 197..198: Control('>'), 199..201: Literal(Integer(10)), 201..202: NewLine, 202..208: Ident("derive"), 209..210: Control('{'), 210..225: Ident("new_album_count"), 226..227: Control('='), 228..232: Ident("this"), 232..233: Control('.'), 233..250: Ident("album_title_count"), 250..251: Control('}'), 251..252: NewLine, 252..258: Ident("select"), 259..260: Control('{'), 260..264: Ident("this"), 264..265: Control('.'), 265..274: Ident("artist_id"), 274..275: Control(','), 276..280: Ident("this"), 280..281: Control('.'), 281..296: Ident("new_album_count"), 296..297: Control('}'), 297..298: NewLine, 298..302: Ident("join"), 303..307: Ident("side"), 307..308: Control(':'), 308..312: Ident("left"), 313..314: Control('('), 315..367: Interpolation('s', "SELECT artist_id,name as artist_name FROM artists"), 368..369: Control(')'), 370..371: Control('('), 371..375: Ident("this"), 375..376: Control('.'), 376..385: Ident("artist_id"), 386..388: Eq, 389..393: Ident("that"), 393..394: Control('.'), 394..403: Ident("artist_id"), 403..404: Control(')'), 404..405: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__group_sort_limit_take.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/group_sort_limit_take.prql --- Tokens( [ 0..0: Start, 0..62: Comment(" Compute the 3 longest songs for each genre and sort by genre"), 62..63: NewLine, 63..75: Comment(" mssql:test"), 75..76: NewLine, 76..80: Ident("from"), 81..87: Ident("tracks"), 87..88: NewLine, 88..94: Ident("select"), 95..96: Control('{'), 96..104: Ident("genre_id"), 104..105: Control(','), 105..117: Ident("milliseconds"), 117..118: Control('}'), 118..119: NewLine, 119..124: Ident("group"), 125..126: Control('{'), 126..134: Ident("genre_id"), 134..135: Control('}'), 136..137: Control('('), 137..138: NewLine, 140..144: Ident("sort"), 145..146: Control('{'), 146..147: Control('-'), 147..159: Ident("milliseconds"), 159..160: Control('}'), 160..161: NewLine, 163..167: Ident("take"), 168..169: Literal(Integer(3)), 169..170: NewLine, 170..171: Control(')'), 171..172: NewLine, 172..176: Ident("join"), 177..183: Ident("genres"), 184..185: Control('('), 185..187: Eq, 187..195: Ident("genre_id"), 195..196: Control(')'), 196..197: NewLine, 197..203: Ident("select"), 204..205: Control('{'), 205..209: Ident("name"), 209..210: Control(','), 211..223: Ident("milliseconds"), 223..224: Control('}'), 224..225: NewLine, 225..229: Ident("sort"), 230..231: Control('{'), 231..232: Control('+'), 232..236: Ident("name"), 236..237: Control(','), 237..238: Control('-'), 238..250: Ident("milliseconds"), 250..251: Control('}'), 251..252: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__invoice_totals.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/invoice_totals.prql --- Tokens( [ 0..0: Start, 0..56: Comment(" clickhouse:skip (clickhouse doesn't have lag function)"), 56..57: NewLine, 57..58: NewLine, 58..130: DocComment(" Calculate a number of metrics about the sales of tracks in each city."), 130..131: NewLine, 131..135: Ident("from"), 136..137: Ident("i"), 137..138: Control('='), 138..146: Ident("invoices"), 146..147: NewLine, 147..151: Ident("join"), 152..154: Ident("ii"), 154..155: Control('='), 155..168: Ident("invoice_items"), 169..170: Control('('), 170..172: Eq, 172..182: Ident("invoice_id"), 182..183: Control(')'), 183..184: NewLine, 184..190: Ident("derive"), 191..192: Control('{'), 192..193: NewLine, 197..201: Ident("city"), 202..203: Control('='), 204..205: Ident("i"), 205..206: Control('.'), 206..218: Ident("billing_city"), 218..219: Control(','), 219..220: NewLine, 224..230: Ident("street"), 231..232: Control('='), 233..234: Ident("i"), 234..235: Control('.'), 235..250: Ident("billing_address"), 250..251: Control(','), 251..252: NewLine, 252..253: Control('}'), 253..254: NewLine, 254..259: Ident("group"), 260..261: Control('{'), 261..265: Ident("city"), 265..266: Control(','), 267..273: Ident("street"), 273..274: Control('}'), 275..276: Control('('), 276..277: NewLine, 281..287: Ident("derive"), 288..293: Ident("total"), 294..295: Control('='), 296..298: Ident("ii"), 298..299: Control('.'), 299..309: Ident("unit_price"), 310..311: Control('*'), 312..314: Ident("ii"), 314..315: Control('.'), 315..323: Ident("quantity"), 323..324: NewLine, 328..337: Ident("aggregate"), 338..339: Control('{'), 339..340: NewLine, 348..358: Ident("num_orders"), 359..360: Control('='), 361..375: Ident("count_distinct"), 376..377: Ident("i"), 377..378: Control('.'), 378..388: Ident("invoice_id"), 388..389: Control(','), 389..390: NewLine, 398..408: Ident("num_tracks"), 409..410: Control('='), 411..414: Ident("sum"), 415..417: Ident("ii"), 417..418: Control('.'), 418..426: Ident("quantity"), 426..427: Control(','), 427..428: NewLine, 436..447: Ident("total_price"), 448..449: Control('='), 450..453: Ident("sum"), 454..459: Ident("total"), 459..460: Control(','), 460..461: NewLine, 465..466: Control('}'), 466..467: NewLine, 467..468: Control(')'), 468..469: NewLine, 469..474: Ident("group"), 475..476: Control('{'), 476..480: Ident("city"), 480..481: Control('}'), 482..483: Control('('), 483..484: NewLine, 488..492: Ident("sort"), 493..499: Ident("street"), 499..500: NewLine, 504..510: Ident("window"), 511..520: Ident("expanding"), 520..521: Control(':'), 521..525: Literal(Boolean(true)), 526..527: Control('('), 527..528: NewLine, 536..542: Ident("derive"), 543..544: Control('{'), 544..568: Ident("running_total_num_tracks"), 569..570: Control('='), 571..574: Ident("sum"), 575..585: Ident("num_tracks"), 585..586: Control('}'), 586..587: NewLine, 591..592: Control(')'), 592..593: NewLine, 593..594: Control(')'), 594..595: NewLine, 595..599: Ident("sort"), 600..601: Control('{'), 601..605: Ident("city"), 605..606: Control(','), 607..613: Ident("street"), 613..614: Control('}'), 614..615: NewLine, 615..621: Ident("derive"), 622..623: Control('{'), 623..643: Ident("num_tracks_last_week"), 644..645: Control('='), 646..649: Ident("lag"), 650..651: Literal(Integer(7)), 652..662: Ident("num_tracks"), 662..663: Control('}'), 663..664: NewLine, 664..670: Ident("select"), 671..672: Control('{'), 672..673: NewLine, 677..681: Ident("city"), 681..682: Control(','), 682..683: NewLine, 687..693: Ident("street"), 693..694: Control(','), 694..695: NewLine, 699..709: Ident("num_orders"), 709..710: Control(','), 710..711: NewLine, 715..725: Ident("num_tracks"), 725..726: Control(','), 726..727: NewLine, 731..755: Ident("running_total_num_tracks"), 755..756: Control(','), 756..757: NewLine, 761..781: Ident("num_tracks_last_week"), 781..782: NewLine, 782..783: Control('}'), 783..784: NewLine, 784..788: Ident("take"), 789..791: Literal(Integer(20)), 791..792: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__loop_01.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/loop_01.prql --- Tokens( [ 0..0: Start, 0..47: Comment(" clickhouse:skip (DB::Exception: Syntax error)"), 47..48: NewLine, 48..161: Comment(" glaredb:skip (DataFusion does not support recursive CTEs https://github.com/apache/arrow-datafusion/issues/462)"), 161..162: NewLine, 162..166: Ident("from"), 167..168: Control('['), 168..169: Control('{'), 169..170: Ident("n"), 171..172: Control('='), 173..174: Literal(Integer(1)), 174..175: Control('}'), 175..176: Control(']'), 176..177: NewLine, 177..183: Ident("select"), 184..185: Ident("n"), 186..187: Control('='), 188..189: Ident("n"), 190..191: Control('-'), 192..193: Literal(Integer(2)), 193..194: NewLine, 194..198: Ident("loop"), 199..200: Control('('), 200..206: Ident("filter"), 207..208: Ident("n"), 209..210: Control('<'), 211..212: Literal(Integer(4)), 213..214: Control('|'), 215..221: Ident("select"), 222..223: Ident("n"), 224..225: Control('='), 226..227: Ident("n"), 228..229: Control('+'), 230..231: Literal(Integer(1)), 231..232: Control(')'), 232..233: NewLine, 233..239: Ident("select"), 240..241: Ident("n"), 242..243: Control('='), 244..245: Ident("n"), 246..247: Control('*'), 248..249: Literal(Integer(2)), 249..250: NewLine, 250..254: Ident("sort"), 255..256: Ident("n"), 256..257: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__math_module.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/math_module.prql --- Tokens( [ 0..0: Start, 0..12: Comment(" mssql:test"), 12..13: NewLine, 13..81: Comment(" sqlite:skip (see https://github.com/rusqlite/rusqlite/issues/1211)"), 81..82: NewLine, 82..86: Ident("from"), 87..95: Ident("invoices"), 95..96: NewLine, 96..100: Ident("take"), 101..102: Literal(Integer(5)), 102..103: NewLine, 103..109: Ident("select"), 110..111: Control('{'), 111..112: NewLine, 116..130: Ident("total_original"), 131..132: Control('='), 133..134: Control('('), 134..139: Ident("total"), 140..141: Control('|'), 142..146: Ident("math"), 146..147: Control('.'), 147..152: Ident("round"), 153..154: Literal(Integer(2)), 154..155: Control(')'), 155..156: Control(','), 156..157: NewLine, 161..168: Ident("total_x"), 169..170: Control('='), 171..172: Control('('), 172..176: Ident("math"), 176..177: Control('.'), 177..179: Ident("pi"), 180..181: Control('-'), 182..187: Ident("total"), 188..189: Control('|'), 190..194: Ident("math"), 194..195: Control('.'), 195..200: Ident("round"), 201..202: Literal(Integer(2)), 203..204: Control('|'), 205..209: Ident("math"), 209..210: Control('.'), 210..213: Ident("abs"), 213..214: Control(')'), 214..215: Control(','), 215..216: NewLine, 220..231: Ident("total_floor"), 232..233: Control('='), 234..235: Control('('), 235..239: Ident("math"), 239..240: Control('.'), 240..245: Ident("floor"), 246..251: Ident("total"), 251..252: Control(')'), 252..253: Control(','), 253..254: NewLine, 258..268: Ident("total_ceil"), 269..270: Control('='), 271..272: Control('('), 272..276: Ident("math"), 276..277: Control('.'), 277..281: Ident("ceil"), 282..287: Ident("total"), 287..288: Control(')'), 288..289: Control(','), 289..290: NewLine, 294..305: Ident("total_log10"), 306..307: Control('='), 308..309: Control('('), 309..313: Ident("math"), 313..314: Control('.'), 314..319: Ident("log10"), 320..325: Ident("total"), 326..327: Control('|'), 328..332: Ident("math"), 332..333: Control('.'), 333..338: Ident("round"), 339..340: Literal(Integer(3)), 340..341: Control(')'), 341..342: Control(','), 342..343: NewLine, 347..357: Ident("total_log2"), 358..359: Control('='), 360..361: Control('('), 361..365: Ident("math"), 365..366: Control('.'), 366..369: Ident("log"), 370..371: Literal(Integer(2)), 372..377: Ident("total"), 378..379: Control('|'), 380..384: Ident("math"), 384..385: Control('.'), 385..390: Ident("round"), 391..392: Literal(Integer(3)), 392..393: Control(')'), 393..394: Control(','), 394..395: NewLine, 399..409: Ident("total_sqrt"), 410..411: Control('='), 412..413: Control('('), 413..417: Ident("math"), 417..418: Control('.'), 418..422: Ident("sqrt"), 423..428: Ident("total"), 429..430: Control('|'), 431..435: Ident("math"), 435..436: Control('.'), 436..441: Ident("round"), 442..443: Literal(Integer(3)), 443..444: Control(')'), 444..445: Control(','), 445..446: NewLine, 450..458: Ident("total_ln"), 459..460: Control('='), 461..462: Control('('), 462..466: Ident("math"), 466..467: Control('.'), 467..469: Ident("ln"), 470..475: Ident("total"), 476..477: Control('|'), 478..482: Ident("math"), 482..483: Control('.'), 483..486: Ident("exp"), 487..488: Control('|'), 489..493: Ident("math"), 493..494: Control('.'), 494..499: Ident("round"), 500..501: Literal(Integer(2)), 501..502: Control(')'), 502..503: Control(','), 503..504: NewLine, 508..517: Ident("total_cos"), 518..519: Control('='), 520..521: Control('('), 521..525: Ident("math"), 525..526: Control('.'), 526..529: Ident("cos"), 530..535: Ident("total"), 536..537: Control('|'), 538..542: Ident("math"), 542..543: Control('.'), 543..547: Ident("acos"), 548..549: Control('|'), 550..554: Ident("math"), 554..555: Control('.'), 555..560: Ident("round"), 561..562: Literal(Integer(2)), 562..563: Control(')'), 563..564: Control(','), 564..565: NewLine, 569..578: Ident("total_sin"), 579..580: Control('='), 581..582: Control('('), 582..586: Ident("math"), 586..587: Control('.'), 587..590: Ident("sin"), 591..596: Ident("total"), 597..598: Control('|'), 599..603: Ident("math"), 603..604: Control('.'), 604..608: Ident("asin"), 609..610: Control('|'), 611..615: Ident("math"), 615..616: Control('.'), 616..621: Ident("round"), 622..623: Literal(Integer(2)), 623..624: Control(')'), 624..625: Control(','), 625..626: NewLine, 630..639: Ident("total_tan"), 640..641: Control('='), 642..643: Control('('), 643..647: Ident("math"), 647..648: Control('.'), 648..651: Ident("tan"), 652..657: Ident("total"), 658..659: Control('|'), 660..664: Ident("math"), 664..665: Control('.'), 665..669: Ident("atan"), 670..671: Control('|'), 672..676: Ident("math"), 676..677: Control('.'), 677..682: Ident("round"), 683..684: Literal(Integer(2)), 684..685: Control(')'), 685..686: Control(','), 686..687: NewLine, 691..700: Ident("total_deg"), 701..702: Control('='), 703..704: Control('('), 704..709: Ident("total"), 710..711: Control('|'), 712..716: Ident("math"), 716..717: Control('.'), 717..724: Ident("degrees"), 725..726: Control('|'), 727..731: Ident("math"), 731..732: Control('.'), 732..739: Ident("radians"), 740..741: Control('|'), 742..746: Ident("math"), 746..747: Control('.'), 747..752: Ident("round"), 753..754: Literal(Integer(2)), 754..755: Control(')'), 755..756: Control(','), 756..757: NewLine, 761..773: Ident("total_square"), 774..775: Control('='), 776..777: Control('('), 777..782: Ident("total"), 783..784: Control('|'), 785..789: Ident("math"), 789..790: Control('.'), 790..793: Ident("pow"), 794..795: Literal(Integer(2)), 796..797: Control('|'), 798..802: Ident("math"), 802..803: Control('.'), 803..808: Ident("round"), 809..810: Literal(Integer(2)), 810..811: Control(')'), 811..812: Control(','), 812..813: NewLine, 817..832: Ident("total_square_op"), 833..834: Control('='), 835..836: Control('('), 836..837: Control('('), 837..842: Ident("total"), 843..845: Pow, 846..847: Literal(Integer(2)), 847..848: Control(')'), 849..850: Control('|'), 851..855: Ident("math"), 855..856: Control('.'), 856..861: Ident("round"), 862..863: Literal(Integer(2)), 863..864: Control(')'), 864..865: Control(','), 865..866: NewLine, 866..867: Control('}'), 867..868: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__pipelines.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/pipelines.prql --- Tokens( [ 0..0: Start, 0..76: Comment(" sqlite:skip (Only works on Sqlite implementations which have the extension"), 76..77: NewLine, 77..88: Comment(" installed"), 88..89: NewLine, 89..164: Comment(" https://stackoverflow.com/questions/24037982/how-to-use-regexp-in-sqlite)"), 164..165: NewLine, 165..166: NewLine, 166..170: Ident("from"), 171..177: Ident("tracks"), 177..178: NewLine, 178..179: NewLine, 179..185: Ident("filter"), 186..187: Control('('), 187..191: Ident("name"), 192..194: RegexSearch, 195..201: Literal(String("Love")), 201..202: Control(')'), 202..203: NewLine, 203..209: Ident("filter"), 210..211: Control('('), 211..212: Control('('), 212..224: Ident("milliseconds"), 225..226: Control('/'), 227..231: Literal(Integer(1000)), 232..233: Control('/'), 234..236: Literal(Integer(60)), 236..237: Control(')'), 238..239: Control('|'), 240..242: Ident("in"), 243..244: Literal(Integer(3)), 244..246: Range { bind_left: true, bind_right: true }, 246..247: Literal(Integer(4)), 247..248: Control(')'), 248..249: NewLine, 249..253: Ident("sort"), 254..262: Ident("track_id"), 262..263: NewLine, 263..267: Ident("take"), 268..269: Literal(Integer(1)), 269..271: Range { bind_left: true, bind_right: true }, 271..273: Literal(Integer(15)), 273..274: NewLine, 274..280: Ident("select"), 281..282: Control('{'), 282..286: Ident("name"), 286..287: Control(','), 288..296: Ident("composer"), 296..297: Control('}'), 297..298: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__read_csv.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/read_csv.prql --- Tokens( [ 0..0: Start, 0..13: Comment(" sqlite:skip"), 13..14: NewLine, 14..29: Comment(" postgres:skip"), 29..30: NewLine, 30..42: Comment(" mysql:skip"), 42..43: NewLine, 43..47: Ident("from"), 48..49: Control('('), 49..57: Ident("read_csv"), 58..90: Literal(String("data_file_root/media_types.csv")), 90..91: Control(')'), 91..92: NewLine, 92..98: Ident("append"), 99..100: Control('('), 100..109: Ident("read_json"), 110..143: Literal(String("data_file_root/media_types.json")), 143..144: Control(')'), 144..145: NewLine, 145..149: Ident("sort"), 150..163: Ident("media_type_id"), 163..164: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__set_ops_remove.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/set_ops_remove.prql --- Tokens( [ 0..0: Start, 0..12: Comment(" mssql:test"), 12..13: NewLine, 13..16: Keyword("let"), 17..25: Ident("distinct"), 26..27: Control('='), 28..31: Ident("rel"), 32..34: ArrowThin, 35..36: Control('('), 36..40: Ident("from"), 41..42: Ident("t"), 43..44: Control('='), 45..51: Ident("_param"), 51..52: Control('.'), 52..55: Ident("rel"), 56..57: Control('|'), 58..63: Ident("group"), 64..65: Control('{'), 65..66: Ident("t"), 66..67: Control('.'), 67..68: Control('*'), 68..69: Control('}'), 70..71: Control('('), 71..75: Ident("take"), 76..77: Literal(Integer(1)), 77..78: Control(')'), 78..79: Control(')'), 79..80: NewLine, 80..81: NewLine, 81..90: Ident("from_text"), 91..97: Ident("format"), 97..98: Control(':'), 98..102: Ident("json"), 103..155: Literal(String("{ \"columns\": [\"a\"], \"data\": [[1], [2], [2], [3]] }")), 155..156: NewLine, 156..164: Ident("distinct"), 164..165: NewLine, 165..171: Ident("remove"), 172..173: Control('('), 173..182: Ident("from_text"), 183..189: Ident("format"), 189..190: Control(':'), 190..194: Ident("json"), 195..237: Literal(String("{ \"columns\": [\"a\"], \"data\": [[1], [2]] }")), 237..238: Control(')'), 238..239: NewLine, 239..243: Ident("sort"), 244..245: Ident("a"), 245..246: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__sort.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/sort.prql --- Tokens( [ 0..0: Start, 0..12: Comment(" mssql:test"), 12..13: NewLine, 13..17: Ident("from"), 18..19: Ident("e"), 19..20: Control('='), 20..29: Ident("employees"), 29..30: NewLine, 30..36: Ident("filter"), 37..47: Ident("first_name"), 48..50: Ne, 51..61: Literal(String("Mitchell")), 61..62: NewLine, 62..66: Ident("sort"), 67..68: Control('{'), 68..78: Ident("first_name"), 78..79: Control(','), 80..89: Ident("last_name"), 89..90: Control('}'), 90..91: NewLine, 91..92: NewLine, 92..144: Comment(" joining may use HashMerge, which can undo ORDER BY"), 144..145: NewLine, 145..149: Ident("join"), 150..157: Ident("manager"), 157..158: Control('='), 158..167: Ident("employees"), 168..172: Ident("side"), 172..173: Control(':'), 173..177: Ident("left"), 178..179: Control('('), 179..180: Ident("e"), 180..181: Control('.'), 181..191: Ident("reports_to"), 192..194: Eq, 195..202: Ident("manager"), 202..203: Control('.'), 203..214: Ident("employee_id"), 214..215: Control(')'), 215..216: NewLine, 216..217: NewLine, 217..223: Ident("select"), 224..225: Control('{'), 225..226: Ident("e"), 226..227: Control('.'), 227..237: Ident("first_name"), 237..238: Control(','), 239..240: Ident("e"), 240..241: Control('.'), 241..250: Ident("last_name"), 250..251: Control(','), 252..259: Ident("manager"), 259..260: Control('.'), 260..270: Ident("first_name"), 270..271: Control('}'), 271..272: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__sort_2.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/sort_2.prql --- Tokens( [ 0..0: Start, 0..4: Ident("from"), 5..11: Ident("albums"), 11..12: NewLine, 12..18: Ident("select"), 19..20: Control('{'), 21..23: Ident("AA"), 23..24: Control('='), 24..32: Ident("album_id"), 32..33: Control(','), 34..43: Ident("artist_id"), 44..45: Control('}'), 45..46: NewLine, 46..50: Ident("sort"), 51..53: Ident("AA"), 53..54: NewLine, 54..60: Ident("filter"), 61..63: Ident("AA"), 64..66: Gte, 67..69: Literal(Integer(25)), 69..70: NewLine, 70..74: Ident("join"), 75..82: Ident("artists"), 83..84: Control('('), 84..86: Eq, 86..95: Ident("artist_id"), 95..96: Control(')'), 96..97: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__sort_3.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/sort_3.prql --- Tokens( [ 0..0: Start, 0..4: Ident("from"), 5..6: Control('['), 6..7: Control('{'), 7..15: Ident("track_id"), 15..16: Control('='), 16..17: Literal(Integer(0)), 17..18: Control(','), 19..27: Ident("album_id"), 27..28: Control('='), 28..29: Literal(Integer(1)), 29..30: Control(','), 31..39: Ident("genre_id"), 39..40: Control('='), 40..41: Literal(Integer(2)), 41..42: Control('}'), 42..43: Control(']'), 43..44: NewLine, 44..50: Ident("select"), 51..52: Control('{'), 53..55: Ident("AA"), 55..56: Control('='), 56..64: Ident("track_id"), 64..65: Control(','), 66..74: Ident("album_id"), 74..75: Control(','), 76..84: Ident("genre_id"), 85..86: Control('}'), 86..87: NewLine, 87..91: Ident("sort"), 92..94: Ident("AA"), 94..95: NewLine, 95..99: Ident("join"), 100..104: Ident("side"), 104..105: Control(':'), 105..109: Ident("left"), 110..111: Control('['), 111..112: Control('{'), 112..120: Ident("album_id"), 120..121: Control('='), 121..122: Literal(Integer(1)), 122..123: Control(','), 124..135: Ident("album_title"), 135..136: Control('='), 136..143: Literal(String("Songs")), 143..144: Control('}'), 144..145: Control(']'), 146..147: Control('('), 147..149: Eq, 149..157: Ident("album_id"), 157..158: Control(')'), 158..159: NewLine, 159..165: Ident("select"), 166..167: Control('{'), 168..170: Ident("AA"), 170..171: Control(','), 172..174: Ident("AT"), 175..176: Control('='), 177..188: Ident("album_title"), 189..191: Coalesce, 192..201: Literal(String("unknown")), 201..202: Control(','), 203..211: Ident("genre_id"), 212..213: Control('}'), 213..214: NewLine, 214..220: Ident("filter"), 221..223: Ident("AA"), 224..225: Control('<'), 226..228: Literal(Integer(25)), 228..229: NewLine, 229..233: Ident("join"), 234..238: Ident("side"), 238..239: Control(':'), 239..243: Ident("left"), 244..245: Control('['), 245..246: Control('{'), 246..254: Ident("genre_id"), 254..255: Control('='), 255..256: Literal(Integer(1)), 256..257: Control(','), 258..269: Ident("genre_title"), 269..270: Control('='), 270..276: Literal(String("Rock")), 276..277: Control('}'), 277..278: Control(']'), 279..280: Control('('), 280..282: Eq, 282..290: Ident("genre_id"), 290..291: Control(')'), 291..292: NewLine, 292..298: Ident("select"), 299..300: Control('{'), 301..303: Ident("AA"), 303..304: Control(','), 305..307: Ident("AT"), 307..308: Control(','), 309..311: Ident("GT"), 312..313: Control('='), 314..325: Ident("genre_title"), 326..328: Coalesce, 329..338: Literal(String("unknown")), 339..340: Control('}'), 340..341: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__switch.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/switch.prql --- Tokens( [ 0..0: Start, 0..75: Comment(" glaredb:skip (May be a bag of String type conversion for Postgres Client)"), 75..76: NewLine, 76..88: Comment(" mssql:test"), 88..89: NewLine, 89..93: Ident("from"), 94..100: Ident("tracks"), 100..101: NewLine, 101..105: Ident("sort"), 106..118: Ident("milliseconds"), 118..119: NewLine, 119..125: Ident("select"), 126..133: Ident("display"), 134..135: Control('='), 136..140: Keyword("case"), 141..142: Control('['), 142..143: NewLine, 147..155: Ident("composer"), 156..158: Ne, 159..163: Literal(Null), 164..166: ArrowFat, 167..175: Ident("composer"), 175..176: Control(','), 176..177: NewLine, 181..189: Ident("genre_id"), 190..191: Control('<'), 192..194: Literal(Integer(17)), 195..197: ArrowFat, 198..211: Literal(String("no composer")), 211..212: Control(','), 212..213: NewLine, 217..221: Literal(Boolean(true)), 222..224: ArrowFat, 225..244: Interpolation('f', "unknown composer"), 244..245: NewLine, 245..246: Control(']'), 246..247: NewLine, 247..251: Ident("take"), 252..254: Literal(Integer(10)), 254..255: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__take.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/take.prql --- Tokens( [ 0..0: Start, 0..12: Comment(" mssql:test"), 12..13: NewLine, 13..17: Ident("from"), 18..24: Ident("tracks"), 24..25: NewLine, 25..29: Ident("sort"), 30..31: Control('{'), 31..32: Control('+'), 32..40: Ident("track_id"), 40..41: Control('}'), 41..42: NewLine, 42..46: Ident("take"), 47..48: Literal(Integer(3)), 48..50: Range { bind_left: true, bind_right: true }, 50..51: Literal(Integer(5)), 51..52: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__text_module.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/text_module.prql --- Tokens( [ 0..0: Start, 0..12: Comment(" mssql:test"), 12..13: NewLine, 13..95: Comment(" glaredb:skip — TODO: started raising an error on 2024-05-20; see `window.prql`"), 95..96: NewLine, 96..114: Comment(" for more details"), 114..115: NewLine, 115..119: Ident("from"), 120..126: Ident("albums"), 126..127: NewLine, 127..133: Ident("select"), 134..135: Control('{'), 135..136: NewLine, 140..145: Ident("title"), 145..146: Control(','), 146..147: NewLine, 151..167: Ident("title_and_spaces"), 168..169: Control('='), 170..184: Interpolation('f', " {title} "), 184..185: Control(','), 185..186: NewLine, 190..193: Ident("low"), 194..195: Control('='), 196..197: Control('('), 197..202: Ident("title"), 203..204: Control('|'), 205..209: Ident("text"), 209..210: Control('.'), 210..215: Ident("lower"), 215..216: Control(')'), 216..217: Control(','), 217..218: NewLine, 222..224: Ident("up"), 225..226: Control('='), 227..228: Control('('), 228..233: Ident("title"), 234..235: Control('|'), 236..240: Ident("text"), 240..241: Control('.'), 241..246: Ident("upper"), 246..247: Control(')'), 247..248: Control(','), 248..249: NewLine, 253..261: Ident("ltrimmed"), 262..263: Control('='), 264..265: Control('('), 265..270: Ident("title"), 271..272: Control('|'), 273..277: Ident("text"), 277..278: Control('.'), 278..283: Ident("ltrim"), 283..284: Control(')'), 284..285: Control(','), 285..286: NewLine, 290..298: Ident("rtrimmed"), 299..300: Control('='), 301..302: Control('('), 302..307: Ident("title"), 308..309: Control('|'), 310..314: Ident("text"), 314..315: Control('.'), 315..320: Ident("rtrim"), 320..321: Control(')'), 321..322: Control(','), 322..323: NewLine, 327..334: Ident("trimmed"), 335..336: Control('='), 337..338: Control('('), 338..343: Ident("title"), 344..345: Control('|'), 346..350: Ident("text"), 350..351: Control('.'), 351..355: Ident("trim"), 355..356: Control(')'), 356..357: Control(','), 357..358: NewLine, 362..365: Ident("len"), 366..367: Control('='), 368..369: Control('('), 369..374: Ident("title"), 375..376: Control('|'), 377..381: Ident("text"), 381..382: Control('.'), 382..388: Ident("length"), 388..389: Control(')'), 389..390: Control(','), 390..391: NewLine, 395..399: Ident("subs"), 400..401: Control('='), 402..403: Control('('), 403..408: Ident("title"), 409..410: Control('|'), 411..415: Ident("text"), 415..416: Control('.'), 416..423: Ident("extract"), 424..425: Literal(Integer(2)), 426..427: Literal(Integer(5)), 427..428: Control(')'), 428..429: Control(','), 429..430: NewLine, 434..441: Ident("replace"), 442..443: Control('='), 444..445: Control('('), 445..450: Ident("title"), 451..452: Control('|'), 453..457: Ident("text"), 457..458: Control('.'), 458..465: Ident("replace"), 466..470: Literal(String("al")), 471..477: Literal(String("PIKA")), 477..478: Control(')'), 478..479: Control(','), 479..480: NewLine, 480..481: Control('}'), 481..482: NewLine, 482..486: Ident("sort"), 487..488: Control('{'), 488..493: Ident("title"), 493..494: Control('}'), 494..495: NewLine, 495..501: Ident("filter"), 502..503: Control('('), 503..508: Ident("title"), 509..510: Control('|'), 511..515: Ident("text"), 515..516: Control('.'), 516..527: Ident("starts_with"), 528..535: Literal(String("Black")), 535..536: Control(')'), 537..539: Or, 540..541: Control('('), 541..546: Ident("title"), 547..548: Control('|'), 549..553: Ident("text"), 553..554: Control('.'), 554..562: Ident("contains"), 563..572: Literal(String("Sabbath")), 572..573: Control(')'), 574..576: Or, 577..578: Control('('), 578..583: Ident("title"), 584..585: Control('|'), 586..590: Ident("text"), 590..591: Control('.'), 591..600: Ident("ends_with"), 601..605: Literal(String("os")), 605..606: Control(')'), 606..607: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__lex__window.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: tokens input_file: prqlc/prqlc/tests/integration/queries/window.prql --- Tokens( [ 0..0: Start, 0..43: Comment(" clickhouse:skip problems with DISTINCT ON"), 43..44: NewLine, 44..183: Comment(" glaredb:skip — TODO: started raising an error on 2024-05-20, from https://github.com/PRQL/prql/actions/runs/9154902656/job/25198160283:"), 183..184: NewLine, 188..263: Comment(" ERROR: This feature is not implemented: Unsupported ast node in sqltorel:"), 263..264: NewLine, 268..344: Comment(" Substring { expr: Identifier(Ident { value: \"title\", quote_style: None }),"), 344..345: NewLine, 349..414: Comment(" substring_from: Some(Value(Number(\"2\", false))), substring_for:"), 414..415: NewLine, 419..469: Comment(" Some(Value(Number(\"5\", false))), special: true }"), 469..470: NewLine, 470..474: Ident("from"), 475..481: Ident("tracks"), 481..482: NewLine, 482..487: Ident("group"), 488..496: Ident("genre_id"), 497..498: Control('('), 498..499: NewLine, 501..505: Ident("sort"), 506..518: Ident("milliseconds"), 518..519: NewLine, 521..527: Ident("derive"), 528..529: Control('{'), 529..530: NewLine, 534..537: Ident("num"), 538..539: Control('='), 540..550: Ident("row_number"), 551..555: Ident("this"), 555..556: Control(','), 556..557: NewLine, 561..566: Ident("total"), 567..568: Control('='), 569..574: Ident("count"), 575..579: Ident("this"), 579..580: Control(','), 580..581: NewLine, 585..593: Ident("last_val"), 594..595: Control('='), 596..600: Ident("last"), 601..609: Ident("track_id"), 609..610: Control(','), 610..611: NewLine, 613..614: Control('}'), 614..615: NewLine, 617..621: Ident("take"), 622..624: Literal(Integer(10)), 624..625: NewLine, 625..626: Control(')'), 626..627: NewLine, 627..631: Ident("sort"), 632..633: Control('{'), 633..641: Ident("genre_id"), 641..642: Control(','), 643..655: Ident("milliseconds"), 655..656: Control('}'), 656..657: NewLine, 657..663: Ident("select"), 664..665: Control('{'), 665..673: Ident("track_id"), 673..674: Control(','), 675..683: Ident("genre_id"), 683..684: Control(','), 685..688: Ident("num"), 688..689: Control(','), 690..695: Ident("total"), 695..696: Control(','), 697..705: Ident("last_val"), 705..706: Control('}'), 706..707: NewLine, 707..713: Ident("filter"), 714..722: Ident("genre_id"), 723..725: Gte, 726..728: Literal(Integer(22)), 728..729: NewLine, ], ) ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__aggregation.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:skip\n# mysql:skip\n# clickhouse:skip\n# glaredb:skip (the string_agg function is not supported)\nfrom tracks\nfilter genre_id == 100\nderive empty_name = name == ''\naggregate {sum track_id, concat_array name, all empty_name, any empty_name}\n" input_file: prqlc/prqlc/tests/integration/queries/aggregation.prql --- 0,,1,0 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__append_select.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nselect { customer_id, invoice_id, billing_country }\ntake 10..15\nappend (\n from invoices\n select { customer_id, invoice_id, billing_country }\n take 40..45\n)\nselect { billing_country, invoice_id }\n" input_file: prqlc/prqlc/tests/integration/queries/append_select.prql --- Ireland,10 United Kingdom,11 Germany,12 USA,13 USA,14 USA,15 Germany,40 Spain,41 Sweden,42 United Kingdom,43 Australia,44 India,45 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__append_select_compute.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nderive total = case [total < 10 => total * 2, true => total]\nselect { customer_id, invoice_id, total }\ntake 5\nappend (\n from invoice_items\n derive unit_price = case [unit_price < 1 => unit_price * 2, true => unit_price]\n select { invoice_line_id, invoice_id, unit_price }\n take 5\n)\nselect { a = customer_id * 2, b = math.round 1 (invoice_id * total) }\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_compute.prql --- 4,4 8,15.8 16,35.6 28,71.3 46,69.3 2,2 4,2 6,4 8,4 10,4 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__append_select_multiple_with_null.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nselect { customer_id, invoice_id, billing_country }\ntake 5\nappend (\n from employees\n select { employee_id, employee_id, country }\n take 5\n)\nappend (\n from invoice_items\n select { invoice_line_id, invoice_id, null }\n take 5\n)\nselect { billing_country, invoice_id }\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_multiple_with_null.prql --- Germany,1 Norway,2 Belgium,3 Canada,4 USA,5 Canada,1 Canada,2 Canada,3 Canada,4 Canada,5 ,1 ,1 ,2 ,2 ,2 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__append_select_nulls.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# duckdb:skip\n# postgres:skip\n\nfrom invoices\nselect {an_id = invoice_id, name = null}\ntake 2\nappend (\n from employees\n select {an_id = null, name = first_name}\n take 2\n)\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_nulls.prql --- 1, 2, ,Andrew ,Nancy ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__append_select_simple.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from invoices\nselect { invoice_id, billing_country }\nappend (\n from invoices\n select { invoice_id = `invoice_id` + 100, billing_country }\n)\nfilter (billing_country | text.starts_with(\"I\"))\n" input_file: prqlc/prqlc/tests/integration/queries/append_select_simple.prql --- 10,Ireland 23,India 45,India 62,Ireland 63,Italy 86,Italy 97,India 108,Italy 120,India 131,India 160,Italy 183,Ireland 186,India 194,Ireland 218,India 229,India 249,Ireland 281,Italy 284,India 292,Italy 315,India 338,India 347,Italy 360,India 378,Ireland 401,Ireland 412,India 110,Ireland 123,India 145,India 162,Ireland 163,Italy 186,Italy 197,India 208,Italy 220,India 231,India 260,Italy 283,Ireland 286,India 294,Ireland 318,India 329,India 349,Ireland 381,Italy 384,India 392,Italy 415,India 438,India 447,Italy 460,India 478,Ireland 501,Ireland 512,India ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__arithmetic.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom [\n { id = 1, x_int = 13, x_float = 13.0, k_int = 5, k_float = 5.0 },\n { id = 2, x_int = -13, x_float = -13.0, k_int = 5, k_float = 5.0 },\n { id = 3, x_int = 13, x_float = 13.0, k_int = -5, k_float = -5.0 },\n { id = 4, x_int = -13, x_float = -13.0, k_int = -5, k_float = -5.0 },\n]\nselect {\n id,\n\n x_int / k_int,\n x_int / k_float,\n x_float / k_int,\n x_float / k_float,\n\n q_ii = x_int // k_int,\n q_if = x_int // k_float,\n q_fi = x_float // k_int,\n q_ff = x_float // k_float,\n\n r_ii = x_int % k_int,\n r_if = x_int % k_float,\n r_fi = x_float % k_int,\n r_ff = x_float % k_float,\n\n (q_ii * k_int + r_ii | math.round 0),\n (q_if * k_float + r_if | math.round 0),\n (q_fi * k_int + r_fi | math.round 0),\n (q_ff * k_float + r_ff | math.round 0),\n}\nsort id\n" input_file: prqlc/prqlc/tests/integration/queries/arithmetic.prql --- 1,2.6,2.6,2.6,2.6,2,2,2,2,3,3,3,3,13,13,13,13 2,-2.6,-2.6,-2.6,-2.6,-2,-2,-2,-2,-3,-3,-3,-3,-13,-13,-13,-13 3,-2.6,-2.6,-2.6,-2.6,-2,-2,-2,-2,3,3,3,3,13,13,13,13 4,2.6,2.6,2.6,2.6,2,2,2,2,-3,-3,-3,-3,-13,-13,-13,-13 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__cast.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nsort {-bytes}\nselect {\n name,\n bin = ((album_id | as REAL) * 99)\n}\ntake 20\n" input_file: prqlc/prqlc/tests/integration/queries/cast.prql --- Through a Looking Glass,22671 Occupation / Precipice,22473 The Young Lords,25047 The Man With Nine Lives,25047 Dave,22869 The Magnificent Warriors,25047 The Lost Warrior,25047 Maternity Leave,22869 Battlestar Galactica, Pt. 3,25047 The Woman King,22473 Murder On the Rising Star,25047 Through the Looking Glass, Pt. 2,22671 The Man from Tallahassee,22671 Better Halves,22572 Tricia Tanaka Is Dead,22671 Experiment In Terra,25047 The Gun On Ice Planet Zero, Pt. 2,25047 Lockdown,22869 Man of Science, Man of Faith (Premiere),22869 Run!,22572 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__constants_only.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from genres\ntake 10\nfilter true\ntake 20\nfilter true\nselect d = 10\n" input_file: prqlc/prqlc/tests/integration/queries/constants_only.prql --- 10 10 10 10 10 10 10 10 10 10 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__date_to_text.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# generic:skip\n# glaredb:skip\n# sqlite:skip\n# mssql:test\nfrom invoices\ntake 20\nselect {\n d1 = (invoice_date | date.to_text \"%Y/%m/%d\"),\n d2 = (invoice_date | date.to_text \"%F\"),\n d3 = (invoice_date | date.to_text \"%D\"),\n d4 = (invoice_date | date.to_text \"%H:%M:%S.%f\"),\n d5 = (invoice_date | date.to_text \"%r\"),\n d6 = (invoice_date | date.to_text \"%A %B %-d %Y\"),\n d7 = (invoice_date | date.to_text \"%a, %-d %b %Y at %I:%M:%S %p\"),\n d8 = (invoice_date | date.to_text \"%+\"),\n d9 = (invoice_date | date.to_text \"%-d/%-m/%y\"),\n d10 = (invoice_date | date.to_text \"%-Hh %Mmin\"),\n d11 = (invoice_date | date.to_text \"%M'%S\\\"\"),\n d12 = (invoice_date | date.to_text \"100%% in %d days\"),\n}\n" input_file: prqlc/prqlc/tests/integration/queries/date_to_text.prql snapshot_kind: text --- 2009/01/01,2009-01-01,01/01/09,00:00:00.000000,12:00:00 AM,Thursday January 1 2009,Thu, 1 Jan 2009 at 12:00:00 AM,2009-01-01T00:00:00.000000Z,1/1/09,0h 00min,00'00",100% in 01 days 2009/01/02,2009-01-02,01/02/09,00:00:00.000000,12:00:00 AM,Friday January 2 2009,Fri, 2 Jan 2009 at 12:00:00 AM,2009-01-02T00:00:00.000000Z,2/1/09,0h 00min,00'00",100% in 02 days 2009/01/03,2009-01-03,01/03/09,00:00:00.000000,12:00:00 AM,Saturday January 3 2009,Sat, 3 Jan 2009 at 12:00:00 AM,2009-01-03T00:00:00.000000Z,3/1/09,0h 00min,00'00",100% in 03 days 2009/01/06,2009-01-06,01/06/09,00:00:00.000000,12:00:00 AM,Tuesday January 6 2009,Tue, 6 Jan 2009 at 12:00:00 AM,2009-01-06T00:00:00.000000Z,6/1/09,0h 00min,00'00",100% in 06 days 2009/01/11,2009-01-11,01/11/09,00:00:00.000000,12:00:00 AM,Sunday January 11 2009,Sun, 11 Jan 2009 at 12:00:00 AM,2009-01-11T00:00:00.000000Z,11/1/09,0h 00min,00'00",100% in 11 days 2009/01/19,2009-01-19,01/19/09,00:00:00.000000,12:00:00 AM,Monday January 19 2009,Mon, 19 Jan 2009 at 12:00:00 AM,2009-01-19T00:00:00.000000Z,19/1/09,0h 00min,00'00",100% in 19 days 2009/02/01,2009-02-01,02/01/09,00:00:00.000000,12:00:00 AM,Sunday February 1 2009,Sun, 1 Feb 2009 at 12:00:00 AM,2009-02-01T00:00:00.000000Z,1/2/09,0h 00min,00'00",100% in 01 days 2009/02/01,2009-02-01,02/01/09,00:00:00.000000,12:00:00 AM,Sunday February 1 2009,Sun, 1 Feb 2009 at 12:00:00 AM,2009-02-01T00:00:00.000000Z,1/2/09,0h 00min,00'00",100% in 01 days 2009/02/02,2009-02-02,02/02/09,00:00:00.000000,12:00:00 AM,Monday February 2 2009,Mon, 2 Feb 2009 at 12:00:00 AM,2009-02-02T00:00:00.000000Z,2/2/09,0h 00min,00'00",100% in 02 days 2009/02/03,2009-02-03,02/03/09,00:00:00.000000,12:00:00 AM,Tuesday February 3 2009,Tue, 3 Feb 2009 at 12:00:00 AM,2009-02-03T00:00:00.000000Z,3/2/09,0h 00min,00'00",100% in 03 days 2009/02/06,2009-02-06,02/06/09,00:00:00.000000,12:00:00 AM,Friday February 6 2009,Fri, 6 Feb 2009 at 12:00:00 AM,2009-02-06T00:00:00.000000Z,6/2/09,0h 00min,00'00",100% in 06 days 2009/02/11,2009-02-11,02/11/09,00:00:00.000000,12:00:00 AM,Wednesday February 11 2009,Wed, 11 Feb 2009 at 12:00:00 AM,2009-02-11T00:00:00.000000Z,11/2/09,0h 00min,00'00",100% in 11 days 2009/02/19,2009-02-19,02/19/09,00:00:00.000000,12:00:00 AM,Thursday February 19 2009,Thu, 19 Feb 2009 at 12:00:00 AM,2009-02-19T00:00:00.000000Z,19/2/09,0h 00min,00'00",100% in 19 days 2009/03/04,2009-03-04,03/04/09,00:00:00.000000,12:00:00 AM,Wednesday March 4 2009,Wed, 4 Mar 2009 at 12:00:00 AM,2009-03-04T00:00:00.000000Z,4/3/09,0h 00min,00'00",100% in 04 days 2009/03/04,2009-03-04,03/04/09,00:00:00.000000,12:00:00 AM,Wednesday March 4 2009,Wed, 4 Mar 2009 at 12:00:00 AM,2009-03-04T00:00:00.000000Z,4/3/09,0h 00min,00'00",100% in 04 days 2009/03/05,2009-03-05,03/05/09,00:00:00.000000,12:00:00 AM,Thursday March 5 2009,Thu, 5 Mar 2009 at 12:00:00 AM,2009-03-05T00:00:00.000000Z,5/3/09,0h 00min,00'00",100% in 05 days 2009/03/06,2009-03-06,03/06/09,00:00:00.000000,12:00:00 AM,Friday March 6 2009,Fri, 6 Mar 2009 at 12:00:00 AM,2009-03-06T00:00:00.000000Z,6/3/09,0h 00min,00'00",100% in 06 days 2009/03/09,2009-03-09,03/09/09,00:00:00.000000,12:00:00 AM,Monday March 9 2009,Mon, 9 Mar 2009 at 12:00:00 AM,2009-03-09T00:00:00.000000Z,9/3/09,0h 00min,00'00",100% in 09 days 2009/03/14,2009-03-14,03/14/09,00:00:00.000000,12:00:00 AM,Saturday March 14 2009,Sat, 14 Mar 2009 at 12:00:00 AM,2009-03-14T00:00:00.000000Z,14/3/09,0h 00min,00'00",100% in 14 days 2009/03/22,2009-03-22,03/22/09,00:00:00.000000,12:00:00 AM,Sunday March 22 2009,Sun, 22 Mar 2009 at 12:00:00 AM,2009-03-22T00:00:00.000000Z,22/3/09,0h 00min,00'00",100% in 22 days ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__distinct.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nselect {album_id, genre_id}\ngroup tracks.* (take 1)\nsort tracks.*\n" input_file: prqlc/prqlc/tests/integration/queries/distinct.prql --- 1,1 2,1 3,1 4,1 5,1 6,1 7,1 8,2 9,3 10,1 11,4 12,5 13,2 14,3 15,3 16,3 17,3 18,4 19,3 20,6 21,7 22,7 23,7 24,7 25,7 26,8 27,8 28,7 29,9 30,1 31,1 32,10 33,7 34,7 35,3 36,1 37,1 38,2 39,4 40,1 41,7 42,4 43,1 44,1 45,7 46,1 47,7 48,2 49,2 50,1 51,2 52,11 53,7 54,1 55,1 56,7 57,7 58,1 59,1 60,1 61,1 62,1 63,1 64,1 65,1 66,1 67,1 68,2 69,7 70,7 71,7 72,6 73,6 73,7 74,4 75,4 76,1 77,4 78,7 79,1 80,1 81,4 82,1 83,12 84,7 85,10 86,7 87,2 88,3 89,4 90,1 91,1 92,3 93,2 94,1 95,3 96,3 97,1 98,13 99,1 100,6 101,13 102,3 102,13 103,1 104,1 105,3 106,3 107,3 108,3 109,1 109,3 110,3 111,3 112,1 112,3 113,1 114,1 115,14 116,1 117,14 118,15 119,4 120,1 121,1 122,7 123,7 124,16 125,3 126,1 127,1 128,1 129,1 130,1 131,1 132,1 133,1 134,1 135,1 136,1 137,1 138,1 139,7 140,7 141,1 141,3 141,8 142,7 143,7 144,1 145,7 146,14 147,1 148,3 149,3 150,3 151,3 152,3 153,3 154,3 155,3 156,3 157,2 158,7 159,7 160,3 161,16 162,3 163,1 164,1 165,1 166,7 167,7 168,7 169,7 170,1 171,1 172,1 173,1 174,3 175,1 176,10 177,1 178,1 179,4 180,1 181,1 182,1 183,1 184,17 185,1 186,1 187,4 188,4 189,1 190,4 191,4 192,1 193,4 194,1 195,1 196,1 197,1 198,1 199,1 200,1 201,4 202,4 203,1 204,2 205,6 206,1 207,3 208,1 209,6 210,6 211,4 212,1 213,1 214,1 215,1 216,1 217,1 218,1 219,4 220,4 221,1 222,7 223,7 224,4 225,4 226,18 227,18 227,19 227,20 228,19 228,21 229,19 229,21 230,19 231,19 231,21 232,1 233,1 234,1 235,1 236,1 237,1 238,1 239,1 240,1 241,8 242,1 243,1 244,1 245,1 246,1 247,7 248,7 249,19 250,19 251,19 251,22 252,1 253,20 254,19 255,9 256,1 257,1 258,17 259,15 260,23 261,19 261,21 262,2 263,16 264,15 265,1 266,7 267,2 268,24 269,23 270,23 271,23 272,24 273,24 274,24 275,24 276,24 277,24 278,24 279,24 280,24 281,24 282,24 283,24 284,24 285,24 286,24 287,24 288,24 289,24 290,24 291,24 292,24 293,24 294,24 295,24 296,24 297,24 298,24 299,24 300,24 301,24 302,24 303,24 304,24 305,24 306,24 307,24 308,24 309,24 310,24 311,24 312,24 313,24 314,24 315,24 316,24 317,25 318,24 319,24 320,24 321,14 322,9 323,23 324,24 325,24 326,24 327,24 328,24 329,24 330,24 331,24 332,24 333,24 334,24 335,24 336,24 337,24 338,24 339,24 340,24 341,24 342,24 343,24 344,24 345,24 346,24 347,10 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__distinct_on.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nselect {genre_id, media_type_id, album_id}\ngroup {genre_id, media_type_id} (sort {-album_id} | take 1)\nsort {-genre_id, media_type_id}\n" input_file: prqlc/prqlc/tests/integration/queries/distinct_on.prql --- 25,2,317 24,2,346 24,4,342 24,5,268 23,2,323 23,3,271 23,4,260 22,3,251 21,3,261 20,3,253 19,3,261 18,3,227 17,1,258 16,1,161 16,5,263 15,1,259 15,5,264 14,1,146 14,2,321 13,1,102 12,1,83 11,1,52 10,1,176 10,2,347 9,1,29 9,2,322 8,1,241 7,1,248 7,5,266 6,1,210 5,1,12 4,1,225 3,1,207 2,1,204 2,5,267 1,1,246 1,2,257 1,5,265 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__genre_counts.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip (ClickHouse prefers aliases to column names https://github.com/PRQL/prql/issues/2827)\n# mssql:test\nlet genre_count = (\n from genres\n aggregate {a = count name}\n)\n\nfrom genre_count\nfilter a > 0\nselect a = -a\n" input_file: prqlc/prqlc/tests/integration/queries/genre_counts.prql --- -25 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__group_all.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom a=albums\ntake 10\njoin tracks (==album_id)\ngroup {a.album_id, a.title} (aggregate price = (sum tracks.unit_price | math.round 2))\nsort album_id\n" input_file: prqlc/prqlc/tests/integration/queries/group_all.prql --- 1,For Those About To Rock We Salute You,9.9 2,Balls to the Wall,0.99 3,Restless and Wild,2.97 4,Let There Be Rock,7.92 5,Big Ones,14.85 6,Jagged Little Pill,12.87 7,Facelift,11.88 8,Warner 25 Anos,13.86 9,Plays Metallica By Four Cellos,7.92 10,Audioslave,13.86 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__group_sort.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nderive d = album_id + 1\ngroup d (\n aggregate {\n n1 = (track_id | sum),\n }\n)\nsort d\ntake 10\nselect { d1 = d, n1 }\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort.prql --- 2,91 3,2 4,12 5,148 6,450 7,572 8,678 9,973 10,644 11,1281 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__group_sort_derive_select_join.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "s\"SELECT album_id,title,artist_id FROM albums\"\ngroup {artist_id} (aggregate { album_title_count = count this.`title`})\nsort {this.artist_id, this.album_title_count}\nderive {new_album_count = this.album_title_count}\nselect {this.artist_id, this.new_album_count}\njoin side:left ( s\"SELECT artist_id,name as artist_name FROM artists\" ) (this.artist_id == that.artist_id)\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort_derive_select_join.prql --- 1,2,1,AC/DC 2,2,2,Accept 3,1,3,Aerosmith 4,1,4,Alanis Morissette 5,1,5,Alice In Chains 6,2,6,Antônio Carlos Jobim 7,1,7,Apocalyptica 8,3,8,Audioslave 9,1,9,BackBeat 10,1,10,Billy Cobham 11,2,11,Black Label Society 12,2,12,Black Sabbath 13,1,13,Body Count 14,1,14,Bruce Dickinson 15,1,15,Buddy Guy 16,2,16,Caetano Veloso 17,1,17,Chico Buarque 18,2,18,Chico Science & Nação Zumbi 19,2,19,Cidade Negra 20,1,20,Cláudio Zoli 21,4,21,Various Artists 22,14,22,Led Zeppelin 23,1,23,Frank Zappa & Captain Beefheart 24,1,24,Marcos Valle 27,3,27,Gilberto Gil 36,1,36,O Rappa 37,1,37,Ed Motta 41,1,41,Elis Regina 42,2,42,Milton Nascimento 46,1,46,Jorge Ben 50,10,50,Metallica 51,3,51,Queen 52,2,52,Kiss 53,2,53,Spyro Gyra 54,2,54,Green Day 55,1,55,David Coverdale 56,1,56,Gonzaguinha 57,1,57,Os Mutantes 58,11,58,Deep Purple 59,3,59,Santana 68,3,68,Miles Davis 69,1,69,Gene Krupa 70,1,70,Toquinho & Vinícius 72,1,72,Vinícius De Moraes 76,2,76,Creedence Clearwater Revival 77,2,77,Cássia Eller 78,1,78,Def Leppard 79,1,79,Dennis Chambers 80,2,80,Djavan 81,2,81,Eric Clapton 82,4,82,Faith No More 83,1,83,Falamansa 84,4,84,Foo Fighters 85,1,85,Frank Sinatra 86,1,86,Funk Como Le Gusta 87,1,87,Godsmack 88,3,88,Guns N' Roses 89,1,89,Incognito 90,21,90,Iron Maiden 91,1,91,James Brown 92,3,92,Jamiroquai 93,1,93,JET 94,1,94,Jimi Hendrix 95,1,95,Joe Satriani 96,1,96,Jota Quest 97,1,97,João Suplicy 98,1,98,Judas Priest 99,2,99,Legião Urbana 100,1,100,Lenny Kravitz 101,2,101,Lulu Santos 102,1,102,Marillion 103,1,103,Marisa Monte 104,1,104,Marvin Gaye 105,1,105,Men At Work 106,1,106,Motörhead 108,1,108,Mônica Marianno 109,1,109,Mötley Crüe 110,2,110,Nirvana 111,1,111,O Terço 112,1,112,Olodum 113,3,113,Os Paralamas Do Sucesso 114,6,114,Ozzy Osbourne 115,1,115,Page & Plant 116,1,116,Passengers 117,1,117,Paul D'Ianno 118,5,118,Pearl Jam 120,1,120,Pink Floyd 121,1,121,Planet Hemp 122,1,122,R.E.M. Feat. Kate Pearson 124,3,124,R.E.M. 125,1,125,Raimundos 126,1,126,Raul Seixas 127,3,127,Red Hot Chili Peppers 128,1,128,Rush 130,2,130,Skank 131,2,131,Smashing Pumpkins 132,1,132,Soundgarden 133,1,133,Stevie Ray Vaughan & Double Trouble 134,1,134,Stone Temple Pilots 135,1,135,System Of A Down 136,1,136,Terry Bozzio, Tony Levin & Steve Stevens 137,2,137,The Black Crowes 138,1,138,The Clash 139,2,139,The Cult 140,1,140,The Doors 141,1,141,The Police 142,3,142,The Rolling Stones 143,2,143,The Tea Party 144,1,144,The Who 145,2,145,Tim Maia 146,2,146,Titãs 147,2,147,Battlestar Galactica 148,1,148,Heroes 149,4,149,Lost 150,10,150,U2 151,1,151,UB40 152,4,152,Van Halen 153,1,153,Velvet Revolver 155,1,155,Zeca Pagodinho 156,3,156,The Office 157,1,157,Dread Zeppelin 158,1,158,Battlestar Galactica (Classic) 159,1,159,Aquaman 179,1,179,Scorpions 180,1,180,House Of Pain 196,1,196,Cake 197,1,197,Aisha Duo 198,1,198,Habib Koité and Bamada 199,1,199,Karsh Kale 200,1,200,The Posies 201,1,201,Luciana Souza/Romero Lubambo 202,1,202,Aaron Goldberg 203,1,203,Nicolaus Esterhazy Sinfonia 204,1,204,Temple of the Dog 205,1,205,Chris Cornell 206,1,206,Alberto Turco & Nova Schola Gregoriana 207,1,207,Richard Marlow & The Choir of Trinity College, Cambridge 208,2,208,English Concert & Trevor Pinnock 209,1,209,Anne-Sophie Mutter, Herbert Von Karajan & Wiener Philharmoniker 210,1,210,Hilary Hahn, Jeffrey Kahane, Los Angeles Chamber Orchestra & Margaret Batjer 211,1,211,Wilhelm Kempff 212,1,212,Yo-Yo Ma 213,1,213,Scholars Baroque Ensemble 214,1,214,Academy of St. Martin in the Fields & Sir Neville Marriner 215,1,215,Academy of St. Martin in the Fields Chamber Ensemble & Sir Neville Marriner 216,1,216,Berliner Philharmoniker, Claudio Abbado & Sabine Meyer 217,1,217,Royal Philharmonic Orchestra & Sir Thomas Beecham 218,1,218,Orchestre Révolutionnaire et Romantique & John Eliot Gardiner 219,1,219,Britten Sinfonia, Ivor Bolton & Lesley Garrett 220,1,220,Chicago Symphony Chorus, Chicago Symphony Orchestra & Sir Georg Solti 221,1,221,Sir Georg Solti & Wiener Philharmoniker 222,1,222,Academy of St. Martin in the Fields, John Birch, Sir Neville Marriner & Sylvia McNair 223,1,223,London Symphony Orchestra & Sir Charles Mackerras 224,1,224,Barry Wordsworth & BBC Concert Orchestra 225,1,225,Herbert Von Karajan, Mirella Freni & Wiener Philharmoniker 226,3,226,Eugene Ormandy 227,1,227,Luciano Pavarotti 228,1,228,Leonard Bernstein & New York Philharmonic 229,1,229,Boston Symphony Orchestra & Seiji Ozawa 230,1,230,Aaron Copland & London Symphony Orchestra 231,1,231,Ton Koopman 232,1,232,Sergei Prokofiev & Yuri Temirkanov 233,1,233,Chicago Symphony Orchestra & Fritz Reiner 234,1,234,Orchestra of The Age of Enlightenment 235,1,235,Emanuel Ax, Eugene Ormandy & Philadelphia Orchestra 236,1,236,James Levine 237,1,237,Berliner Philharmoniker & Hans Rosbaud 238,1,238,Maurizio Pollini 240,1,240,Gustav Mahler 241,1,241,Felix Schmidt, London Symphony Orchestra & Rafael Frühbeck de Burgos 242,1,242,Edo de Waart & San Francisco Symphony 243,1,243,Antal Doráti & London Symphony Orchestra 244,1,244,Choir Of Westminster Abbey & Simon Preston 245,2,245,Michael Tilson Thomas & San Francisco Symphony 246,1,246,Chor der Wiener Staatsoper, Herbert Von Karajan & Wiener Philharmoniker 247,1,247,The King's Singers 248,3,248,Berliner Philharmoniker & Herbert Von Karajan 249,1,249,Sir Georg Solti, Sumi Jo & Wiener Philharmoniker 250,1,250,Christopher O'Riley 251,1,251,Fretwork 252,2,252,Amy Winehouse 253,1,253,Calexico 254,1,254,Otto Klemperer & Philharmonia Orchestra 255,1,255,Yehudi Menuhin 256,1,256,Philharmonia Orchestra & Sir Neville Marriner 257,1,257,Academy of St. Martin in the Fields, Sir Neville Marriner & Thurston Dart 258,1,258,Les Arts Florissants & William Christie 259,1,259,The 12 Cellists of The Berlin Philharmonic 260,1,260,Adrian Leaper & Doreen de Feis 261,1,261,Roger Norrington, London Classical Players 262,1,262,Charles Dutoit & L'Orchestre Symphonique de Montréal 263,1,263,Equale Brass Ensemble, John Eliot Gardiner & Munich Monteverdi Orchestra and Choir 264,1,264,Kent Nagano and Orchestre de l'Opéra de Lyon 265,1,265,Julian Bream 266,1,266,Martin Roscoe 267,1,267,Göteborgs Symfoniker & Neeme Järvi 268,1,268,Itzhak Perlman 269,1,269,Michele Campanella 270,1,270,Gerald Moore 271,1,271,Mela Tenenbaum, Pro Musica Prague & Richard Kapp 272,1,272,Emerson String Quartet 273,1,273,C. Monteverdi, Nigel Rogers - Chiaroscuro; London Baroque; London Cornett & Sackbu 274,1,274,Nash Ensemble 275,1,275,Philip Glass Ensemble ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__group_sort_filter_derive_select_join.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "s\"SELECT album_id,title,artist_id FROM albums\"\ngroup {artist_id} (aggregate { album_title_count = count this.`title`})\nsort {this.artist_id, this.album_title_count}\nfilter (this.album_title_count) > 10\nderive {new_album_count = this.album_title_count}\nselect {this.artist_id, this.new_album_count}\njoin side:left ( s\"SELECT artist_id,name as artist_name FROM artists\" ) (this.artist_id == that.artist_id)\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort_filter_derive_select_join.prql --- 22,14,22,Led Zeppelin 58,11,58,Deep Purple 90,21,90,Iron Maiden ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__group_sort_limit_take.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# Compute the 3 longest songs for each genre and sort by genre\n# mssql:test\nfrom tracks\nselect {genre_id,milliseconds}\ngroup {genre_id} (\n sort {-milliseconds}\n take 3\n)\njoin genres (==genre_id)\nselect {name, milliseconds}\nsort {+name,-milliseconds}\n" input_file: prqlc/prqlc/tests/integration/queries/group_sort_limit_take.prql --- Alternative,672773 Alternative,414474 Alternative,384497 Alternative & Punk,558602 Alternative & Punk,548336 Alternative & Punk,518556 Blues,589531 Blues,528692 Blues,505521 Bossa Nova,409965 Bossa Nova,392437 Bossa Nova,244297 Classical,596519 Classical,582029 Classical,567494 Comedy,2541875 Comedy,2519436 Comedy,1814855 Drama,5088838 Drama,2780416 Drama,2698791 Easy Listening,292075 Easy Listening,275879 Easy Listening,266605 Electronica/Dance,529684 Electronica/Dance,422321 Electronica/Dance,385697 Heavy Metal,516649 Heavy Metal,508107 Heavy Metal,441782 Hip Hop/Rap,410409 Hip Hop/Rap,315637 Hip Hop/Rap,239908 Jazz,907520 Jazz,843964 Jazz,807392 Latin,543007 Latin,526132 Latin,482429 Metal,816509 Metal,789472 Metal,671712 Opera,174813 Pop,663426 Pop,409906 Pop,315960 R&B/Soul,418293 R&B/Soul,341629 R&B/Soul,340218 Reggae,366733 Reggae,353671 Reggae,341498 Rock,1612329 Rock,1196094 Rock,1116734 Rock And Roll,163265 Rock And Roll,161123 Rock And Roll,147591 Sci Fi & Fantasy,2960293 Sci Fi & Fantasy,2956998 Sci Fi & Fantasy,2956081 Science Fiction,2713755 Science Fiction,2627961 Science Fiction,2626376 Soundtrack,383764 Soundtrack,340767 Soundtrack,330266 TV Shows,5286953 TV Shows,2825166 TV Shows,2782333 World,300605 World,285837 World,284107 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__invoice_totals.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip (clickhouse doesn't have lag function)\n\n#! Calculate a number of metrics about the sales of tracks in each city.\nfrom i=invoices\njoin ii=invoice_items (==invoice_id)\nderive {\n city = i.billing_city,\n street = i.billing_address,\n}\ngroup {city, street} (\n derive total = ii.unit_price * ii.quantity\n aggregate {\n num_orders = count_distinct i.invoice_id,\n num_tracks = sum ii.quantity,\n total_price = sum total,\n }\n)\ngroup {city} (\n sort street\n window expanding:true (\n derive {running_total_num_tracks = sum num_tracks}\n )\n)\nsort {city, street}\nderive {num_tracks_last_week = lag 7 num_tracks}\nselect {\n city,\n street,\n num_orders,\n num_tracks,\n running_total_num_tracks,\n num_tracks_last_week\n}\ntake 20\n" input_file: prqlc/prqlc/tests/integration/queries/invoice_totals.prql --- Amsterdam,Lijnbaansgracht 120bg,7,38,38, Bangalore,3,Raj Bhavan Road,6,36,36, Berlin,Barbarossastraße 19,7,38,38, Berlin,Tauentzienstraße 8,7,38,76, Bordeaux,9, Place Louis Barthou,7,38,38, Boston,69 Salem Street,7,38,38, Brasília,Qe 7 Bloco G,7,38,38, Brussels,Grétrystraat 63,7,38,38,38 Budapest,Erzsébet krt. 58.,7,38,38,36 Buenos Aires,307 Macacha Güemes,7,38,38,38 Chicago,162 E Superior Street,7,38,38,38 Copenhagen,Sønder Boulevard 51,7,38,38,38 Cupertino,1 Infinite Loop,7,38,38,38 Delhi,12,Community Centre,7,38,38,38 Dijon,68, Rue Jouvence,7,38,38,38 Dublin,3 Chatham Street,7,38,38,38 Edinburgh ,110 Raeburn Pl,7,38,38,38 Edmonton,8210 111 ST NW,7,38,38,38 Fort Worth,2211 W Berry Street,7,38,38,38 Frankfurt,Berger Straße 10,7,38,38,38 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__loop_01.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# clickhouse:skip (DB::Exception: Syntax error)\n# glaredb:skip (DataFusion does not support recursive CTEs https://github.com/apache/arrow-datafusion/issues/462)\nfrom [{n = 1}]\nselect n = n - 2\nloop (filter n < 4 | select n = n + 1)\nselect n = n * 2\nsort n\n" input_file: prqlc/prqlc/tests/integration/queries/loop_01.prql --- -2 0 2 4 6 8 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__math_module.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\n# sqlite:skip (see https://github.com/rusqlite/rusqlite/issues/1211)\nfrom invoices\ntake 5\nselect {\n total_original = (total | math.round 2),\n total_x = (math.pi - total | math.round 2 | math.abs),\n total_floor = (math.floor total),\n total_ceil = (math.ceil total),\n total_log10 = (math.log10 total | math.round 3),\n total_log2 = (math.log 2 total | math.round 3),\n total_sqrt = (math.sqrt total | math.round 3),\n total_ln = (math.ln total | math.exp | math.round 2),\n total_cos = (math.cos total | math.acos | math.round 2),\n total_sin = (math.sin total | math.asin | math.round 2),\n total_tan = (math.tan total | math.atan | math.round 2),\n total_deg = (total | math.degrees | math.radians | math.round 2),\n total_square = (total | math.pow 2 | math.round 2),\n total_square_op = ((total ** 2) | math.round 2),\n}\n" input_file: prqlc/prqlc/tests/integration/queries/math_module.prql --- 1.98,1.16,1,2,0.297,0.986,1.407,1.98,1.98,1.16,-1.16,1.98,3.92,3.92 3.96,0.82,3,4,0.598,1.986,1.99,3.96,2.32,-0.82,0.82,3.96,15.68,15.68 5.94,2.8,5,6,0.774,2.57,2.437,5.94,0.34,-0.34,-0.34,5.94,35.28,35.28 8.91,5.77,8,9,0.95,3.155,2.985,8.91,2.63,0.51,-0.51,8.91,79.39,79.39 13.86,10.72,13,14,1.142,3.793,3.723,13.86,1.29,1.29,1.29,13.86,192.1,192.1 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__pipelines.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# sqlite:skip (Only works on Sqlite implementations which have the extension\n# installed\n# https://stackoverflow.com/questions/24037982/how-to-use-regexp-in-sqlite)\n\nfrom tracks\n\nfilter (name ~= \"Love\")\nfilter ((milliseconds / 1000 / 60) | in 3..4)\nsort track_id\ntake 1..15\nselect {name, composer}\n" input_file: prqlc/prqlc/tests/integration/queries/pipelines.prql --- My Love,Jauperi/Zeu Góes The Girl I Love She Got Long Black Wavy Hair,Jimmy Page/John Bonham/John Estes/John Paul Jones/Robert Plant Love Gun,Paul Stanley Do You Love Me,Paul Stanley, B. Ezrin, K. Fowley Calling Dr. Love,Gene Simmons Um Love, Love Child,Bolin/Coverdale Love Conquers All,Blackmore, Glover, Turner You Can't Do it Right (With the One You Love),D.Coverdale/G.Hughes/Glenn Hughes/R.Blackmore/Ritchie Blackmore She Loves Me Not,Bill Gould/Mike Bordin/Mike Patton Underwater Love,Faith No More Loves Been Good To Me,rod mckuen Love Or Confusion,Jimi Hendrix May This Be Love,Jimi Hendrix Do You Love Me,Paul Stanley, Bob Ezrin, Kim Fowley ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__read_csv.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# sqlite:skip\n# postgres:skip\n# mysql:skip\nfrom (read_csv \"data_file_root/media_types.csv\")\nappend (read_json \"data_file_root/media_types.json\")\nsort media_type_id\n" input_file: prqlc/prqlc/tests/integration/queries/read_csv.prql --- 1,MPEG audio file 1,MPEG audio file 2,Protected AAC audio file 2,Protected AAC audio file 3,Protected MPEG-4 video file 3,Protected MPEG-4 video file 4,Purchased AAC audio file 4,Purchased AAC audio file 5,AAC audio file 5,AAC audio file ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__set_ops_remove.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nlet distinct = rel -> (from t = _param.rel | group {t.*} (take 1))\n\nfrom_text format:json '{ \"columns\": [\"a\"], \"data\": [[1], [2], [2], [3]] }'\ndistinct\nremove (from_text format:json '{ \"columns\": [\"a\"], \"data\": [[1], [2]] }')\nsort a\n" input_file: prqlc/prqlc/tests/integration/queries/set_ops_remove.prql --- 3 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__sort.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom e=employees\nfilter first_name != \"Mitchell\"\nsort {first_name, last_name}\n\n# joining may use HashMerge, which can undo ORDER BY\njoin manager=employees side:left (e.reports_to == manager.employee_id)\n\nselect {e.first_name, e.last_name, manager.first_name}\n" input_file: prqlc/prqlc/tests/integration/queries/sort.prql --- Andrew,Adams,Michael Jane,Peacock,Nancy Laura,Callahan,Michael Margaret,Park,Nancy Michael,Mitchell,Andrew Nancy,Edwards,Andrew Robert,King,Michael Steve,Johnson,Nancy ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__sort_2.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from albums\nselect { AA=album_id, artist_id }\nsort AA\nfilter AA >= 25\njoin artists (==artist_id)\n" input_file: prqlc/prqlc/tests/integration/queries/sort_2.prql --- 25,18,18,Chico Science & Nação Zumbi 26,19,19,Cidade Negra 27,19,19,Cidade Negra 28,20,20,Cláudio Zoli 29,21,21,Various Artists 30,22,22,Led Zeppelin 31,23,23,Frank Zappa & Captain Beefheart 32,21,21,Various Artists 33,24,24,Marcos Valle 34,6,6,Antônio Carlos Jobim 35,50,50,Metallica 36,51,51,Queen 37,52,52,Kiss 38,53,53,Spyro Gyra 39,54,54,Green Day 40,55,55,David Coverdale 41,56,56,Gonzaguinha 42,57,57,Os Mutantes 43,58,58,Deep Purple 44,22,22,Led Zeppelin 45,21,21,Various Artists 46,59,59,Santana 47,37,37,Ed Motta 48,68,68,Miles Davis 49,68,68,Miles Davis 50,58,58,Deep Purple 51,69,69,Gene Krupa 52,70,70,Toquinho & Vinícius 53,21,21,Various Artists 54,76,76,Creedence Clearwater Revival 55,76,76,Creedence Clearwater Revival 56,77,77,Cássia Eller 57,77,77,Cássia Eller 58,58,58,Deep Purple 59,58,58,Deep Purple 60,58,58,Deep Purple 61,58,58,Deep Purple 62,58,58,Deep Purple 63,58,58,Deep Purple 64,58,58,Deep Purple 65,58,58,Deep Purple 66,58,58,Deep Purple 67,78,78,Def Leppard 68,79,79,Dennis Chambers 69,80,80,Djavan 70,80,80,Djavan 71,41,41,Elis Regina 72,81,81,Eric Clapton 73,81,81,Eric Clapton 74,82,82,Faith No More 75,82,82,Faith No More 76,82,82,Faith No More 77,82,82,Faith No More 78,83,83,Falamansa 79,84,84,Foo Fighters 80,84,84,Foo Fighters 81,84,84,Foo Fighters 82,84,84,Foo Fighters 83,85,85,Frank Sinatra 84,86,86,Funk Como Le Gusta 85,27,27,Gilberto Gil 86,27,27,Gilberto Gil 87,27,27,Gilberto Gil 88,87,87,Godsmack 89,54,54,Green Day 90,88,88,Guns N' Roses 91,88,88,Guns N' Roses 92,88,88,Guns N' Roses 93,89,89,Incognito 94,90,90,Iron Maiden 95,90,90,Iron Maiden 96,90,90,Iron Maiden 97,90,90,Iron Maiden 98,90,90,Iron Maiden 99,90,90,Iron Maiden 100,90,90,Iron Maiden 101,90,90,Iron Maiden 102,90,90,Iron Maiden 103,90,90,Iron Maiden 104,90,90,Iron Maiden 105,90,90,Iron Maiden 106,90,90,Iron Maiden 107,90,90,Iron Maiden 108,90,90,Iron Maiden 109,90,90,Iron Maiden 110,90,90,Iron Maiden 111,90,90,Iron Maiden 112,90,90,Iron Maiden 113,90,90,Iron Maiden 114,90,90,Iron Maiden 115,91,91,James Brown 116,92,92,Jamiroquai 117,92,92,Jamiroquai 118,92,92,Jamiroquai 119,93,93,JET 120,94,94,Jimi Hendrix 121,95,95,Joe Satriani 122,46,46,Jorge Ben 123,96,96,Jota Quest 124,97,97,João Suplicy 125,98,98,Judas Priest 126,52,52,Kiss 127,22,22,Led Zeppelin 128,22,22,Led Zeppelin 129,22,22,Led Zeppelin 130,22,22,Led Zeppelin 131,22,22,Led Zeppelin 132,22,22,Led Zeppelin 133,22,22,Led Zeppelin 134,22,22,Led Zeppelin 135,22,22,Led Zeppelin 136,22,22,Led Zeppelin 137,22,22,Led Zeppelin 138,22,22,Led Zeppelin 139,99,99,Legião Urbana 140,99,99,Legião Urbana 141,100,100,Lenny Kravitz 142,101,101,Lulu Santos 143,101,101,Lulu Santos 144,102,102,Marillion 145,103,103,Marisa Monte 146,104,104,Marvin Gaye 147,105,105,Men At Work 148,50,50,Metallica 149,50,50,Metallica 150,50,50,Metallica 151,50,50,Metallica 152,50,50,Metallica 153,50,50,Metallica 154,50,50,Metallica 155,50,50,Metallica 156,50,50,Metallica 157,68,68,Miles Davis 158,42,42,Milton Nascimento 159,42,42,Milton Nascimento 160,106,106,Motörhead 161,108,108,Mônica Marianno 162,109,109,Mötley Crüe 163,110,110,Nirvana 164,110,110,Nirvana 165,111,111,O Terço 166,112,112,Olodum 167,113,113,Os Paralamas Do Sucesso 168,113,113,Os Paralamas Do Sucesso 169,113,113,Os Paralamas Do Sucesso 170,114,114,Ozzy Osbourne 171,114,114,Ozzy Osbourne 172,114,114,Ozzy Osbourne 173,114,114,Ozzy Osbourne 174,114,114,Ozzy Osbourne 175,115,115,Page & Plant 176,116,116,Passengers 177,117,117,Paul D'Ianno 178,118,118,Pearl Jam 179,118,118,Pearl Jam 180,118,118,Pearl Jam 181,118,118,Pearl Jam 182,118,118,Pearl Jam 183,120,120,Pink Floyd 184,121,121,Planet Hemp 185,51,51,Queen 186,51,51,Queen 187,122,122,R.E.M. Feat. Kate Pearson 188,124,124,R.E.M. 189,124,124,R.E.M. 190,124,124,R.E.M. 191,125,125,Raimundos 192,126,126,Raul Seixas 193,127,127,Red Hot Chili Peppers 194,127,127,Red Hot Chili Peppers 195,127,127,Red Hot Chili Peppers 196,128,128,Rush 197,59,59,Santana 198,59,59,Santana 199,130,130,Skank 200,130,130,Skank 201,131,131,Smashing Pumpkins 202,131,131,Smashing Pumpkins 203,132,132,Soundgarden 204,53,53,Spyro Gyra 205,133,133,Stevie Ray Vaughan & Double Trouble 206,134,134,Stone Temple Pilots 207,135,135,System Of A Down 208,136,136,Terry Bozzio, Tony Levin & Steve Stevens 209,137,137,The Black Crowes 210,137,137,The Black Crowes 211,138,138,The Clash 212,139,139,The Cult 213,139,139,The Cult 214,140,140,The Doors 215,141,141,The Police 216,142,142,The Rolling Stones 217,142,142,The Rolling Stones 218,142,142,The Rolling Stones 219,143,143,The Tea Party 220,143,143,The Tea Party 221,144,144,The Who 222,145,145,Tim Maia 223,145,145,Tim Maia 224,146,146,Titãs 225,146,146,Titãs 226,147,147,Battlestar Galactica 227,147,147,Battlestar Galactica 228,148,148,Heroes 229,149,149,Lost 230,149,149,Lost 231,149,149,Lost 232,150,150,U2 233,150,150,U2 234,150,150,U2 235,150,150,U2 236,150,150,U2 237,150,150,U2 238,150,150,U2 239,150,150,U2 240,150,150,U2 241,151,151,UB40 242,152,152,Van Halen 243,152,152,Van Halen 244,152,152,Van Halen 245,152,152,Van Halen 246,153,153,Velvet Revolver 247,72,72,Vinícius De Moraes 248,155,155,Zeca Pagodinho 249,156,156,The Office 250,156,156,The Office 251,156,156,The Office 252,157,157,Dread Zeppelin 253,158,158,Battlestar Galactica (Classic) 254,159,159,Aquaman 255,150,150,U2 256,114,114,Ozzy Osbourne 257,179,179,Scorpions 258,180,180,House Of Pain 259,36,36,O Rappa 260,196,196,Cake 261,149,149,Lost 262,197,197,Aisha Duo 263,198,198,Habib Koité and Bamada 264,199,199,Karsh Kale 265,200,200,The Posies 266,201,201,Luciana Souza/Romero Lubambo 267,202,202,Aaron Goldberg 268,203,203,Nicolaus Esterhazy Sinfonia 269,204,204,Temple of the Dog 270,205,205,Chris Cornell 271,8,8,Audioslave 272,206,206,Alberto Turco & Nova Schola Gregoriana 273,207,207,Richard Marlow & The Choir of Trinity College, Cambridge 274,208,208,English Concert & Trevor Pinnock 275,209,209,Anne-Sophie Mutter, Herbert Von Karajan & Wiener Philharmoniker 276,210,210,Hilary Hahn, Jeffrey Kahane, Los Angeles Chamber Orchestra & Margaret Batjer 277,211,211,Wilhelm Kempff 278,212,212,Yo-Yo Ma 279,213,213,Scholars Baroque Ensemble 280,214,214,Academy of St. Martin in the Fields & Sir Neville Marriner 281,215,215,Academy of St. Martin in the Fields Chamber Ensemble & Sir Neville Marriner 282,216,216,Berliner Philharmoniker, Claudio Abbado & Sabine Meyer 283,217,217,Royal Philharmonic Orchestra & Sir Thomas Beecham 284,218,218,Orchestre Révolutionnaire et Romantique & John Eliot Gardiner 285,219,219,Britten Sinfonia, Ivor Bolton & Lesley Garrett 286,220,220,Chicago Symphony Chorus, Chicago Symphony Orchestra & Sir Georg Solti 287,221,221,Sir Georg Solti & Wiener Philharmoniker 288,222,222,Academy of St. Martin in the Fields, John Birch, Sir Neville Marriner & Sylvia McNair 289,223,223,London Symphony Orchestra & Sir Charles Mackerras 290,224,224,Barry Wordsworth & BBC Concert Orchestra 291,225,225,Herbert Von Karajan, Mirella Freni & Wiener Philharmoniker 292,226,226,Eugene Ormandy 293,227,227,Luciano Pavarotti 294,228,228,Leonard Bernstein & New York Philharmonic 295,229,229,Boston Symphony Orchestra & Seiji Ozawa 296,230,230,Aaron Copland & London Symphony Orchestra 297,231,231,Ton Koopman 298,232,232,Sergei Prokofiev & Yuri Temirkanov 299,233,233,Chicago Symphony Orchestra & Fritz Reiner 300,234,234,Orchestra of The Age of Enlightenment 301,235,235,Emanuel Ax, Eugene Ormandy & Philadelphia Orchestra 302,236,236,James Levine 303,237,237,Berliner Philharmoniker & Hans Rosbaud 304,238,238,Maurizio Pollini 305,240,240,Gustav Mahler 306,241,241,Felix Schmidt, London Symphony Orchestra & Rafael Frühbeck de Burgos 307,242,242,Edo de Waart & San Francisco Symphony 308,243,243,Antal Doráti & London Symphony Orchestra 309,244,244,Choir Of Westminster Abbey & Simon Preston 310,245,245,Michael Tilson Thomas & San Francisco Symphony 311,226,226,Eugene Ormandy 312,245,245,Michael Tilson Thomas & San Francisco Symphony 313,246,246,Chor der Wiener Staatsoper, Herbert Von Karajan & Wiener Philharmoniker 314,247,247,The King's Singers 315,208,208,English Concert & Trevor Pinnock 316,248,248,Berliner Philharmoniker & Herbert Von Karajan 317,249,249,Sir Georg Solti, Sumi Jo & Wiener Philharmoniker 318,250,250,Christopher O'Riley 319,251,251,Fretwork 320,248,248,Berliner Philharmoniker & Herbert Von Karajan 321,252,252,Amy Winehouse 322,252,252,Amy Winehouse 323,253,253,Calexico 324,254,254,Otto Klemperer & Philharmonia Orchestra 325,255,255,Yehudi Menuhin 326,256,256,Philharmonia Orchestra & Sir Neville Marriner 327,257,257,Academy of St. Martin in the Fields, Sir Neville Marriner & Thurston Dart 328,258,258,Les Arts Florissants & William Christie 329,259,259,The 12 Cellists of The Berlin Philharmonic 330,260,260,Adrian Leaper & Doreen de Feis 331,261,261,Roger Norrington, London Classical Players 332,262,262,Charles Dutoit & L'Orchestre Symphonique de Montréal 333,263,263,Equale Brass Ensemble, John Eliot Gardiner & Munich Monteverdi Orchestra and Choir 334,264,264,Kent Nagano and Orchestre de l'Opéra de Lyon 335,265,265,Julian Bream 336,248,248,Berliner Philharmoniker & Herbert Von Karajan 337,266,266,Martin Roscoe 338,267,267,Göteborgs Symfoniker & Neeme Järvi 339,268,268,Itzhak Perlman 340,269,269,Michele Campanella 341,270,270,Gerald Moore 342,271,271,Mela Tenenbaum, Pro Musica Prague & Richard Kapp 343,226,226,Eugene Ormandy 344,272,272,Emerson String Quartet 345,273,273,C. Monteverdi, Nigel Rogers - Chiaroscuro; London Baroque; London Cornett & Sackbu 346,274,274,Nash Ensemble 347,275,275,Philip Glass Ensemble ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__sort_3.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "from [{track_id=0, album_id=1, genre_id=2}]\nselect { AA=track_id, album_id, genre_id }\nsort AA\njoin side:left [{album_id=1, album_title=\"Songs\"}] (==album_id)\nselect { AA, AT = album_title ?? \"unknown\", genre_id }\nfilter AA < 25\njoin side:left [{genre_id=1, genre_title=\"Rock\"}] (==genre_id)\nselect { AA, AT, GT = genre_title ?? \"unknown\" }\n" input_file: prqlc/prqlc/tests/integration/queries/sort_3.prql --- 0,Songs,unknown ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__switch.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# glaredb:skip (May be a bag of String type conversion for Postgres Client)\n# mssql:test\nfrom tracks\nsort milliseconds\nselect display = case [\n composer != null => composer,\n genre_id < 17 => 'no composer',\n true => f'unknown composer'\n]\ntake 10\n" input_file: prqlc/prqlc/tests/integration/queries/switch.prql --- Samuel Rosa no composer no composer no composer L. Muggerud no composer L. Muggerud unknown composer Gilberto Gil Chico Science ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__take.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\nfrom tracks\nsort {+track_id}\ntake 3..5\n" input_file: prqlc/prqlc/tests/integration/queries/take.prql --- 3,Fast As a Shark,3,2,1,F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman,230619,3990994,0.99 4,Restless and Wild,3,2,1,F. Baltes, R.A. Smith-Diesel, S. Kaufman, U. Dirkscneider & W. Hoffman,252051,4331779,0.99 5,Princess of the Dawn,3,2,1,Deaffy & R.A. Smith-Diesel,375418,6290521,0.99 ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__text_module.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:test\n# glaredb:skip — TODO: started raising an error on 2024-05-20; see `window.prql`\n# for more details\nfrom albums\nselect {\n title,\n title_and_spaces = f\" {title} \",\n low = (title | text.lower),\n up = (title | text.upper),\n ltrimmed = (title | text.ltrim),\n rtrimmed = (title | text.rtrim),\n trimmed = (title | text.trim),\n len = (title | text.length),\n subs = (title | text.extract 2 5),\n replace = (title | text.replace \"al\" \"PIKA\"),\n}\nsort {title}\nfilter (title | text.starts_with \"Black\") || (title | text.contains \"Sabbath\") || (title | text.ends_with \"os\")\n" input_file: prqlc/prqlc/tests/integration/queries/text_module.prql --- Bach: The Brandenburg Concertos, Bach: The Brandenburg Concertos ,bach: the brandenburg concertos,BACH: THE BRANDENBURG CONCERTOS,Bach: The Brandenburg Concertos,Bach: The Brandenburg Concertos,Bach: The Brandenburg Concertos,31,ach: ,Bach: The Brandenburg Concertos Bach: Violin Concertos, Bach: Violin Concertos ,bach: violin concertos,BACH: VIOLIN CONCERTOS,Bach: Violin Concertos,Bach: Violin Concertos,Bach: Violin Concertos,22,ach: ,Bach: Violin Concertos Bartok: Violin & Viola Concertos, Bartok: Violin & Viola Concertos ,bartok: violin & viola concertos,BARTOK: VIOLIN & VIOLA CONCERTOS,Bartok: Violin & Viola Concertos,Bartok: Violin & Viola Concertos,Bartok: Violin & Viola Concertos,32,artok,Bartok: Violin & Viola Concertos Black Album, Black Album ,black album,BLACK ALBUM,Black Album,Black Album,Black Album,11,lack ,Black Album Black Sabbath, Black Sabbath ,black sabbath,BLACK SABBATH,Black Sabbath,Black Sabbath,Black Sabbath,13,lack ,Black Sabbath Black Sabbath Vol. 4 (Remaster), Black Sabbath Vol. 4 (Remaster) ,black sabbath vol. 4 (remaster),BLACK SABBATH VOL. 4 (REMASTER),Black Sabbath Vol. 4 (Remaster),Black Sabbath Vol. 4 (Remaster),Black Sabbath Vol. 4 (Remaster),31,lack ,Black Sabbath Vol. 4 (Remaster) Da Lama Ao Caos, Da Lama Ao Caos ,da lama ao caos,DA LAMA AO CAOS,Da Lama Ao Caos,Da Lama Ao Caos,Da Lama Ao Caos,15,a Lam,Da Lama Ao Caos Jorge Ben Jor 25 Anos, Jorge Ben Jor 25 Anos ,jorge ben jor 25 anos,JORGE BEN JOR 25 ANOS,Jorge Ben Jor 25 Anos,Jorge Ben Jor 25 Anos,Jorge Ben Jor 25 Anos,21,orge ,Jorge Ben Jor 25 Anos Meus Momentos, Meus Momentos ,meus momentos,MEUS MOMENTOS,Meus Momentos,Meus Momentos,Meus Momentos,13,eus M,Meus Momentos Mozart: Wind Concertos, Mozart: Wind Concertos ,mozart: wind concertos,MOZART: WIND CONCERTOS,Mozart: Wind Concertos,Mozart: Wind Concertos,Mozart: Wind Concertos,22,ozart,Mozart: Wind Concertos Plays Metallica By Four Cellos, Plays Metallica By Four Cellos ,plays metallica by four cellos,PLAYS METALLICA BY FOUR CELLOS,Plays Metallica By Four Cellos,Plays Metallica By Four Cellos,Plays Metallica By Four Cellos,30,lays ,Plays MetPIKAlica By Four Cellos Warner 25 Anos, Warner 25 Anos ,warner 25 anos,WARNER 25 ANOS,Warner 25 Anos,Warner 25 Anos,Warner 25 Anos,14,arner,Warner 25 Anos ================================================ FILE: prqlc/prqlc/tests/integration/snapshots/integration__queries__results__window.snap ================================================ --- source: prqlc/prqlc/tests/integration/queries.rs expression: "# mssql:skip Conversion(\"cannot interpret I64(Some(1)) as an i32 value\")', connection.rs:200:34\n# duckdb:skip problems with DISTINCT ON (duckdb internal error: [with INPUT_TYPE = int; RESULT_TYPE = unsigned char]: Assertion `min_val <= input' failed.)\n# clickhouse:skip problems with DISTINCT ON\n# postgres:skip problems with DISTINCT ON\n# glaredb:skip — TODO: started raising an error on 2024-05-20, from https://github.com/PRQL/prql/actions/runs/9154902656/job/25198160283:\n # ERROR: This feature is not implemented: Unsupported ast node in sqltorel:\n # Substring { expr: Identifier(Ident { value: \"title\", quote_style: None }),\n # substring_from: Some(Value(Number(\"2\", false))), substring_for:\n # Some(Value(Number(\"5\", false))), special: true }\nfrom tracks\ngroup genre_id (\n sort milliseconds\n derive {\n num = row_number this,\n total = count this,\n last_val = last track_id,\n }\n take 10\n)\nsort {genre_id, milliseconds}\nselect {track_id, genre_id, num, total, last_val}\nfilter genre_id >= 22\n" input_file: prqlc/prqlc/tests/integration/queries/window.prql --- 3219,22,1,17,3219 3218,22,2,17,3218 3214,22,3,17,3214 3210,22,4,17,3210 3213,22,5,17,3213 3216,22,6,17,3216 3208,22,7,17,3208 3211,22,8,17,3211 3215,22,9,17,3215 3221,22,10,17,3221 3379,23,1,40,3379 3384,23,2,40,3384 3399,23,3,40,3399 3395,23,4,40,3395 3377,23,5,40,3377 3478,23,6,40,3478 3385,23,7,40,3385 3390,23,8,40,3390 3381,23,9,40,3381 3393,23,10,40,3393 3496,24,1,74,3496 3501,24,2,74,3501 3448,24,3,74,3448 3452,24,4,74,3452 3483,24,5,74,3483 3449,24,6,74,3449 3408,24,7,74,3408 3447,24,8,74,3447 3492,24,9,74,3492 3500,24,10,74,3500 3451,25,1,1,3451 ================================================ FILE: prqlc/prqlc/tests/integration/sql.rs ================================================ //! Simple tests for "this PRQL creates this SQL" go here. use insta::assert_snapshot; use prqlc::{sql, ErrorMessages, Options, SourceTree, Target}; use rstest::rstest; pub(crate) fn compile(prql: &str) -> Result { prqlc::compile( prql, &Options::default() .no_signature() .with_display(prqlc::DisplayOptions::Plain), ) } fn compile_with_sql_dialect(prql: &str, dialect: sql::Dialect) -> Result { prqlc::compile( prql, &Options::default() .no_signature() .with_target(Target::Sql(Some(dialect))) .with_display(prqlc::DisplayOptions::Plain), ) } #[test] fn test_stdlib() { assert_snapshot!(compile(r###" from employees aggregate ( {salary_usd = min salary} ) "###).unwrap(), @r" SELECT MIN(salary) AS salary_usd FROM employees " ); assert_snapshot!(compile(r###" from employees aggregate ( {salary_usd = (math.round 2 salary)} ) "###).unwrap(), @r" SELECT ROUND(salary, 2) AS salary_usd FROM employees " ); } #[test] fn test_stdlib_math_module() { assert_snapshot!(compile(r#" from employees select { salary_abs = math.abs salary, salary_floor = math.floor salary, salary_ceil = math.ceil salary, salary_pi = math.pi, salary_exp = math.exp salary, salary_ln = math.ln salary, salary_log10 = math.log10 salary, salary_log = math.log 2 salary, salary_sqrt = math.sqrt salary, salary_degrees = math.degrees salary, salary_radians = math.radians salary, salary_cos = math.cos salary, salary_acos = math.acos salary, salary_sin = math.sin salary, salary_asin = math.asin salary, salary_tan = math.tan salary, salary_atan = math.atan salary, salary_pow = (salary | math.pow 2), salary_pow_op = salary ** 2, } "#).unwrap(), @r" SELECT ABS(salary) AS salary_abs, FLOOR(salary) AS salary_floor, CEIL(salary) AS salary_ceil, PI() AS salary_pi, EXP(salary) AS salary_exp, LN(salary) AS salary_ln, LOG10(salary) AS salary_log10, LOG10(salary) / LOG10(2) AS salary_log, SQRT(salary) AS salary_sqrt, DEGREES(salary) AS salary_degrees, RADIANS(salary) AS salary_radians, COS(salary) AS salary_cos, ACOS(salary) AS salary_acos, SIN(salary) AS salary_sin, ASIN(salary) AS salary_asin, TAN(salary) AS salary_tan, ATAN(salary) AS salary_atan, POW(salary, 2) AS salary_pow, POW(salary, 2) AS salary_pow_op FROM employees " ); } #[test] fn test_stdlib_math_module_mssql() { assert_snapshot!(compile(r#" prql target:sql.mssql from employees select { salary_abs = math.abs salary, salary_floor = math.floor salary, salary_ceil = math.ceil salary, salary_pi = math.pi, salary_exp = math.exp salary, salary_ln = math.ln salary, salary_log10 = math.log10 salary, salary_log = math.log 2 salary, salary_sqrt = math.sqrt salary, salary_degrees = math.degrees salary, salary_radians = math.radians salary, salary_cos = math.cos salary, salary_acos = math.acos salary, salary_sin = math.sin salary, salary_asin = math.asin salary, salary_tan = math.tan salary, salary_atan = math.atan salary, salary_pow = (salary | math.pow 2), } "#).unwrap(), @r" SELECT ABS(salary) AS salary_abs, FLOOR(salary) AS salary_floor, CEILING(salary) AS salary_ceil, PI() AS salary_pi, EXP(salary) AS salary_exp, LOG(salary) AS salary_ln, LOG10(salary) AS salary_log10, LOG10(salary) / LOG10(2) AS salary_log, SQRT(salary) AS salary_sqrt, DEGREES(salary) AS salary_degrees, RADIANS(salary) AS salary_radians, COS(salary) AS salary_cos, ACOS(salary) AS salary_acos, SIN(salary) AS salary_sin, ASIN(salary) AS salary_asin, TAN(salary) AS salary_tan, ATAN(salary) AS salary_atan, POWER(salary, 2) AS salary_pow FROM employees " ); } #[test] fn test_stdlib_text_module() { assert_snapshot!(compile(r#" from employees select { name_lower = (name | text.lower), name_upper = (name | text.upper), name_ltrim = (name | text.ltrim), name_rtrim = (name | text.rtrim), name_trim = (name | text.trim), name_length = (name | text.length), name_extract = (name | text.extract 3 5), name_replace = (name | text.replace "pika" "chu"), name_starts_with = (name | text.starts_with "pika"), name_contains = (name | text.contains "pika"), name_ends_with = (name | text.ends_with "pika"), } "#).unwrap(), @r" SELECT LOWER(name) AS name_lower, UPPER(name) AS name_upper, LTRIM(name) AS name_ltrim, RTRIM(name) AS name_rtrim, TRIM(name) AS name_trim, CHAR_LENGTH(name) AS name_length, SUBSTRING(name, 3, 5) AS name_extract, REPLACE(name, 'pika', 'chu') AS name_replace, name LIKE CONCAT('pika', '%') AS name_starts_with, name LIKE CONCAT('%', 'pika', '%') AS name_contains, name LIKE CONCAT('%', 'pika') AS name_ends_with FROM employees " ); } #[rstest] #[case::generic(sql::Dialect::Generic, "LIKE CONCAT('%', 'pika', '%')")] #[case::sqlite(sql::Dialect::SQLite, "LIKE '%' || 'pika' || '%'")] // `CONCAT` is not supported in SQLite fn like_concat(#[case] dialect: sql::Dialect, #[case] expected_like: &'static str) { let query = r#" from employees select { name_ends_with = (name | text.contains "pika"), } "#; let expected = format!( r#" SELECT name {expected_like} AS name_ends_with FROM employees "# ); assert_eq!( compile_with_sql_dialect(query, dialect).unwrap(), expected.trim_start() ) } #[rstest] #[case::generic( sql::Dialect::Generic, r#" SELECT a, b, "col space" FROM employees "# )] #[case::snowflake( sql::Dialect::Snowflake, r#" SELECT "a", "b", "col space" FROM "employees" "# )] fn test_quoting_style(#[case] dialect: sql::Dialect, #[case] expected_sql: &'static str) { let query = r#" from employees select { a, `b`, `col space` } "#; assert_eq!( compile_with_sql_dialect(query, dialect).unwrap(), expected_sql.trim_start() ) } #[rstest] #[case::clickhouse( sql::Dialect::ClickHouse, "formatDateTimeInJodaSyntax(invoice_date, 'dd/MM/yyyy')" )] #[case::duckdb(sql::Dialect::DuckDb, "strftime(invoice_date, '%d/%m/%Y')")] #[case::postgres(sql::Dialect::Postgres, "TO_CHAR(invoice_date, 'DD/MM/YYYY')")] #[case::mssql(sql::Dialect::MsSql, "FORMAT(invoice_date, 'dd/MM/yyyy')")] #[case::mysql(sql::Dialect::MySql, "DATE_FORMAT(invoice_date, '%d/%m/%Y')")] #[case::bigquery( sql::Dialect::BigQuery, "FORMAT_TIMESTAMP('%d/%m/%Y', CAST(invoice_date AS TIMESTAMP))" )] fn date_to_text_operator( #[case] dialect: sql::Dialect, #[case] expected_date_to_text: &'static str, ) { let query = r#" from invoices select { invoice_date = (invoice_date | date.to_text "%d/%m/%Y") }"#; let expected = format!( r#" SELECT {expected_date_to_text} AS invoice_date FROM invoices "# ); assert_eq!( compile_with_sql_dialect(query, dialect).unwrap(), expected.trim_start() ) } #[test] fn date_to_text_bigquery_rfc3339() { assert_snapshot!(compile(r#" prql target:sql.bigquery from [{d = @2021-01-01}] derive { d_str = (d | date.to_text "%+") }"#).unwrap(), @" WITH table_0 AS ( SELECT DATE '2021-01-01' AS d ) SELECT d, FORMAT_TIMESTAMP('%Y-%m-%dT%H:%M:%S%Ez', CAST(d AS TIMESTAMP)) AS d_str FROM table_0 "); } #[test] fn json_of_test() { let pl = prqlc::prql_to_pl("from employees | take 10").unwrap(); let json = prqlc::json::from_pl(&pl).unwrap(); // Since the AST is so in flux right now just test that the brackets are present assert_eq!(json.chars().next().unwrap(), '{'); assert_eq!(json.chars().nth(json.len() - 1).unwrap(), '}'); } #[test] fn test_precedence_division() { assert_snapshot!((compile(r###" from artists derive { p1 = a - (b + c), # needs parentheses p2 = x / (y * z), # needs parentheses np1 = x / y / z, # doesn't need parentheses p3 = x / (y / z), # needs parentheses np4 = (x / y) / z, # doesn't need parentheses } "###).unwrap()), @r" SELECT *, a - (b + c) AS p1, x / (y * z) AS p2, x / y / z AS np1, x / (y / z) AS p3, x / y / z AS np4 FROM artists "); } #[test] fn test_precedence_01() { assert_snapshot!((compile(r###" from artists derive { p1 = a - (b + c), # needs parentheses p2 = a / (b * c), # needs parentheses np1 = a + (b - c), # no parentheses np2 = (a + b) - c, # no parentheses } "###).unwrap()), @r" SELECT *, a - (b + c) AS p1, a / (b * c) AS p2, a + b - c AS np1, a + b - c AS np2 FROM artists "); } #[test] fn test_precedence_02() { assert_snapshot!((compile(r###" from x derive { temp_c = (temp_f - 32) / 1.8, temp_f = temp_c * 9/5, temp_z = temp_x + 9 - 5, } "###).unwrap()), @r" SELECT *, (temp_f - 32) / 1.8 AS temp_c, (temp_f - 32) / 1.8 * 9 / 5 AS temp_f, temp_x + 9 - 5 AS temp_z FROM x "); } #[test] fn test_precedence_03() { assert_snapshot!((compile(r###" from numbers derive { sum_1 = a + b, sum_2 = std.add a b, g = -a } select { result = c * sum_1 + sum_2, a * g } "###).unwrap()), @r" SELECT c * (a + b) + a + b AS result, a * - a FROM numbers "); } #[test] fn test_precedence_04() { assert_snapshot!((compile(r###" from comparisons select { gtz = a > 0, ltz = !(a > 0), zero = !gtz && !ltz, is_not_equal = !(a==b), is_not_gt = !(a>b), negated_is_null_1 = !a == null, negated_is_null_2 = (!a) == null, is_not_null = !(a == null), (a + b) == null, } "###).unwrap()), @r" SELECT a > 0 AS gtz, NOT a > 0 AS ltz, NOT a > 0 AND NOT NOT a > 0 AS zero, NOT a = b AS is_not_equal, NOT a > b AS is_not_gt, (NOT a) IS NULL AS negated_is_null_1, (NOT a) IS NULL AS negated_is_null_2, NOT a IS NULL AS is_not_null, a + b IS NULL FROM comparisons "); } #[test] fn test_precedence_05() { assert_snapshot!(compile( r###" from numbers derive x = (y - z) select { c - (a + b), c + (a - b), c + a - b, c + a + b, (c + a) - b, ((c - d) - (a - b)), ((c + d) + (a - b)), a / (b * c), +x, -x, } "### ).unwrap(), @r" SELECT c - (a + b), c + a - b, c + a - b, c + a + b, c + a - b, c - d - (a - b), c + d + a - b, a / (b * c), y - z AS x, -(y - z) FROM numbers " ); } #[test] #[ignore] // FIXME: right associativity of `pow` is not implemented yet fn test_pow_is_right_associative() { assert_snapshot!(compile(r#" from numbers select { c ** a ** b } "#).unwrap(), @r#" SELECT POW(c, POW(a, b)) FROM numbers "# ); } #[test] fn test_append() { assert_snapshot!(compile(r###" from employees append managers "###).unwrap(), @r" SELECT * FROM employees UNION ALL SELECT * FROM managers "); assert_snapshot!(compile(r###" from employees select {name, cost = salary} take 3 append ( from employees select {name, cost = salary + bonuses} take 10 ) "###).unwrap(), @r" SELECT * FROM ( SELECT name, salary AS cost FROM employees LIMIT 3 ) AS table_2 UNION ALL SELECT * FROM ( SELECT name, salary + bonuses AS cost FROM employees LIMIT 10 ) AS table_3 "); assert_snapshot!(compile(r###" let distinct = rel -> (_param.rel | group this (take 1)) let union = func `default_db.bottom` top -> (top | append bottom | distinct) from employees union (from managers) "###).unwrap(), @r" SELECT * FROM employees UNION DISTINCT SELECT * FROM managers "); assert_snapshot!(compile(r###" let distinct = rel -> (_param.rel | group this (take 1)) let union = func `default_db.bottom` top -> (top | append bottom | distinct) from employees append managers union all_employees_of_some_other_company "###).unwrap(), @r" SELECT * FROM employees UNION ALL SELECT * FROM managers UNION DISTINCT SELECT * FROM all_employees_of_some_other_company "); } #[test] fn test_remove_01() { assert_snapshot!(compile(r#" from albums remove artists "#).unwrap(), @r" SELECT * FROM albums AS t EXCEPT ALL SELECT * FROM artists AS b " ); } #[test] fn test_remove_02() { assert_snapshot!(compile(r#" from album select artist_id remove ( from artist | select artist_id ) "#).unwrap(), @r" WITH table_0 AS ( SELECT artist_id FROM artist ) SELECT artist_id FROM album EXCEPT ALL SELECT * FROM table_0 " ); } #[test] fn test_remove_03() { assert_snapshot!(compile(r#" from album select {artist_id, title} remove ( from artist | select artist_id ) "#).unwrap(), @r" WITH table_0 AS ( SELECT artist_id FROM artist ) SELECT album.artist_id, album.title FROM album LEFT OUTER JOIN table_0 ON album.artist_id = table_0.artist_id WHERE table_0.artist_id IS NULL " ); } #[test] fn test_remove_04() { assert_snapshot!(compile(r#" prql target:sql.sqlite from album remove artist "#).unwrap_err(), @r" Error: The dialect SQLiteDialect does not support EXCEPT ALL ↳ Hint: providing more column information will allow the query to be translated to an anti-join. " ); } #[test] fn test_remove_05() { assert_snapshot!(compile(r#" prql target:sql.sqlite let distinct = rel -> (from t = _param.rel | group {t.*} (take 1)) let except = `default_db.bottom` top -> (top | distinct | remove bottom) from album select {artist_id, title} except (from artist | select {artist_id, name}) "#).unwrap(), @r" WITH table_0 AS ( SELECT artist_id, name FROM artist ) SELECT artist_id, title FROM album EXCEPT SELECT * FROM table_0 " ); } #[test] fn test_remove_06() { assert_snapshot!(compile(r#" prql target:sql.sqlite let distinct = rel -> (from t = _param.rel | group {t.*} (take 1)) let except = func `default_db.bottom` top -> (top | distinct | remove bottom) from album except artist "#).unwrap(), @r" SELECT * FROM album AS t EXCEPT SELECT * FROM artist AS b " ); } #[test] fn test_intersect_01() { assert_snapshot!(compile(r#" from album intersect artist "#).unwrap(), @r" SELECT * FROM album AS t INTERSECT ALL SELECT * FROM artist AS b " ); } #[test] fn test_intersect_02() { assert_snapshot!(compile(r#" from album select artist_id intersect ( from artist | select artist_id ) "#).unwrap(), @r" WITH table_0 AS ( SELECT artist_id FROM artist ) SELECT artist_id FROM album INTERSECT ALL SELECT * FROM table_0 " ); } #[test] fn test_intersect_03() { assert_snapshot!(compile(r#" let distinct = rel -> (_param.rel | group this (take 1)) from album select artist_id distinct intersect ( from artist | select artist_id ) distinct "#).unwrap(), @r" WITH table_0 AS ( SELECT artist_id FROM artist ), table_1 AS ( SELECT artist_id FROM album INTERSECT DISTINCT SELECT * FROM table_0 ) SELECT DISTINCT artist_id FROM table_1 " ); } #[test] fn test_intersect_04() { assert_snapshot!(compile(r#" let distinct = rel -> (_param.rel | group this (take 1)) from album select artist_id intersect ( from artist | select artist_id ) distinct "#).unwrap(), @r" WITH table_0 AS ( SELECT artist_id FROM artist ), table_1 AS ( SELECT artist_id FROM album INTERSECT ALL SELECT * FROM table_0 ) SELECT DISTINCT artist_id FROM table_1 " ); } #[test] fn test_intersect_05() { assert_snapshot!(compile(r#" let distinct = rel -> (_param.rel | group this (take 1)) from album select artist_id distinct intersect ( from artist | select artist_id ) "#).unwrap(), @r" WITH table_0 AS ( SELECT artist_id FROM artist ) SELECT artist_id FROM album INTERSECT DISTINCT SELECT * FROM table_0 " ); } #[test] fn test_intersect_06() { assert_snapshot!(compile(r#" prql target:sql.sqlite from album intersect artist "#).unwrap_err(), @r" Error: The dialect SQLiteDialect does not support INTERSECT ALL ↳ Hint: providing more column information will allow the query to be translated to an anti-join. " ); } #[test] fn test_intersect_07() { assert_snapshot!(compile(r#" from ds2 = foo.t1 join side:inner ds1 = bar.t2 (ds2.idx==ds1.idx) aggregate { count this } "#).unwrap(), @r" SELECT COUNT(*) FROM foo.t1 AS ds2 INNER JOIN bar.t2 AS ds1 ON ds2.idx = ds1.idx " ); } #[test] fn test_sort_in_nested_join() { assert_snapshot!(compile(r#" from albums join side:left ( from artists sort {-`artist-id`} take 10 ) (this.artist_id == that.artist_id) | take 10 "#).unwrap(), @r#" WITH table_0 AS ( SELECT * FROM artists ORDER BY "artist-id" DESC LIMIT 10 ) SELECT albums.*, table_0.* FROM albums LEFT OUTER JOIN table_0 ON albums.artist_id = table_0.artist_id LIMIT 10 "# ); } #[test] fn test_sort_in_nested_join_with_extra_derive_and_select() { // #5302 assert_snapshot!(compile(r#" from albums join side:left ( from artists derive { my_new_col = f"artist: {name}" } group {my_new_col} (aggregate { first_name = first this.`name`}) sort {this.my_new_col, first_name} derive {new_name = first_name, other_new_name = first_name} select {this.my_new_col, this.new_name, this.other_new_name} ) (this.id == that.my_new_col) "#).unwrap(), @r" WITH table_1 AS ( SELECT CONCAT('artist: ', name) AS my_new_col, FIRST_VALUE(name) AS _expr_0 FROM artists GROUP BY CONCAT('artist: ', name) ), table_2 AS ( SELECT my_new_col, _expr_0 AS new_name, _expr_0 AS other_new_name, _expr_0 FROM table_1 ), table_0 AS ( SELECT my_new_col, new_name, other_new_name, _expr_0 FROM table_2 ) SELECT albums.*, table_0.my_new_col, table_0.new_name, table_0.other_new_name FROM albums LEFT OUTER JOIN table_0 ON albums.id = table_0.my_new_col " ); } #[test] fn test_sort_in_nested_append() { assert_snapshot!(compile(r#" from `albums` select { `album_id`, `title` } sort {+`album_id`} take 2 append ( from `albums` select { `album_id`, `title` } sort {-`album_id`} take 2 ) "#).unwrap(), @r" SELECT * FROM ( SELECT album_id, title FROM albums ORDER BY album_id LIMIT 2 ) AS table_2 UNION ALL SELECT * FROM ( SELECT album_id, title FROM albums ORDER BY album_id DESC LIMIT 2 ) AS table_3 " ); } #[test] fn test_sort_select_redundant_cte() { assert_snapshot!((compile(r#" let a = ( from sometable sort {foo} select { foo } ) let b = ( from a ) from b "# ).unwrap()), @r" WITH a AS ( SELECT foo FROM sometable ), b AS ( SELECT foo FROM a ) SELECT foo FROM b ORDER BY foo "); } #[test] fn test_column_name_extraction_in_s_strings() { assert_snapshot!(compile(r#" from s"SELECT album_id, artist_id `title` FROM `albums`" join side:left ( s"SELECT id, name FROM `artists`" ) (this.artist_id == that.id) "#).unwrap(), @r" WITH table_0 AS ( SELECT album_id, artist_id `title` FROM `albums` ), table_1 AS ( SELECT id, name FROM `artists` ) SELECT table_0.artist_id, table_0.album_id, table_0.title, table_1.id, table_1.name FROM table_0 LEFT OUTER JOIN table_1 ON table_0.artist_id = table_1.id " ) } #[test] fn test_rn_ids_are_unique() { // this is wrong, output will have duplicate y_id and x_id assert_snapshot!((compile(r###" from y_orig group {y_id} ( take 2 # take 1 uses `distinct` instead of partitioning, which might be a separate bug ) group {x_id} ( take 3 ) "###).unwrap()), @r" WITH table_1 AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY y_id) AS _expr_1 FROM y_orig ), table_0 AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY x_id) AS _expr_0 FROM table_1 WHERE _expr_1 <= 2 ) SELECT * FROM table_0 WHERE _expr_0 <= 3 "); } #[test] fn test_quoting_01() { // GH-#822 assert_snapshot!((compile(r###" prql target:sql.postgres let UPPER = ( default_db.lower ) from UPPER join `some_schema.tablename` (==id) derive `from` = 5 "###).unwrap()), @r#" WITH "UPPER" AS ( SELECT * FROM lower ) SELECT "UPPER".*, "some_schema.tablename".*, 5 AS "from" FROM "UPPER" INNER JOIN "some_schema.tablename" ON "UPPER".id = "some_schema.tablename".id "#); } #[test] fn test_quoting_02() { // GH-1493 let query = r###" from `dir/*.parquet` "###; assert_snapshot!((compile(query).unwrap()), @r#" SELECT * FROM "dir/*.parquet" "#); } #[test] fn test_quoting_03() { // GH-#852 assert_snapshot!((compile(r###" prql target:sql.bigquery from `schema.table` join `schema.table2` (==id) join c = `schema.t-able` (`schema.table`.id == c.id) "###).unwrap()), @r" SELECT `schema.table`.*, `schema.table2`.*, c.* FROM `schema.table` INNER JOIN `schema.table2` ON `schema.table`.id = `schema.table2`.id INNER JOIN `schema.t-able` AS c ON `schema.table`.id = c.id "); } #[test] fn test_quoting_04() { assert_snapshot!((compile(r###" from table select `first name` "###).unwrap()), @r#" SELECT "first name" FROM "table" "#); } #[test] fn test_quoting_05() { assert_snapshot!((compile(r###" from as = Assessment "###).unwrap()), @r#" SELECT * FROM "Assessment" AS "as" "#); } #[test] fn test_quoting_06() { let prql = " prql target:sql.bigquery from `some_dataset.demo` select {`hash`} "; assert_snapshot!(compile(prql).unwrap(), @r" SELECT `hash` FROM `some_dataset.demo` "); } #[test] fn test_sorts_01() { assert_snapshot!((compile(r###" from invoices sort {issued_at, -amount, +num_of_articles} "### ).unwrap()), @r" SELECT * FROM invoices ORDER BY issued_at, amount DESC, num_of_articles "); assert_snapshot!((compile(r#" from x derive somefield = "something" sort {somefield} select {renamed = somefield} "# ).unwrap()), @r" WITH table_0 AS ( SELECT 'something' AS renamed, 'something' AS _expr_0 FROM x ) SELECT renamed FROM table_0 ORDER BY renamed "); } #[test] fn test_sorts_02() { // issue #3129 assert_snapshot!((compile(r###" let x = ( from table sort index select {fieldA} ) from x "### ).unwrap()), @r#" WITH table_0 AS ( SELECT "fieldA", "index" FROM "table" ), x AS ( SELECT "fieldA", "index" FROM table_0 ) SELECT "fieldA" FROM x ORDER BY "index" "#); } #[test] fn test_sorts_03() { // TODO: this is invalid SQL: a._expr_0 does not exist assert_snapshot!((compile(r#" from a join b side:left (==col) sort a.col select !{a.col} take 5 "# ).unwrap()), @r" WITH table_0 AS ( SELECT a.*, b.*, a.col AS _expr_0 FROM a LEFT OUTER JOIN b ON a.col = b.col ORDER BY a._expr_0 LIMIT 5 ) SELECT * FROM table_0 ORDER BY _expr_0 "); } #[test] fn test_sort_before_aggregate() { assert_snapshot!((compile(r#" from a sort a.col aggregate { result = sum a.col_to_agg } "# ).unwrap()), @r" SELECT COALESCE(SUM(col_to_agg), 0) AS result FROM a "); } #[test] fn test_numbers() { let query = r###" from numbers select { v = 5.000_000_1, w = 5_000, x = 5, y = 5.0, z = 5.00, } "###; assert_snapshot!((compile(query).unwrap()), @r" SELECT 5.0000001 AS v, 5000 AS w, 5 AS x, 5.0 AS y, 5.0 AS z FROM numbers "); } #[test] fn test_ranges() { assert_snapshot!((compile(r###" from employees derive { close = (distance | in ..50), middle = (distance | in 50..100), far = (distance | in 100..), (country_founding | in @1776-07-04..@1787-09-17) } "###).unwrap()), @r" SELECT *, distance <= 50 AS close, distance BETWEEN 50 AND 100 AS middle, distance >= 100 AS far, country_founding BETWEEN DATE '1776-07-04' AND DATE '1787-09-17' FROM employees "); } #[test] fn test_in_values_01() { assert_snapshot!((compile(r#" from employees filter (title | in ["Sales Manager", "Sales Support Agent"]) filter (employee_id | in [1, 2, 5]) filter (f"{emp_group}.{role}" | in ["sales_ne.mgr", "sales_mw.mgr"]) filter (s"{metadata} ->> '$.location'" | in ["Northeast", "Midwest"]) "#).unwrap()), @r" SELECT * FROM employees WHERE title IN ('Sales Manager', 'Sales Support Agent') AND employee_id IN (1, 2, 5) AND CONCAT(emp_group, '.', role) IN ('sales_ne.mgr', 'sales_mw.mgr') AND metadata ->> '$.location' IN ('Northeast', 'Midwest') "); } #[test] #[ignore] // unimplemented, column ref type resolution required fn test_in_values_02() { assert_snapshot!((compile(r#" let allowed_titles = ["Sales Manager", "Sales Support Agent"] from employees derive {allowed_ids = [1, 2, 5]} filter (title | in allowed_titles) filter (title | in allowed_ids) "#).unwrap()), @r###" SELECT * FROM employees WHERE title IN ('Sales Manager', 'Sales Support Agent') "###); } #[test] #[ignore] // unimplemented, column ref type resolution required fn test_in_values_03() { assert_snapshot!((compile(r#" from employees derive allowed_titles = case [ is_guest => ["Sales Manager"], true => ["Sales Manager", "Sales Support Agent"], ] filter (title | in allowed_titles) "#).unwrap()), @r###" SELECT * FROM employees WHERE title IN ('Sales Manager', 'Sales Support Agent') "###); } #[test] fn test_not_in_values() { assert_snapshot!((compile(r#" from employees filter !(title | in ["Sales Manager", "Sales Support Agent"]) "#).unwrap()), @r" SELECT * FROM employees WHERE NOT title IN ('Sales Manager', 'Sales Support Agent') "); } #[test] fn test_in_no_values() { assert_snapshot!((compile(r#" from employees filter (title | in []) "#).unwrap()), @r" SELECT * FROM employees WHERE false "); } #[test] fn test_in_values_err_01() { assert_snapshot!((compile(r###" from employees derive { ng = ([1, 2] | in [3, 4]) } "###).unwrap_err()), @r" Error: ╭─[ :3:29 ] │ 3 │ derive { ng = ([1, 2] | in [3, 4]) } │ ────┬──── │ ╰────── args to `std.array_in` must be an expression and an array ───╯ "); } #[test] fn test_interval() { let query = r###" from projects derive first_check_in = start + 10days "###; assert_snapshot!((compile(query).unwrap()), @r#" SELECT *, "start" + INTERVAL 10 DAY AS first_check_in FROM projects "#); let query = r###" prql target:sql.postgres from projects derive first_check_in = start + 10days "###; assert_snapshot!((compile(query).unwrap()), @r#" SELECT *, "start" + INTERVAL '10 DAY' AS first_check_in FROM projects "#); let query = r###" prql target:sql.glaredb from projects derive first_check_in = start + 10days "###; assert_snapshot!((compile(query).unwrap()), @r#" SELECT *, "start" + INTERVAL '10 DAY' AS first_check_in FROM projects "#); let query = r###" prql target:sql.snowflake from projects derive first_check_in = start + 10days "###; assert_snapshot!((compile(query).unwrap()), @r#" SELECT *, "start" + INTERVAL '10 DAY' AS "first_check_in" FROM "projects" "#); } #[test] fn test_dates() { assert_snapshot!((compile(r###" from to_do_empty_table derive { date = @2011-02-01, timestamp = @2011-02-01T10:00, time = @14:00, # datetime = @2011-02-01T10:00, } "###).unwrap()), @r" SELECT *, DATE '2011-02-01' AS date, TIMESTAMP '2011-02-01T10:00' AS timestamp, TIME '14:00' AS time FROM to_do_empty_table "); } #[test] fn test_window_functions_00() { assert_snapshot!((compile(r###" from employees group last_name ( derive {count first_name} ) "###).unwrap()), @r" SELECT *, COUNT(*) OVER (PARTITION BY last_name) FROM employees "); } #[test] fn test_window_functions_02() { let query = r#" from co=cust_order join ol=order_line (==order_id) derive { order_month = s"TO_CHAR({co.order_date}, '%Y-%m')", order_day = s"TO_CHAR({co.order_date}, '%Y-%m-%d')", } group {order_month, order_day} ( aggregate { num_orders = s"COUNT(DISTINCT {co.order_id})", num_books = count ol.book_id, total_price = sum ol.price, } ) group {order_month} ( sort order_day window expanding:true ( derive {running_total_num_books = sum num_books} ) ) sort order_day derive {num_books_last_week = lag 7 num_books} "#; assert_snapshot!((compile(query).unwrap()), @r" WITH table_0 AS ( SELECT TO_CHAR(co.order_date, '%Y-%m') AS order_month, TO_CHAR(co.order_date, '%Y-%m-%d') AS order_day, COUNT(DISTINCT co.order_id) AS num_orders, COUNT(*) AS num_books, COALESCE(SUM(ol.price), 0) AS total_price FROM cust_order AS co INNER JOIN order_line AS ol ON co.order_id = ol.order_id GROUP BY TO_CHAR(co.order_date, '%Y-%m'), TO_CHAR(co.order_date, '%Y-%m-%d') ) SELECT order_month, order_day, num_orders, num_books, total_price, SUM(num_books) OVER ( PARTITION BY order_month ORDER BY order_day ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS running_total_num_books, LAG(num_books, 7) OVER ( ORDER BY order_day ) AS num_books_last_week FROM table_0 ORDER BY order_day "); } #[test] fn test_window_functions_03() { // lag must be recognized as window function, even outside of group context // rank must not have two OVER clauses let query = r###" from daily_orders derive {last_week = lag 7 num_orders} derive {first_count = first num_orders} derive {last_count = last num_orders} group month ( derive {total_month = sum num_orders} ) "###; assert_snapshot!((compile(query).unwrap()), @r" SELECT *, LAG(num_orders, 7) OVER () AS last_week, FIRST_VALUE(num_orders) OVER () AS first_count, LAST_VALUE(num_orders) OVER () AS last_count, SUM(num_orders) OVER (PARTITION BY month) AS total_month FROM daily_orders "); } #[test] fn test_window_functions_04() { // sort does not affects into groups, group undoes sorting let query = r###" from daily_orders sort day group month (derive {total_month = rank day}) derive {last_week = lag 7 num_orders} "###; assert_snapshot!((compile(query).unwrap()), @r" SELECT *, RANK() OVER (PARTITION BY month) AS total_month, LAG(num_orders, 7) OVER () AS last_week FROM daily_orders "); } #[test] fn test_window_functions_05() { // sort does not leak out of groups let query = r###" from daily_orders sort day group month (sort num_orders | window expanding:true (derive {rank day})) derive {num_orders_last_week = lag 7 num_orders} "###; assert_snapshot!((compile(query).unwrap()), @r" SELECT *, RANK() OVER ( PARTITION BY month ORDER BY num_orders ), LAG(num_orders, 7) OVER () AS num_orders_last_week FROM daily_orders "); } #[test] fn test_window_functions_06() { // detect sum as a window function, even without group or window assert_snapshot!((compile(r###" from foo derive {a = sum b} group c ( derive {d = sum b} ) "###).unwrap()), @r" SELECT *, SUM(b) OVER () AS a, SUM(b) OVER (PARTITION BY c) AS d FROM foo "); } #[test] fn test_window_functions_07() { assert_snapshot!((compile(r###" from foo window expanding:true ( derive {running_total = sum b} ) "###).unwrap()), @r" SELECT *, SUM(b) OVER (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total FROM foo "); } #[test] fn test_window_functions_08() { assert_snapshot!((compile(r###" from foo window rolling:3 ( derive {last_three = sum b} ) "###).unwrap()), @r" SELECT *, SUM(b) OVER (ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS last_three FROM foo "); } #[test] fn test_window_functions_09() { assert_snapshot!((compile(r###" from foo window rows:0..4 ( derive {next_four_rows = sum b} ) "###).unwrap()), @r" SELECT *, SUM(b) OVER ( ROWS BETWEEN CURRENT ROW AND 4 FOLLOWING ) AS next_four_rows FROM foo "); } #[test] fn test_window_functions_10() { assert_snapshot!((compile(r###" from foo sort day window range:-4..4 ( derive {next_four_days = sum b} ) "###).unwrap()), @r" SELECT *, SUM(b) OVER ( ORDER BY day RANGE BETWEEN 4 PRECEDING AND 4 FOLLOWING ) AS next_four_days FROM foo ORDER BY day "); } #[test] fn test_window_functions_11() { assert_snapshot!((compile(r###" from employees sort age derive {num = row_number this} "###).unwrap()), @r" SELECT *, ROW_NUMBER() OVER ( ORDER BY age ) AS num FROM employees ORDER BY age "); } #[test] fn test_window_functions_12() { // window params need to be simple expressions assert_snapshot!((compile(r###" from x derive {b = lag 1 a} window ( sort b derive {c = lag 1 a} ) "###).unwrap()), @r" WITH table_0 AS ( SELECT *, LAG(a, 1) OVER () AS b FROM x ) SELECT *, LAG(a, 1) OVER ( ORDER BY b ) AS c FROM table_0 ORDER BY b "); assert_snapshot!((compile(r###" from x derive {b = lag 1 a} group b ( derive {c = lag 1 a} ) "###).unwrap()), @r" WITH table_0 AS ( SELECT LAG(a, 1) OVER () AS b, * FROM x ) SELECT *, LAG(a, 1) OVER (PARTITION BY b) AS c FROM table_0 "); } #[test] fn test_window_functions_13() { // window params need to be simple expressions assert_snapshot!((compile(r###" from tracks group {album_id} ( window (derive {grp = milliseconds - (row_number this)}) ) group {grp} ( window (derive {count = row_number this}) ) "###).unwrap()), @r" WITH table_0 AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY album_id) AS _expr_0 FROM tracks ) SELECT milliseconds - _expr_0 AS grp, *, ROW_NUMBER() OVER (PARTITION BY milliseconds - _expr_0) AS count FROM table_0 "); } #[test] fn test_window_single_item_range() { assert_snapshot!(compile(r###" from login_event window rows:1..1 ( sort time_upload derive { last_user = min user_id } ) "###).unwrap(), @r" SELECT *, MIN(user_id) OVER ( ORDER BY time_upload ROWS BETWEEN 1 FOLLOWING AND 1 FOLLOWING ) AS last_user FROM login_event ORDER BY time_upload "); } #[test] fn test_name_resolving() { let query = r###" from numbers derive x = 5 select {y = 6, z = x + y + a} "###; assert_snapshot!((compile(query).unwrap()), @r" SELECT 6 AS y, 5 + 6 + a AS z FROM numbers "); } #[test] fn test_strings() { let query = r#" from empty_table_to_do select { x = "two households'", y = 'two households"', z = f"a {x} b' {y} c", v = f'a {x} b" {y} c', } "#; assert_snapshot!((compile(query).unwrap()), @r#" SELECT 'two households''' AS x, 'two households"' AS y, CONCAT( 'a ', 'two households''', ' b'' ', 'two households"', ' c' ) AS z, CONCAT( 'a ', 'two households''', ' b" ', 'two households"', ' c' ) AS v FROM empty_table_to_do "#); } #[test] fn test_filter() { // https://github.com/PRQL/prql/issues/469 let query = r###" from employees filter {age > 25, age < 40} "###; assert!(compile(query).is_err()); assert_snapshot!((compile(r###" from employees filter age > 25 && age < 40 "###).unwrap()), @r" SELECT * FROM employees WHERE age > 25 AND age < 40 "); assert_snapshot!((compile(r###" from employees filter age > 25 filter age < 40 "###).unwrap()), @r" SELECT * FROM employees WHERE age > 25 AND age < 40 "); } #[test] fn test_nulls_01() { assert_snapshot!((compile(r###" from employees select amount = null "###).unwrap()), @r" SELECT NULL AS amount FROM employees "); } #[test] fn test_nulls_02() { // coalesce assert_snapshot!((compile(r###" from employees derive amount = amount + 2 ?? 3 * 5 "###).unwrap()), @r" SELECT *, COALESCE(amount + 2, 3 * 5) AS amount FROM employees "); } #[test] fn test_nulls_03() { // IS NULL assert_snapshot!((compile(r###" from employees filter first_name == null && null == last_name "###).unwrap()), @r" SELECT * FROM employees WHERE first_name IS NULL AND last_name IS NULL "); } #[test] fn test_nulls_04() { // IS NOT NULL assert_snapshot!((compile(r###" from employees filter first_name != null && null != last_name "###).unwrap()), @r" SELECT * FROM employees WHERE first_name IS NOT NULL AND last_name IS NOT NULL "); } #[test] fn test_take_01() { assert_snapshot!((compile(r###" from employees take ..10 "###).unwrap()), @r" SELECT * FROM employees LIMIT 10 "); } #[test] fn test_take_02() { assert_snapshot!((compile(r###" from employees take 5..10 "###).unwrap()), @r" SELECT * FROM employees LIMIT 6 OFFSET 4 "); } #[test] fn test_take_03() { assert_snapshot!((compile(r###" from employees take 5.. "###).unwrap()), @r" SELECT * FROM employees OFFSET 4 "); } #[test] fn test_take_04() { assert_snapshot!((compile(r###" from employees take 5..5 "###).unwrap()), @r" SELECT * FROM employees LIMIT 1 OFFSET 4 "); } #[test] fn test_take_05() { // should be one SELECT assert_snapshot!((compile(r###" from employees take 11..20 take 1..5 "###).unwrap()), @r" SELECT * FROM employees LIMIT 5 OFFSET 10 "); } #[test] fn test_take_06() { // should be two SELECTTs assert_snapshot!((compile(r###" from employees take 11..20 sort name take 1..5 "###).unwrap()), @r" WITH table_0 AS ( SELECT * FROM employees LIMIT 10 OFFSET 10 ) SELECT * FROM table_0 ORDER BY name LIMIT 5 "); } #[test] fn test_take_07() { assert_snapshot!((compile(r###" from employees take 0..1 "###).unwrap_err()), @r" Error: ╭─[ :3:5 ] │ 3 │ take 0..1 │ ────┬──── │ ╰────── take expected a positive int range, but found 0..1 ───╯ "); } #[test] fn test_take_08() { assert_snapshot!((compile(r###" from employees take (-1..) "###).unwrap_err()), @r" Error: ╭─[ :3:5 ] │ 3 │ take (-1..) │ ─────┬───── │ ╰─────── take expected a positive int range, but found -1.. ───╯ "); } #[test] fn test_take_09() { assert_snapshot!((compile(r###" from employees select a take 5..5.6 "###).unwrap_err()), @r" Error: ╭─[ :4:5 ] │ 4 │ take 5..5.6 │ ─────┬───── │ ╰─────── take expected a positive int range, but found 5..? ───╯ "); } #[test] fn test_take_10() { assert_snapshot!((compile(r###" from employees take (-1) "###).unwrap_err()), @r" Error: ╭─[ :3:5 ] │ 3 │ take (-1) │ ────┬──── │ ╰────── take expected a positive int range, but found ..-1 ───╯ "); } #[test] fn test_take_mssql() { assert_snapshot!((compile(r#" prql target:sql.mssql from tracks take 3..5 "#).unwrap()), @r" SELECT * FROM tracks ORDER BY ( SELECT NULL ) OFFSET 2 ROWS FETCH FIRST 3 ROWS ONLY "); assert_snapshot!((compile(r#" prql target:sql.mssql from tracks take ..5 "#).unwrap()), @r" SELECT * FROM tracks ORDER BY ( SELECT NULL ) OFFSET 0 ROWS FETCH FIRST 5 ROWS ONLY "); assert_snapshot!((compile(r#" prql target:sql.mssql from tracks take 3.. "#).unwrap()), @r" SELECT * FROM tracks OFFSET 2 ROWS "); } #[test] fn test_mssql_distinct_fetch() { // Issue #5628: MSSQL requires ORDER BY items to appear in SELECT list when DISTINCT is used. // Using (SELECT NULL) for ORDER BY with DISTINCT is invalid in MSSQL. // Case 1: UnnamedExpr - simple column reference assert_snapshot!((compile(r#" prql target:sql.mssql from t take 100 group {this.`District`} (take 1) select {this.`District`} "#).unwrap()), @r###" SELECT DISTINCT "District" FROM t ORDER BY "District" OFFSET 0 ROWS FETCH FIRST 100 ROWS ONLY "###); // Case 2: ExprWithAlias - uses the alias for ORDER BY assert_snapshot!((compile(r#" prql target:sql.mssql from t take 100 group {d = this.`District`} (take 1) select {d} "#).unwrap()), @r###" SELECT DISTINCT "District" AS d FROM t ORDER BY d OFFSET 0 ROWS FETCH FIRST 100 ROWS ONLY "###); // Case 3: Multiple columns - uses first column for ORDER BY assert_snapshot!((compile(r#" prql target:sql.mssql from t take 100 group {this.`A`, this.`B`} (take 1) select {this.`A`, this.`B`} "#).unwrap()), @r###" SELECT DISTINCT "A", "B" FROM t ORDER BY "A" OFFSET 0 ROWS FETCH FIRST 100 ROWS ONLY "###); } #[test] fn test_distinct_01() { // window functions cannot materialize into where statement: CTE is needed assert_snapshot!((compile(r###" from employees derive {rn = row_number id} filter rn > 2 "###).unwrap()), @r" WITH table_0 AS ( SELECT *, ROW_NUMBER() OVER () AS rn FROM employees ) SELECT * FROM table_0 WHERE rn > 2 "); } #[test] fn test_distinct_02() { // basic distinct assert_snapshot!((compile(r###" from employees select first_name group first_name (take 1) "###).unwrap()), @r" SELECT DISTINCT first_name FROM employees "); } #[test] fn test_distinct_03() { // distinct on two columns assert_snapshot!((compile(r###" from employees select {first_name, last_name} group {first_name, last_name} (take 1) "###).unwrap()), @r" SELECT DISTINCT first_name, last_name FROM employees "); } #[test] fn test_distinct_04() { // We want distinct only over first_name and last_name, so we can't use a // `DISTINCT *` here. assert_snapshot!((compile(r###" from employees group {first_name, last_name} (take 1) "###).unwrap()), @r" WITH table_0 AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY first_name, last_name) AS _expr_0 FROM employees ) SELECT * FROM table_0 WHERE _expr_0 <= 1 "); } #[test] fn test_distinct_05() { // Check that a different order doesn't stop distinct from being used. assert!(compile( "from employees | select {first_name, last_name} | group {last_name, first_name} (take 1)" ) .unwrap() .contains("DISTINCT")); } #[test] fn test_distinct_06() { // head assert_snapshot!((compile(r###" from employees group department (take 3) "###).unwrap()), @r" WITH table_0 AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY department) AS _expr_0 FROM employees ) SELECT * FROM table_0 WHERE _expr_0 <= 3 "); } #[test] fn test_distinct_07() { assert_snapshot!((compile(r###" from employees group department (sort salary | take 2..3) "###).unwrap()), @r" WITH table_0 AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY department ORDER BY salary ) AS _expr_0 FROM employees ) SELECT * FROM table_0 WHERE _expr_0 BETWEEN 2 AND 3 "); } #[test] fn test_distinct_08() { assert_snapshot!((compile(r###" from employees group department (sort salary | take 4..4) "###).unwrap()), @r" WITH table_0 AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY department ORDER BY salary ) AS _expr_0 FROM employees ) SELECT * FROM table_0 WHERE _expr_0 = 4 "); } #[test] fn test_distinct_09() { assert_snapshot!(compile(" from invoices select {billing_country, billing_city} group {billing_city} ( take 1 ) sort billing_city ").unwrap(), @r" WITH table_0 AS ( SELECT billing_city, billing_country, ROW_NUMBER() OVER (PARTITION BY billing_city) AS _expr_0 FROM invoices ) SELECT billing_city, billing_country FROM table_0 WHERE _expr_0 <= 1 ORDER BY billing_city "); } #[test] fn test_distinct_on_01() { assert_snapshot!((compile(r###" prql target:sql.postgres from employees group department ( sort age take 1 ) "###).unwrap()), @r" SELECT DISTINCT ON (department) * FROM employees ORDER BY department, age "); } #[test] fn test_distinct_on_02() { assert_snapshot!((compile(r###" prql target:sql.duckdb from x select {class, begins} group {begins} (take 1) "###).unwrap()), @r" SELECT DISTINCT ON (begins) begins, class FROM x "); } #[test] fn test_distinct_on_03() { assert_snapshot!((compile(r###" prql target:sql.duckdb from tab1 group col1 ( take 1 ) derive foo = 1 select foo "###).unwrap()), @r" WITH table_0 AS ( SELECT DISTINCT ON (col1) * FROM tab1 ) SELECT 1 AS foo FROM table_0 "); } #[test] fn test_distinct_on_04() { assert_snapshot!((compile(r###" prql target:sql.duckdb from a join b (b.a_id == a.id) group {a.id} ( sort b.x take 1 ) select {a.id, b.y} "###).unwrap()), @r" SELECT DISTINCT ON (a.id) a.id, b.y FROM a INNER JOIN b ON b.a_id = a.id ORDER BY a.id, b.x "); } #[test] fn test_group_take_n_01() { assert_snapshot!((compile(r###" prql target:sql.postgres from employees group department ( sort age take 2 ) "###).unwrap()), @r" WITH table_0 AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY department ORDER BY age ) AS _expr_0 FROM employees ) SELECT * FROM table_0 WHERE _expr_0 <= 2 "); } #[test] fn test_group_take_n_02() { assert_snapshot!((compile(r###" prql target:sql.postgres from employees group department ( sort age take 2.. ) "###).unwrap()), @r" WITH table_0 AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY department ORDER BY age ) AS _expr_0 FROM employees ) SELECT * FROM table_0 WHERE _expr_0 >= 2 "); } #[test] fn test_join() { assert_snapshot!((compile(r###" from x join y (==id) "###).unwrap()), @r" SELECT x.*, y.* FROM x INNER JOIN y ON x.id = y.id "); compile("from x | join y {==x.id}").unwrap_err(); } #[test] fn test_join_side_literal() { assert_snapshot!((compile(r###" let my_side = "right" from x join y (==id) side:my_side "###).unwrap()), @r" SELECT x.*, y.* FROM x RIGHT OUTER JOIN y ON x.id = y.id "); } #[test] fn test_join_side_literal_err() { assert_snapshot!((compile(r###" let my_side = 42 from x join y (==id) side:my_side "###).unwrap_err()), @r" Error: ╭─[ :5:24 ] │ 5 │ join y (==id) side:my_side │ ───┬─── │ ╰───── `side` expected inner, left, right or full, but found 42 ───╯ "); } #[test] fn test_join_side_literal_via_func() { assert_snapshot!((compile(r###" let my_join = func m c s :"right" tbl -> ( join side:_param.s m (c == that.k) tbl ) from x my_join default_db.y this.id s:"left" "###).unwrap()), @r" SELECT x.*, y.* FROM x LEFT OUTER JOIN y ON x.id = y.k "); } #[test] fn test_join_side_literal_via_func_err() { assert_snapshot!((compile(r###" let my_join = func m c s :"right" tbl -> ( join side:_param.s m (c == that.k) tbl ) from x my_join default_db.y this.id s:"four" "###).unwrap_err()), @r#" Error: ╭─[ :3:19 ] │ 3 │ join side:_param.s m (c == that.k) tbl │ ────┬─── │ ╰───── `side` expected inner, left, right or full, but found "four" ───╯ "#); } #[test] fn test_join_with_param_name_collision() { // Regression test for issue #5015 // When joining tables that both have a column named "source", // the compiler should not report an ambiguous name error due to // the "source" parameter from the std library's "from" function. assert_snapshot!((compile(r###" let a = ( from events_a select { event_id, source, } ) from a join a (==event_id) select { event_id = a.event_id, } "###).unwrap()), @r" WITH a AS ( SELECT event_id, source FROM events_a ) SELECT table_0.event_id FROM a INNER JOIN a AS table_0 ON a.event_id = table_0.event_id "); } #[test] fn test_from_json() { // Test that the SQL generated from the JSON of the PRQL is the same as the raw PRQL let original_prql = r#" from e=employees join salaries (==emp_no) group {e.emp_no, e.gender} ( aggregate { emp_salary = average salaries.salary } ) join de=dept_emp (==emp_no) join dm=dept_manager ( (dm.dept_no == de.dept_no) && s"(de.from_date, de.to_date) OVERLAPS (dm.from_date, dm.to_date)" ) group {dm.emp_no, gender} ( aggregate { salary_avg = average emp_salary, salary_sd = stddev emp_salary } ) derive mng_no = emp_no join managers=employees (==emp_no) derive mng_name = s"managers.first_name || ' ' || managers.last_name" select {mng_name, managers.gender, salary_avg, salary_sd} "#; let source_tree = SourceTree::from(original_prql); let sql_from_prql = Ok(prqlc::prql_to_pl_tree(&source_tree).unwrap()) .and_then(|ast| prqlc::semantic::resolve_and_lower(ast, &[], None)) .and_then(|rq| sql::compile(rq, &Options::default())) .unwrap(); let sql_from_json = prqlc::prql_to_pl(original_prql) .map(|x| prqlc::json::from_pl(&x).unwrap()) .map(|json| prqlc::json::to_pl(&json).unwrap()) .and_then(prqlc::pl_to_rq) .and_then(|rq| prqlc::rq_to_sql(rq, &Options::default())) .unwrap(); assert_eq!(sql_from_prql, sql_from_json); } #[test] fn test_f_string() { let query = r#" from employees derive age = year_born - s'now()' select { f"Hello my name is {prefix}{first_name} {last_name}", f"and I am {age} years old." } "#; assert_snapshot!( compile(query).unwrap(), @r" SELECT CONCAT( 'Hello my name is ', prefix, first_name, ' ', last_name ), CONCAT('and I am ', year_born - now(), ' years old.') FROM employees " ); assert_snapshot!( prqlc::compile( query, &Options::default() .no_signature() .with_target(Target::Sql(Some(sql::Dialect::SQLite))) ).unwrap(), @r" SELECT 'Hello my name is ' || prefix || first_name || ' ' || last_name, 'and I am ' || year_born - now() || ' years old.' FROM employees " ); } #[test] fn test_sql_of_ast_1() { let query = r#" from employees filter country == "USA" group {title, country} ( aggregate {average salary} ) sort title take 20 "#; let sql = compile(query).unwrap(); assert_snapshot!(sql, @r" SELECT title, country, AVG(salary) FROM employees WHERE country = 'USA' GROUP BY title, country ORDER BY title LIMIT 20 " ); } #[test] fn test_sql_of_ast_02() { assert_snapshot!(compile(r#" from employees aggregate sum_salary = s"sum({salary})" filter sum_salary > 100 "#).unwrap(), @r" SELECT sum(salary) AS sum_salary FROM employees HAVING sum(salary) > 100 "); } #[test] fn test_bare_s_string() { let query = r#" let grouping = s""" SELECT SUM(a) FROM tbl GROUP BY GROUPING SETS ((b, c, d), (d), (b, d)) """ from grouping "#; let sql = compile(query).unwrap(); assert_snapshot!(sql, @r" WITH table_0 AS ( SELECT SUM(a) FROM tbl GROUP BY GROUPING SETS ((b, c, d), (d), (b, d)) ) SELECT * FROM table_0 " ); } #[test] fn test_bare_s_string_01() { // Test that case insensitive SELECT is accepted. We allow it as it is valid SQL. assert_snapshot!(compile(r#" let a = s"select insensitive from rude" from a "#).unwrap(), @r" WITH table_0 AS ( SELECT insensitive from rude ) SELECT insensitive FROM table_0 " ); } #[test] fn test_bare_s_string_02() { // Check a mixture of cases for good measure. assert_snapshot!(compile(r#" let a = s"sElEcT insensitive from rude" from a "#).unwrap(), @r" WITH table_0 AS ( SELECT insensitive from rude ) SELECT insensitive FROM table_0 " ); } #[test] fn test_bare_s_string_03() { // Check SELECT\n. assert_snapshot!(compile(r#" let a = s" SELECT foo FROM bar" from a "#).unwrap(), @r" WITH table_0 AS ( SELECT foo FROM bar ) SELECT foo FROM table_0 "); } #[test] fn test_bare_s_string_04() { assert_snapshot!(compile(r#" s"SELECTTfoo" "#).unwrap_err(), @r" Error: s-strings representing a table must start with `SELECT ` ↳ Hint: this is a limitation by current compiler implementation "); } #[test] // Confirm that a regular expr_call in a table definition works as expected. fn test_table_definition_with_expr_call() { let query = r###" let e = take 4 (from employees) from e "###; let sql = compile(query).unwrap(); assert_snapshot!(sql, @r" WITH e AS ( SELECT * FROM employees LIMIT 4 ) SELECT * FROM e " ); } #[test] fn test_prql_to_sql_1() { assert_snapshot!(compile(r#" from employees aggregate { count salary, sum salary, } "#).unwrap(), @r" SELECT COUNT(*), COALESCE(SUM(salary), 0) FROM employees " ); assert_snapshot!(compile(r#" prql target:sql.postgres from developers group team ( aggregate { skill_width = count_distinct specialty, } ) "#).unwrap(), @r" SELECT team, COUNT(DISTINCT specialty) AS skill_width FROM developers GROUP BY team " ) } #[test] #[ignore] fn test_prql_to_sql_2() { let query = r#" from employees filter country == "USA" # Each line transforms the previous result. derive { # This adds columns / variables. gross_salary = salary + payroll_tax, gross_cost = gross_salary + benefits_cost # Variables can use other variables. } filter gross_cost > 0 group {title, country} ( aggregate { # `by` are the columns to group by. average salary, # These are aggregation calcs run on each group. sum salary, average gross_salary, sum gross_salary, average gross_cost, sum_gross_cost = sum gross_cost, ct = count salary, } ) sort sum_gross_cost filter ct > 200 take 20 "#; let sql = compile(query).unwrap(); assert_snapshot!(sql, @r###" WITH table_0 AS ( SELECT title, country, salary + payroll_tax + benefits_cost AS _expr_0, salary + payroll_tax AS _expr_1, salary FROM employees WHERE country = 'USA' ) SELECT title, country, AVG(salary), COALESCE(SUM(salary), 0), AVG(_expr_1), COALESCE(SUM(_expr_1), 0), AVG(_expr_0), COALESCE(SUM(_expr_0), 0) AS sum_gross_cost, COUNT(*) AS ct FROM table_0 WHERE _expr_0 > 0 GROUP BY title, country HAVING COUNT(*) > 200 ORDER BY sum_gross_cost LIMIT 20 "###); } #[test] fn test_prql_to_sql_table() { // table let query = r#" let newest_employees = ( from employees sort tenure take 50 ) let average_salaries = ( from salaries group country ( aggregate { average_country_salary = average salary } ) ) from newest_employees join average_salaries (==country) select {name, salary, average_country_salary} "#; let sql = compile(query).unwrap(); assert_snapshot!(sql, @" WITH newest_employees AS ( SELECT * FROM employees ORDER BY tenure LIMIT 50 ), average_salaries AS ( SELECT country, AVG(salary) AS average_country_salary FROM salaries GROUP BY country ) SELECT newest_employees.name, newest_employees.salary, average_salaries.average_country_salary FROM newest_employees INNER JOIN average_salaries ON newest_employees.country = average_salaries.country ORDER BY employees.tenure " ); } #[test] fn test_nonatomic() { // A take, then two aggregates let query = r#" from employees take 20 filter country == "USA" group {title, country} ( aggregate { salary = average salary } ) group {title, country} ( aggregate { sum_gross_cost = average salary } ) sort sum_gross_cost "#; assert_snapshot!((compile(query).unwrap()), @r" WITH table_1 AS ( SELECT title, country, salary FROM employees LIMIT 20 ), table_0 AS ( SELECT title, country, AVG(salary) AS _expr_0 FROM table_1 WHERE country = 'USA' GROUP BY title, country ) SELECT title, country, AVG(_expr_0) AS sum_gross_cost FROM table_0 GROUP BY title, country ORDER BY sum_gross_cost "); // A aggregate, then sort and filter let query = r###" from employees group {title, country} ( aggregate { sum_gross_cost = average salary } ) sort sum_gross_cost filter sum_gross_cost > 0 "###; assert_snapshot!((compile(query).unwrap()), @r" SELECT title, country, AVG(salary) AS sum_gross_cost FROM employees GROUP BY title, country HAVING AVG(salary) > 0 ORDER BY sum_gross_cost "); } #[test] /// Confirm a nonatomic table works. fn test_nonatomic_table() { // A take, then two aggregates let query = r#" let a = ( from employees take 50 group country (aggregate {s"count(*)"}) ) from a join b (==country) select {name, salary, average_country_salary} "#; assert_snapshot!((compile(query).unwrap()), @r" WITH table_0 AS ( SELECT country FROM employees LIMIT 50 ), a AS ( SELECT country, count(*) FROM table_0 GROUP BY country ) SELECT b.name, b.salary, b.average_country_salary FROM a INNER JOIN b ON a.country = b.country "); } #[test] fn test_table_names_between_splits_01() { assert_snapshot!(compile(r###" from employees join d = department (==dept_no) take 10 derive emp_no = employees.emp_no join s = salaries (==emp_no) select {employees.emp_no, d.name, s.salary} "###).unwrap(), @r" WITH table_0 AS ( SELECT employees.emp_no, d.name FROM employees INNER JOIN department AS d ON employees.dept_no = d.dept_no LIMIT 10 ) SELECT table_0.emp_no, table_0.name, s.salary FROM table_0 INNER JOIN salaries AS s ON table_0.emp_no = s.emp_no "); } #[test] fn test_table_names_between_splits_02() { assert_snapshot!(compile(r###" from e = employees take 10 join salaries (==emp_no) select {e.*, salaries.salary} "###).unwrap(), @r" WITH table_0 AS ( SELECT * FROM employees AS e LIMIT 10 ) SELECT table_0.*, salaries.salary FROM table_0 INNER JOIN salaries ON table_0.emp_no = salaries.emp_no "); } #[test] fn test_table_alias_01() { assert_snapshot!((compile(r###" from e = employees join salaries side:left (salaries.emp_no == e.emp_no) group {e.emp_no} ( aggregate { emp_salary = average salaries.salary } ) select {emp_no, emp_salary} "###).unwrap()), @r" SELECT e.emp_no, AVG(salaries.salary) AS emp_salary FROM employees AS e LEFT OUTER JOIN salaries ON salaries.emp_no = e.emp_no GROUP BY e.emp_no "); } #[test] fn test_table_alias_02() { assert_snapshot!((compile(r#" from e = employees select e.first_name filter e.first_name == "Fred" "#).unwrap()), @r" SELECT first_name FROM employees AS e WHERE first_name = 'Fred' "); } #[test] fn test_targets() { // Generic let query = r###" prql target:sql.generic from Employees select {FirstName, `last name`} take 3 "###; assert_snapshot!((compile(query).unwrap()), @r#" SELECT "FirstName", "last name" FROM "Employees" LIMIT 3 "#); // SQL server let query = r###" prql target:sql.mssql from Employees select {FirstName, `last name`} take 3 "###; assert_snapshot!((compile(query).unwrap()), @r#" SELECT "FirstName", "last name" FROM "Employees" ORDER BY ( SELECT NULL ) OFFSET 0 ROWS FETCH FIRST 3 ROWS ONLY "#); // MySQL let query = r###" prql target:sql.mysql from Employees select {FirstName, `last name`} take 3 "###; assert_snapshot!((compile(query).unwrap()), @r" SELECT `FirstName`, `last name` FROM `Employees` LIMIT 3 "); } #[test] fn test_target_clickhouse() { let query = r###" prql target:sql.clickhouse from github_json derive {event_type_dotted = `event.type`} "###; assert_snapshot!((compile(query).unwrap()), @r" SELECT *, `event.type` AS event_type_dotted FROM github_json "); } #[test] fn test_ident_escaping() { // Generic let query = r#" from `anim"ls` derive {`čebela` = BeeName, medved = `bear's_name`} "#; assert_snapshot!((compile(query).unwrap()), @r#" SELECT *, "BeeName" AS "čebela", "bear's_name" AS medved FROM "anim""ls" "#); // MySQL let query = r#" prql target:sql.mysql from `anim"ls` derive {`čebela` = BeeName, medved = `bear's_name`} "#; assert_snapshot!((compile(query).unwrap()), @r#" SELECT *, `BeeName` AS `čebela`, `bear's_name` AS medved FROM `anim"ls` "#); } #[test] fn test_literal() { let query = r###" from employees derive {always_true = true} "###; let sql = compile(query).unwrap(); assert_snapshot!(sql, @r" SELECT *, true AS always_true FROM employees " ); } #[test] fn test_same_column_names() { // #820 let query = r###" let x = ( from x_table select only_in_x = foo ) let y = ( from y_table select foo ) from x join y (foo == only_in_x) "###; assert_snapshot!(compile(query).unwrap(), @r" WITH x AS ( SELECT foo AS only_in_x FROM x_table ), y AS ( SELECT foo FROM y_table ) SELECT x.only_in_x, y.foo FROM x INNER JOIN y ON y.foo = x.only_in_x " ); } #[test] fn test_double_aggregate() { // #941 compile( r###" from numbers group {type} ( aggregate { total_amt = sum amount, } aggregate { max amount } ) "###, ) .unwrap_err(); assert_snapshot!(compile(r###" from numbers group {`type`} ( aggregate { total_amt = sum amount, max amount } ) "###).unwrap(), @r" SELECT type, COALESCE(SUM(amount), 0) AS total_amt, MAX(amount) FROM numbers GROUP BY type " ); } #[test] fn test_window_function_coalesce() { // #3587 assert_snapshot!(compile(r###" from x select {a, b=a} window ( select { cumsum_a=(sum a), cumsum_b=(sum b) } ) "###).unwrap(), @r" SELECT SUM(a) OVER () AS cumsum_a, SUM(a) OVER () AS cumsum_b FROM x " ); } #[test] fn test_casting() { assert_snapshot!(compile(r###" from x select {a} derive { b = (a | as int) + 10, c = (a | as int) - 10, d = (a | as float) * 10, e = (a | as float) / 10, } "###).unwrap(), @r" SELECT a, CAST(a AS int) + 10 AS b, CAST(a AS int) - 10 AS c, CAST(a AS float) * 10 AS d, CAST(a AS float) / 10 AS e FROM x " ); } #[test] fn test_toposort() { // #1183 assert_snapshot!(compile(r###" let b = ( from somesource ) let a = ( from b ) from b "###).unwrap(), @r" WITH b AS ( SELECT * FROM somesource ) SELECT * FROM b " ); } #[test] fn test_inline_tables() { assert_snapshot!(compile(r###" ( from employees select {emp_id, name, surname, `type`, amount} ) join s = (from salaries | select {emp_id, salary}) (==emp_id) "###).unwrap(), @r" WITH table_0 AS ( SELECT emp_id, salary FROM salaries ) SELECT employees.emp_id, employees.name, employees.surname, employees.type, employees.amount, table_0.emp_id, table_0.salary FROM employees INNER JOIN table_0 ON employees.emp_id = table_0.emp_id " ); } #[test] fn test_filter_and_select_unchanged_alias() { // #1185 assert_snapshot!(compile(r###" from account filter account.name != null select {name = account.name} "###).unwrap(), @r" SELECT name FROM account WHERE name IS NOT NULL " ); } #[test] fn test_filter_and_select_changed_alias() { // #1185 assert_snapshot!(compile(r###" from account filter account.name != null select {renamed_name = account.name} "###).unwrap(), @r" SELECT name AS renamed_name FROM account WHERE name IS NOT NULL " ); // #1207 assert_snapshot!(compile(r#" from x filter name != "Bob" select name = name ?? "Default" "#).unwrap(), @r" SELECT COALESCE(name, 'Default') AS name FROM x WHERE name <> 'Bob' " ); } #[test] fn test_unused_alias() { // #1308 assert_snapshot!(compile(r###" from account select n = {account.name} "###).unwrap_err(), @r" Error: ╭─[ :3:16 ] │ 3 │ select n = {account.name} │ ───────┬────── │ ╰──────── unexpected assign to `n` │ │ Help: move assign into the tuple: `[n = ...]` ───╯ ") } #[test] fn test_table_s_string_01() { assert_snapshot!(compile(r#" let main = s"SELECT DISTINCT ON first_name, age FROM employees ORDER BY age ASC" "#).unwrap(), @r" WITH table_0 AS ( SELECT DISTINCT ON first_name, age FROM employees ORDER BY age ASC ) SELECT * FROM table_0 " ); } #[test] fn test_table_s_string_02() { assert_snapshot!(compile(r#" s""" SELECT DISTINCT ON first_name, id, age FROM employees ORDER BY age ASC """ join s = s"SELECT * FROM salaries" (==id) "#).unwrap(), @r" WITH table_0 AS ( SELECT DISTINCT ON first_name, id, age FROM employees ORDER BY age ASC ), table_1 AS ( SELECT * FROM salaries ) SELECT table_0.*, table_1.* FROM table_0 INNER JOIN table_1 ON table_0.id = table_1.id " ); } #[test] fn test_table_s_string_03() { assert_snapshot!(compile(r#" s"""SELECT * FROM employees""" filter country == "USA" "#).unwrap(), @r" WITH table_0 AS ( SELECT * FROM employees ) SELECT * FROM table_0 WHERE country = 'USA' " ); } #[test] fn test_table_s_string_04() { assert_snapshot!(compile(r#" s"""SELECT * FROM employees""" select {e = this} filter e.country == "USA" "#).unwrap(), @r" WITH table_0 AS ( SELECT * FROM employees ) SELECT * FROM table_0 WHERE country = 'USA' " ); } #[test] fn test_table_s_string_05() { assert_snapshot!(compile(r#" let weeks_between = start end -> s"SELECT generate_series({start}, {end}, '1 week') as date" let current_week = -> s"date(date_trunc('week', current_date))" weeks_between @2022-06-03 (current_week + 4) "#).unwrap(), @r" WITH table_0 AS ( SELECT generate_series( DATE '2022-06-03', date(date_trunc('week', current_date)) + 4, '1 week' ) as date ) SELECT * FROM table_0 " ); } #[test] fn test_table_s_string_06() { assert_snapshot!(compile(r#" s"SELECT * FROM {default_db.x}" "#).unwrap(), @r" WITH table_0 AS ( SELECT * FROM x ) SELECT * FROM table_0 " ); } #[test] fn test_direct_table_references() { assert_snapshot!(compile( r#" from x select s"{x}.field" "#, ) .unwrap_err(), @r#" Error: ╭─[ :3:15 ] │ 3 │ select s"{x}.field" │ ┬ │ ╰── table instance cannot be referenced directly │ │ Help: column name might be missing? ───╯ "#); assert_snapshot!(compile( r###" from x select x "###, ) .unwrap(), @r" SELECT * FROM x "); } #[test] fn test_table_variable_in_scalar_context() { // https://github.com/PRQL/prql/issues/5158 assert_snapshot!(compile( r#" let mod_id = (from users | filter login == "nightpool" | select id | take 1) from modlog filter actor_id == mod_id "#, ) .unwrap_err(), @r#" Error: ╭─[ :5:24 ] │ 5 │ filter actor_id == mod_id │ ───┬── │ ╰──── table variable cannot be used as a scalar value │ │ Help: use a join instead, or inline the subquery ───╯ "#); } #[test] fn test_name_shadowing() { assert_snapshot!(compile( r###" from x select {a, a, a = a + 1} "###).unwrap(), @r" SELECT a AS _expr_0, a + 1 AS a FROM x " ); assert_snapshot!(compile( r###" from x select a derive a derive a = a + 1 derive a = a + 2 "###).unwrap(), @r" SELECT a AS _expr_0, a + 1, a + 1 + 2 AS a FROM x " ); } #[test] fn test_group_all() { assert_snapshot!(compile( r###" prql target:sql.sqlite from a=albums group a.* (aggregate {count this}) "###).unwrap_err(), @"Error: Target dialect does not support * in this position."); assert_snapshot!(compile( r###" from e=albums group !{genre_id} (aggregate {count this}) "###).unwrap_err(), @"Error: Excluding columns not supported as this position"); } #[test] fn test_output_column_deduplication() { // #1249 assert_snapshot!(compile( r#" from report derive r = s"RANK() OVER ()" filter r == 1 "#).unwrap(), @r" WITH table_0 AS ( SELECT *, RANK() OVER () AS r FROM report ) SELECT * FROM table_0 WHERE r = 1 " ); } #[test] fn test_case_01() { assert_snapshot!(compile( r###" from employees derive display_name = case [ nickname != null => nickname, true => f'{first_name} {last_name}' ] "###).unwrap(), @r" SELECT *, CASE WHEN nickname IS NOT NULL THEN nickname ELSE CONCAT(first_name, ' ', last_name) END AS display_name FROM employees " ); } #[test] fn test_case_02() { assert_snapshot!(compile( r###" from employees derive display_name = case [ nickname != null => nickname, first_name != null => f'{first_name} {last_name}' ] "###).unwrap(), @r" SELECT *, CASE WHEN nickname IS NOT NULL THEN nickname WHEN first_name IS NOT NULL THEN CONCAT(first_name, ' ', last_name) ELSE NULL END AS display_name FROM employees " ); } #[test] fn test_case_03() { assert_snapshot!(compile( r###" from tracks select category = case [ length > avg_length => 'long' ] group category (aggregate {count this}) "###).unwrap(), @r" WITH table_0 AS ( SELECT CASE WHEN length > avg_length THEN 'long' ELSE NULL END AS category, length, avg_length FROM tracks ) SELECT category, COUNT(*) FROM table_0 GROUP BY category " ); } #[test] fn test_sql_options() { let options = Options::default(); let sql = prqlc::compile("from x", &options).unwrap(); assert!(sql.contains('\n')); assert!(sql.contains("-- Generated by")); let options = Options::default().no_signature().no_format(); let sql = prqlc::compile("from x", &options).unwrap(); assert!(!sql.contains('\n')); assert!(!sql.contains("-- Generated by")); } #[test] fn test_static_analysis() { assert_snapshot!(compile( r###" from x select { a = (- (-3)), b = !(!(!(!(!(true))))), a3 = null ?? y, a3 = case [ false == true => 1, 7 == 3 => 2, 7 == y => 3, 7.3 == 7.3 => 4, z => 5, true => 6 ], } "###).unwrap(), @r" SELECT 3 AS a, false AS b, y AS _expr_0, CASE WHEN 7 = y THEN 3 ELSE 4 END AS a3 FROM x " ); } #[test] fn test_closures_and_pipelines() { assert_snapshot!(compile( r#" let addthree = a b c -> s"{a} || {b} || {c}" let arg = myarg myfunc -> ( myfunc myarg ) from y select x = ( addthree "apples" arg "bananas" arg "citrus" ) "#).unwrap(), @r" SELECT 'apples' || 'bananas' || 'citrus' AS x FROM y " ); } #[test] fn test_basic_agg() { assert_snapshot!(compile(r#" from employees aggregate { count salary, count this, } "#).unwrap(), @r" SELECT COUNT(*), COUNT(*) FROM employees " ); } #[test] fn test_exclude_columns_01() { assert_snapshot!(compile(r#" from tracks select {track_id, title, composer, bytes} select !{title, composer} "#).unwrap(), @r" SELECT track_id, bytes FROM tracks " ); } #[test] fn test_exclude_columns_02() { assert_snapshot!(compile(r#" from tracks select {track_id, title, composer, bytes} group !{title, composer} (aggregate {count this}) "#).unwrap(), @r" SELECT track_id, bytes, COUNT(*) FROM tracks GROUP BY track_id, bytes " ); } #[test] fn test_exclude_columns_03() { assert_snapshot!(compile(r#" from artists derive nick = name select !{artists.*} "#).unwrap(), @r" SELECT name AS nick FROM artists " ); } #[test] fn test_exclude_columns_04() { assert_snapshot!(compile(r#" prql target:sql.bigquery from tracks select !{milliseconds,bytes} "#).unwrap(), @r" SELECT * EXCEPT (milliseconds, bytes) FROM tracks " ); } #[test] fn test_exclude_columns_05() { assert_snapshot!(compile(r#" prql target:sql.snowflake from tracks select !{milliseconds,bytes} "#).unwrap(), @r#" SELECT * EXCLUDE ("milliseconds", "bytes") FROM "tracks" "# ); } #[test] fn test_exclude_columns_06() { assert_snapshot!(compile(r#" prql target:sql.duckdb from tracks select !{milliseconds,bytes} "#).unwrap(), @r" SELECT * EXCLUDE (milliseconds, bytes) FROM tracks " ); } #[test] fn test_exclude_columns_07() { assert_snapshot!(compile(r#" prql target:sql.duckdb from s"SELECT * FROM foo" select !{bar} "#).unwrap(), @r" WITH table_0 AS ( SELECT * FROM foo ) SELECT * EXCLUDE (bar) FROM table_0 " ); } #[test] fn test_custom_transforms() { assert_snapshot!(compile(r#" let my_transform = ( derive double = single * 2 sort name ) from tab my_transform take 3 "#).unwrap(), @r" SELECT *, single * 2 AS double FROM tab ORDER BY name LIMIT 3 " ); } #[test] fn test_name_inference() { assert_snapshot!(compile(r#" from albums select {artist_id + album_id} # nothing inferred infer "#).unwrap(), @r" SELECT artist_id + album_id FROM albums " ); let sql1 = compile( r#" from albums select {artist_id} select {albums.artist_id} "#, ) .unwrap(); let sql2 = compile( r#" from albums select {albums.artist_id} select {albums.artist_id} "#, ) .unwrap(); assert_eq!(sql1, sql2); assert_snapshot!( sql1, @r" SELECT artist_id FROM albums " ); } #[test] fn test_from_text_01() { assert_snapshot!(compile(r#" from_text format:csv """ a,b,c 1,2,3 4,5,6 """ select {b, c} "#).unwrap(), @r" WITH table_0 AS ( SELECT '1' AS a, '2' AS b, '3' AS c UNION ALL SELECT '4' AS a, '5' AS b, '6' AS c ) SELECT b, c FROM table_0 " ); } #[test] fn test_from_text_02() { assert_snapshot!(compile(r#" from_text format:json ''' [{"a": 1, "b": "x", "c": false }, {"a": 4, "b": "y", "c": null }] ''' select {b, c} "#).unwrap(), @r" WITH table_0 AS ( SELECT 1 AS a, 'x' AS b, false AS c UNION ALL SELECT 4 AS a, 'y' AS b, NULL AS c ) SELECT b, c FROM table_0 " ); } #[test] fn test_from_text_03() { assert_snapshot!(compile(r#" std.from_text format:json '''{ "columns": ["a", "b", "c"], "data": [ [1, "x", false], [4, "y", null] ] }''' select {b, c} "#).unwrap(), @r" WITH table_0 AS ( SELECT 1 AS a, 'x' AS b, false AS c UNION ALL SELECT 4 AS a, 'y' AS b, NULL AS c ) SELECT b, c FROM table_0 " ); } #[test] fn test_from_text_04() { assert_snapshot!(compile(r#" std.from_text 'a,b' "#).unwrap(), @r" WITH table_0 AS ( SELECT NULL AS a, NULL AS b WHERE false ) SELECT a, b FROM table_0 " ); } #[test] fn test_from_text_05() { assert_snapshot!(compile(r#" std.from_text format:json '''{"columns": ["a", "b", "c"], "data": []}''' "#).unwrap(), @r" WITH table_0 AS ( SELECT NULL AS a, NULL AS b, NULL AS c WHERE false ) SELECT a, b, c FROM table_0 " ); } #[test] fn test_from_text_06() { assert_snapshot!(compile(r#" std.from_text '' "#).unwrap(), @r" WITH table_0 AS ( SELECT NULL WHERE false ) SELECT NULL FROM table_0 " ); } #[test] fn test_from_text_07() { assert_snapshot!(compile(r#" std.from_text format:json '''{"columns": [], "data": [[], []]}''' "#).unwrap(), @r" WITH table_0 AS ( SELECT UNION ALL SELECT ) SELECT NULL FROM table_0 " ); } #[test] fn test_header() { // Test both target & version at the same time let header = format!( r#" prql target:sql.mssql version:"{}.{}" "#, env!("CARGO_PKG_VERSION_MAJOR"), env!("CARGO_PKG_VERSION_MINOR") ); assert_snapshot!(compile(format!(r#" {header} from a take 5 "#).as_str()).unwrap(),@r" SELECT * FROM a ORDER BY ( SELECT NULL ) OFFSET 0 ROWS FETCH FIRST 5 ROWS ONLY "); } #[test] fn test_header_target_error() { assert_snapshot!(compile(r#" prql target:foo from a "#).unwrap_err(),@r#"Error: target `"foo"` not found"#); assert_snapshot!(compile(r#" prql target:sql.foo from a "#).unwrap_err(),@r#"Error: target `"sql.foo"` not found"#); assert_snapshot!(compile(r#" prql target:foo.bar from a "#).unwrap_err(),@r#"Error: target `"foo.bar"` not found"#); // TODO: Can we use the span of: // - Ideally just `dialect`? // - At least not the first empty line? assert_snapshot!(compile(r#" prql dialect:foo.bar from a "#).unwrap_err(),@r" Error: ╭─[ :1:1 ] │ 1 │ ╭─▶ 2 │ ├─▶ prql dialect:foo.bar │ │ │ ╰────────────────────────────── unknown query definition arguments `dialect` ───╯ "); } #[test] fn shortest_prql_version() { let mut escape_version = insta::Settings::new(); escape_version.add_filter(r"'.*'", "[VERSION]"); escape_version.bind(|| { assert_snapshot!(compile(r#"[{version = prql.version}]"#).unwrap(),@r" WITH table_0 AS ( SELECT [VERSION] AS version ) SELECT version FROM table_0 "); assert_snapshot!(compile(r#" from x derive y = std.prql.version "#).unwrap(),@r" SELECT *, [VERSION] AS y FROM x "); }) } #[test] fn test_loop() { assert_snapshot!(compile(r#" [{n = 1}] select n = n - 2 loop ( select n = n+1 filter n<5 ) select n = n * 2 take 4 "#).unwrap(), @r" WITH RECURSIVE table_0 AS ( SELECT 1 AS n ), table_1 AS ( SELECT n - 2 AS _expr_0 FROM table_0 UNION ALL SELECT _expr_1 FROM ( SELECT _expr_0 + 1 AS _expr_1 FROM table_1 ) AS table_4 WHERE _expr_1 < 5 ) SELECT _expr_0 * 2 AS n FROM table_1 AS table_3 LIMIT 4 " ); } #[test] fn test_loop_2() { assert_snapshot!(compile(r#" read_csv 'employees.csv' filter last_name=="Mitchell" loop ( join manager=employees (manager.employee_id==this.reports_to) select manager.* ) "#).unwrap(), @r" WITH RECURSIVE table_0 AS ( SELECT * FROM read_csv('employees.csv') ), table_1 AS ( SELECT * FROM table_0 WHERE last_name = 'Mitchell' UNION ALL SELECT manager.* FROM table_1 INNER JOIN employees AS manager ON manager.employee_id = table_1.reports_to ) SELECT * FROM table_1 AS table_2 " ); } #[test] fn test_params() { assert_snapshot!(compile(r#" from invoices select {i = this} filter $1 <= i.date || i.date <= $2 select { i.id, i.total, } filter i.total > $3 "#).unwrap(), @r" SELECT id, total FROM invoices WHERE ( $1 <= date OR date <= $2 ) AND total > $3 " ) } // for #1969 #[test] fn test_datetime() { let query = &r#" from test_table select {date = @2022-12-31, time = @08:30, timestamp = @2020-01-01T13:19:55-0800} "#; assert_snapshot!( compile(query).unwrap(), @r" SELECT DATE '2022-12-31' AS date, TIME '08:30' AS time, TIMESTAMP '2020-01-01T13:19:55-0800' AS timestamp FROM test_table " ) } #[test] fn test_datetime_sqlite() { // for #1969 assert_snapshot!(compile(r#" prql target:sql.sqlite from x select { date = @2022-12-31, time = @08:30, time_tz = @03:05+08:00, time_tz2 = @03:05+0800, timestamp1 = @2020-01-01T13:19:55-0800, timestamp2 = @2021-03-14T03:05+0800, timestamp3 = @2021-03-14T03:05+08:00, } "#).unwrap(), @r" SELECT DATE('2022-12-31') AS date, TIME('08:30') AS time, TIME('03:05+08:00') AS time_tz, TIME('03:05+08:00') AS time_tz2, DATETIME('2020-01-01T13:19:55-08:00') AS timestamp1, DATETIME('2021-03-14T03:05+08:00') AS timestamp2, DATETIME('2021-03-14T03:05+08:00') AS timestamp3 FROM x " ); } #[test] fn test_datetime_parsing() { assert_snapshot!(compile(r#" from test_tables select {date = @2022-12-31, time = @08:30, timestamp = @2020-01-01T13:19:55-0800} "#).unwrap(), @r" SELECT DATE '2022-12-31' AS date, TIME '08:30' AS time, TIMESTAMP '2020-01-01T13:19:55-0800' AS timestamp FROM test_tables " ); } #[test] fn test_lower() { assert_snapshot!(compile(r#" from test_tables derive {lower_name = (name | text.lower)} "#).unwrap(), @r" SELECT *, LOWER(name) AS lower_name FROM test_tables " ); } #[test] fn test_upper() { assert_snapshot!(compile(r#" from test_tables derive {upper_name = text.upper name} select {upper_name} "#).unwrap(), @r" SELECT UPPER(name) AS upper_name FROM test_tables " ); } #[test] fn test_1535() { assert_snapshot!(compile(r#" from x.y.z "#).unwrap(), @r" SELECT * FROM x.y.z " ); } #[test] fn test_read_parquet_duckdb() { assert_snapshot!(compile(r#" std.read_parquet 'x.parquet' join (std.read_parquet "y.parquet") (==foo) "#).unwrap(), @r" WITH table_0 AS ( SELECT * FROM read_parquet('x.parquet') ), table_1 AS ( SELECT * FROM read_parquet('y.parquet') ) SELECT table_0.*, table_1.* FROM table_0 INNER JOIN table_1 ON table_0.foo = table_1.foo " ); // TODO: `from x=(read_parquet 'x.parquet')` currently fails } #[test] fn test_read_parquet_with_named_args() { assert_snapshot!(compile_with_sql_dialect(r#" std.read_parquet 'data.parquet' union_by_name:true "#, sql::Dialect::DuckDb).unwrap(), @r" WITH table_0 AS ( SELECT * FROM read_parquet( 'data.parquet', binary_as_string = false, file_row_number = false, hive_partitioning = NULL, union_by_name = true ) ) SELECT * FROM table_0 " ); assert_snapshot!(compile_with_sql_dialect(r#" std.read_parquet 'data.parquet' union_by_name:true binary_as_string:true "#, sql::Dialect::DuckDb).unwrap(), @r" WITH table_0 AS ( SELECT * FROM read_parquet( 'data.parquet', binary_as_string = true, file_row_number = false, hive_partitioning = NULL, union_by_name = true ) ) SELECT * FROM table_0 " ); } #[test] fn test_read_json_duckdb() { assert_snapshot!(compile_with_sql_dialect(r#" from (read_json 'data.json') "#, sql::Dialect::DuckDb).unwrap(), @r" WITH table_0 AS ( SELECT * FROM read_json_auto('data.json') ) SELECT * FROM table_0 " ); } #[test] fn test_read_json_clickhouse() { assert_snapshot!(compile_with_sql_dialect(r#" from (read_json 'data.json') "#, sql::Dialect::ClickHouse).unwrap(), @r" WITH table_0 AS ( SELECT * FROM file('data.json', 'Json') ) SELECT * FROM table_0 " ); } #[test] fn test_read_json_generic() { assert_snapshot!(compile(r#" from (read_json 'data.json') "#).unwrap(), @r" WITH table_0 AS ( SELECT * FROM read_json('data.json') ) SELECT * FROM table_0 " ); } #[test] fn test_excess_columns() { // https://github.com/PRQL/prql/issues/2079 assert_snapshot!(compile(r#" from tracks derive d = track_id sort d select {title} "#).unwrap(), @r" WITH table_0 AS ( SELECT title, track_id AS _expr_0 FROM tracks ) SELECT title FROM table_0 ORDER BY _expr_0 " ); } #[test] fn test_regex_search() { assert_snapshot!(compile(r#" from tracks derive is_bob_marley = artist_name ~= "Bob\\sMarley" "#).unwrap(), @r" SELECT *, REGEXP(artist_name, 'Bob\sMarley') AS is_bob_marley FROM tracks " ); } #[test] fn test_intervals() { assert_snapshot!(compile(r#" from foo select dt = 1years + 1months + 1weeks + 1days + 1hours + 1minutes + 1seconds + 1milliseconds + 1microseconds "#).unwrap(), @r" SELECT INTERVAL 1 YEAR + INTERVAL 1 MONTH + INTERVAL 1 WEEK + INTERVAL 1 DAY + INTERVAL 1 HOUR + INTERVAL 1 MINUTE + INTERVAL 1 SECOND + INTERVAL 1 MILLISECOND + INTERVAL 1 MICROSECOND AS dt FROM foo " ); } #[test] fn test_into() { assert_snapshot!(compile(r#" from data into table_a from table_a select {x, y} "#).unwrap(), @r" WITH table_a AS ( SELECT * FROM data ) SELECT x, y FROM table_a " ); } #[test] fn test_array_01() { compile( r#" let a = [1, 2, false] from x "#, ) .unwrap(); assert_snapshot!(compile(r#" let my_relation = [ {a = 3, b = false}, {a = 4, b = true}, ] let main = (my_relation | filter b) "#).unwrap(), @r" WITH table_0 AS ( SELECT 3 AS a, false AS b UNION ALL SELECT 4 AS a, true AS b ), my_relation AS ( SELECT a, b FROM table_0 ) SELECT a, b FROM my_relation WHERE b " ); } #[test] fn test_array_02() { assert_snapshot!(compile(r###" let x = p1 -> s"x({p1})" from [{a=null}, {a=2}] filter (a | in [2, 4]) select { empty_array = [], single_element = [42], null_element = [null], complex_expressions = [a + a, (a * 2) + 1], nested_function_calls = [(min a), (max a ?? 0)], passing_as_arg = x [1,2,3], nested = ['a', ['b']] } "###).unwrap(), @r" WITH table_0 AS ( SELECT NULL AS a UNION ALL SELECT 2 AS a ) SELECT [] AS empty_array, [42] AS single_element, [NULL] AS null_element, [a + a, a * 2 + 1] AS complex_expressions, [MIN(a) OVER (), MAX(COALESCE(a, 0)) OVER ()] AS nested_function_calls, x([1, 2, 3]) AS passing_as_arg, [ 'a', [ 'b' ] ] AS nested FROM table_0 WHERE a IN (2, 4) "); } #[test] fn test_array_03() { assert_snapshot!(compile(r###" from employees select {e = this} select [e.first_name, e.last_name] "###).unwrap(), @r" SELECT [first_name, last_name] FROM employees "); } #[test] fn test_double_stars() { assert_snapshot!(compile(r#" from tb1 join tb2 (==c2) take 5 filter (tb2.c3 < 100) "#).unwrap(), @r" WITH table_0 AS ( SELECT tb1.*, tb2.* FROM tb1 INNER JOIN tb2 ON tb1.c2 = tb2.c2 LIMIT 5 ) SELECT * FROM table_0 WHERE c3 < 100 " ); assert_snapshot!(compile(r#" prql target:sql.duckdb from tb1 join tb2 (==c2) take 5 filter (tb2.c3 < 100) "#).unwrap(), @r" WITH table_0 AS ( SELECT tb1.*, tb2.* FROM tb1 INNER JOIN tb2 ON tb1.c2 = tb2.c2 LIMIT 5 ) SELECT * FROM table_0 WHERE c3 < 100 " ); } #[test] fn test_lineage() { // #2627 assert_snapshot!(compile(r#" from_text """ a 1 2 3 """ derive a = a "#).unwrap(), @r" WITH table_0 AS ( SELECT ' 1' AS a UNION ALL SELECT ' 2' AS a UNION ALL SELECT ' 3' AS a ) SELECT a FROM table_0 " ); // #2392 assert_snapshot!(compile(r#" from_text format:json """{ "columns": ["a"], "data": [[1]] }""" derive a = a + 1 "#).unwrap(), @r" WITH table_0 AS ( SELECT 1 AS a ) SELECT a AS _expr_0, a + 1 AS a FROM table_0 " ); } #[test] fn test_type_as_column_name() { // #2503 assert_snapshot!(compile(r#" let f = tbl -> ( t = tbl select t.date ) from foo f"#) .unwrap(), @r" SELECT date FROM foo AS t "); } #[test] fn test_error_code() { let err = compile( r###" let a = (from x) "###, ) .unwrap_err(); assert_eq!(err.inner[0].code.as_ref().unwrap(), "E0001"); } #[test] fn large_query() { // This was causing a stack overflow on Windows, ref https://github.com/PRQL/prql/issues/2857 compile( r###" from employees filter gross_cost > 0 group {title} ( aggregate { ct = count this, } ) sort ct filter ct > 200 take 20 sort ct filter ct > 200 take 20 sort ct filter ct > 200 take 20 sort ct filter ct > 200 take 20 "###, ) .unwrap(); } #[test] fn test_returning_constants_only() { assert_snapshot!(compile( r###" from tb1 sort {a} select {c = b} select {d = 10} "###, ) .unwrap(), @r" WITH table_0 AS ( SELECT 10 AS d, a FROM tb1 ) SELECT d FROM table_0 ORDER BY a "); assert_snapshot!(compile( r###" from tb1 take 10 filter true take 20 filter true select d = 10 "###, ) .unwrap(), @r" WITH table_1 AS ( SELECT NULL FROM tb1 LIMIT 10 ), table_0 AS ( SELECT NULL FROM table_1 WHERE true LIMIT 20 ) SELECT 10 AS d FROM table_0 WHERE true "); } #[test] fn test_conflicting_names_at_split() { // issue #2697 assert_snapshot!(compile( r#" from s = workflow_steps join wp=workflow_phases (s.phase_id == wp.id) filter wp.name == "CREATE_OUTLET" join w=workflow (wp.workflow_id == w.id) select { step_id = s.id, phase_id = wp.id, } "#, ) .unwrap(), @r" WITH table_0 AS ( SELECT wp.id, s.id AS _expr_0, wp.workflow_id FROM workflow_steps AS s INNER JOIN workflow_phases AS wp ON s.phase_id = wp.id WHERE wp.name = 'CREATE_OUTLET' ) SELECT table_0._expr_0 AS step_id, table_0.id AS phase_id FROM table_0 INNER JOIN workflow AS w ON table_0.workflow_id = w.id "); } #[test] fn test_relation_literal_quoting() { // issue #3484 assert_snapshot!(compile( r###" from [ {`small number`=1e-10, `large number`=1e10}, ] select {`small number`, `large number`} "###, ) .unwrap(), @r#" WITH table_0 AS ( SELECT 1e-10 AS "small number", 10000000000.0 AS "large number" ) SELECT "small number", "large number" FROM table_0 "#); } #[test] fn test_relation_var_name_clashes_01() { assert_snapshot!(compile( r###" let table_0 = (from a) from table_0 take 10 filter x > 0 "###, ) .unwrap(), @r" WITH table_0 AS ( SELECT * FROM a ), table_1 AS ( SELECT * FROM table_0 LIMIT 10 ) SELECT * FROM table_1 WHERE x > 0 "); } #[test] fn test_relation_var_name_clashes_02() { // issue #3713 assert_snapshot!(compile( r###" from t join t (==x) "###, ) .unwrap(), @r" SELECT t.*, table_0.* FROM t INNER JOIN t AS table_0 ON t.x = table_0.x "); } #[test] #[ignore] fn test_select_this() { // Currently broken for a few reasons: // - type of `this` is not resolved as tuple, but an union? // - lineage is not computed correctly assert_snapshot!(compile( r###" from x select {a, b} select this "###, ) .unwrap(), @r###" SELECT a, b FROM x "###); } #[test] fn test_select_bare_wildcard() { // Regression test for #5694: bare `*` in `select` should produce // a helpful error, not panic. assert_snapshot!(compile(r#" from page_titles select * "#).unwrap_err(), @" Error: ╭─[ :3:12 ] │ 3 │ select * │ ┬ │ ╰── Column wildcard `*` must be qualified, e.g. `table_name.*` ───╯ " ); } #[test] fn test_select_repeated_and_derived() { assert_snapshot!(compile( r###" from tb_0 take 100 select {cc0 = c1,cc1 = c2,cc2 = c1} select {ccc0 = cc1,ccc1 = 1} select {cccc0 = 1,cccc1 = ccc0,cccc3 = 1,cccc4 = 0,cccc5 = 0,cccc6 = 0,cccc7 = 0} derive {cccc8 = 0,cccc9 = 0,cccc10 = 0} "###, ) .unwrap(), @r###" WITH table_0 AS ( SELECT c2 AS _expr_0 FROM tb_0 LIMIT 100 ) SELECT 1 AS cccc0, _expr_0 AS cccc1, 1 AS cccc3, 0 AS cccc4, 0 AS cccc5, 0 AS cccc6, 0 AS cccc7, 0 AS cccc8, 0 AS cccc9, 0 AS cccc10 FROM table_0 "###); } #[test] fn test_group_exclude() { assert_snapshot!(compile( r###" from x select {a, b} group {a} (derive c = a + 1) "###, ) .unwrap_err(), @r" Error: ╭─[ :4:27 ] │ 4 │ group {a} (derive c = a + 1) │ ┬ │ ╰── Unknown name `a` │ │ Help: available columns: x.b ───╯ "); // assert_snapshot!(compile( // r###" // from x // select {a, b} // group {a + 1} (aggregate {sum b}) // "###, // ) // .unwrap_err(), @r###" // SELECT // a, // b // FROM // x // "###); } #[test] fn test_table_declarations() { assert_snapshot!(compile( r###" module default_db { module my_schema { let my_table <[{ id = int, a = text }]> } let another_table <[{ id = int, b = text }]> } from my_schema.my_table | join another_table (==id) | take 10 "###, ) .unwrap(), @r" SELECT my_table.id, my_table.a, another_table.id, another_table.b FROM my_schema.my_table INNER JOIN another_table ON my_table.id = another_table.id LIMIT 10 "); } #[test] fn test_param_declarations() { assert_snapshot!(compile( r###" let a from x | filter b == a "###, ) .unwrap(), @r" SELECT * FROM x WHERE b = $a "); } #[test] fn test_relation_aliasing() { assert_snapshot!(compile( r###" from x | select {y = this} | select {y.hello} "###, ) .unwrap(), @r" SELECT hello FROM x "); } #[test] fn test_import() { assert_snapshot!(compile( r###" module hello { let world = 1 } import a = hello.world from x | select a "###, ) .unwrap(), @r" SELECT 1 FROM x "); } #[test] fn unstable_ordering() { // https://github.com/PRQL/prql/issues/5053 assert_snapshot!(compile(r###" # All lines are mandatory from foo take 10000 # We need 8+ aliases to trigger the issue derive { a1 = 1, a2 = 1, a3 = 1, a4 = 1, a5 = 1, a6 = 1, a7 = 1, a8 = 1 } # The `select !` itself is required, but its content is not select !{ a1, a2, a3, a4, a5, a6, a7, a8 } # We may remove `u` from both these statements, but the `select !` must remain select { b, c, u } select !{ u } # Aggregate verb seems to not matter group { b } ( aggregate { c = count c } ) derive { d = c } select !{ c } group { d } ( aggregate { b = sum b } ) sort { d }"###).unwrap(), @r" WITH table_1 AS ( SELECT b FROM foo LIMIT 10000 ), table_0 AS ( SELECT b, COUNT(*) AS _expr_0 FROM table_1 GROUP BY b ) SELECT _expr_0 AS d, COALESCE(SUM(b), 0) AS b FROM table_0 GROUP BY _expr_0 ORDER BY d "); } #[test] fn test_type_error_placement() { assert_snapshot!(compile(r###" let foo = x -> (x | as integer) from t select (true && (foo y)) "###).unwrap(), @r" SELECT true AND CAST(y AS integer) FROM t "); } #[test] fn test_missing_columns_group_complex_compute() { // https://github.com/PRQL/prql/issues/5354 // The focus for this tests is on whether the `hire_date` column is available where it's needed. // Additional `city` derive are there only to trigger the issue. assert_snapshot!(compile( r#"prql target:sql.postgres from employees derive `year` = s'EXTRACT(year from {`hire_date`})' derive { `year_label` = f"Year {`year`}" } derive { `city` = case [ this.`city` == "Calgary" => "A city", true => this.`city` ] } derive { `city` = case [ this.`city` == "Edmonton" => "Another city", true => this.`city` ] } group {`year`, `year_label`} (take 1) select {this.`year_label`} "#, ) .unwrap(), @r" SELECT DISTINCT ON ( EXTRACT( year from hire_date ), CONCAT( 'Year ', EXTRACT( year from hire_date ) ) ) CONCAT( 'Year ', EXTRACT( year from hire_date ) ) AS year_label FROM employees "); } #[test] fn test_append_select_compute() { // Test for handling complex append with select and compute operations assert_snapshot!(compile(r###" from invoices derive total = case [total < 10 => total * 2, true => total] select { customer_id, invoice_id, total } take 5 append ( from invoice_items derive unit_price = case [unit_price < 1 => unit_price * 2, true => unit_price] select { invoice_line_id, invoice_id, unit_price } take 5 ) select { a = customer_id * 2, b = math.round 1 (invoice_id * total) } "###).unwrap(), @r" WITH table_1 AS ( SELECT * FROM ( SELECT invoice_id, CASE WHEN total < 10 THEN total * 2 ELSE total END AS _expr_0, customer_id FROM invoices LIMIT 5 ) AS table_3 UNION ALL SELECT * FROM ( SELECT invoice_id, CASE WHEN unit_price < 1 THEN unit_price * 2 ELSE unit_price END AS unit_price, invoice_line_id FROM invoice_items LIMIT 5 ) AS table_4 ) SELECT customer_id * 2 AS a, ROUND(invoice_id * _expr_0, 1) AS b FROM table_1 "); } #[test] fn test_append_select_multiple() { // Test for handling multiple append operations with grouping and aggregation assert_snapshot!(compile(r###" from invoices select { customer_id, invoice_id, total, useless1, useless2 } take 5 append ( from employees select { employee_id, employee_id + 1, reports_to, useless3, useless4 } take 5 ) group { customer_id } (aggregate { invoice_id = math.round 1 (sum invoice_id), total = math.round 1 (sum total), useless1 = sum useless1 }) append ( from invoice_items select { invoice_id, invoice_line_id, 0, useless5 } take 5 ) sort { +invoice_id, +total } select { total, invoice_id } "###).unwrap(), @" WITH table_3 AS ( SELECT * FROM ( SELECT customer_id, total, invoice_id FROM invoices LIMIT 5 ) AS table_6 UNION ALL SELECT * FROM ( SELECT employee_id, reports_to, employee_id + 1 FROM employees LIMIT 5 ) AS table_7 ), table_2 AS ( SELECT ROUND(COALESCE(SUM(total), 0), 1) AS total, ROUND(COALESCE(SUM(invoice_id), 0), 1) AS invoice_id FROM table_3 GROUP BY customer_id UNION ALL SELECT * FROM ( SELECT invoice_id, invoice_line_id FROM invoice_items LIMIT 5 ) AS table_8 ) SELECT total, invoice_id FROM table_2 ORDER BY invoice_id, total "); } #[test] fn test_append_with_cte() { // Regression test for issue #5494 - append with CTEs (let statements) // Tests that positional mapping doesn't cause out-of-bounds errors assert_snapshot!(compile(r###" prql target:sql.postgres let invoices_wrap = ( from invoices select { invoice_id, billing_country } ) let employees_wrap = ( from employees select { employee_id, country } ) from invoices_wrap derive { source = "invoices" } append ( from employees_wrap derive { source = "employees" } ) "###).unwrap(), @r" WITH invoices_wrap AS ( SELECT invoice_id, billing_country FROM invoices ), employees_wrap AS ( SELECT employee_id, country FROM employees ) SELECT invoice_id, billing_country, 'invoices' AS source FROM invoices_wrap UNION ALL SELECT employee_id, country, 'employees' AS source FROM employees_wrap "); } #[test] fn test_distinct_on_sort_on_compute() { // Test for handling distinct on with sorting on computed columns. // Note: table_0 includes billing_city and _expr_1 even though they're unused // downstream. This is a side effect of the fix for #5130 which keeps Computes // together with Aggregates when Filter follows. assert_snapshot!(compile(r###" from invoices derive code = case [customer_id < 10 => billing_postal_code, true => null] group {customer_id, billing_city, billing_country} ( sort {-this.code} take 1 ) filter (customer_id | in [4]) group {billing_country} (aggregate {total = math.round 2 (sum total)}) "###).unwrap(), @" WITH table_1 AS ( SELECT billing_country, total, customer_id, billing_city, CASE WHEN customer_id < 10 THEN billing_postal_code ELSE NULL END AS _expr_1, billing_postal_code FROM invoices ), table_0 AS ( SELECT billing_country, total, customer_id, ROW_NUMBER() OVER ( PARTITION BY customer_id, billing_city, billing_country ORDER BY _expr_1 DESC ) AS _expr_0, billing_city, _expr_1 FROM table_1 ) SELECT billing_country, ROUND(COALESCE(SUM(total), 0), 2) AS total FROM table_0 WHERE _expr_0 <= 1 AND customer_id IN (4) GROUP BY billing_country "); } /// Ensures that the sort happens on `table0`.`_expr_0` and not on `table2`.`_expr_0` #[test] fn test_sort_cast_filter_join_select() { assert_snapshot!(compile(r###" from albums select { this.`title`, this.`artist_id`, this.`album_id` } sort { this.`artist_id`, this.`album_id` } derive { `artist_id` = as `double precision` this.`artist_id` } filter (this.`artist_id` != null) join side:left artists (this.`artist_id` == that.`artist_id`) select {this.`artist_id`, this.`title`, this.`name`} "### ).unwrap(), @" WITH table_1 AS ( SELECT CAST(artist_id AS double precision) AS artist_id, title, artist_id AS _expr_0, album_id FROM albums ), table_2 AS ( SELECT artist_id, title, _expr_0, album_id FROM table_1 WHERE artist_id IS NOT NULL ), table_0 AS ( SELECT artist_id, title, _expr_0, album_id FROM table_2 ) SELECT table_0.artist_id, table_0.title, artists.name FROM table_0 LEFT OUTER JOIN artists ON table_0.artist_id = artists.artist_id ORDER BY table_0._expr_0, table_0.album_id ") } /// Ensures that the sort happens on `table1`.`artist_id` and not on `table2`.`artist_id` #[test] fn test_sort_filter_derive_join_select() { assert_snapshot!(compile(r###" from albums sort this.`artist_id` filter (this.`artist_id` != null) derive { `artist_id` = as `double precision` this.`artist_id` } join side:left (from artists select {`artist_id_right` = this.`artist_id`} ) (this.`artist_id` == that.`artist_id_right`) select {this.`artist_id`, this.`title`} "### ).unwrap(), @" WITH table_2 AS ( SELECT CAST(artist_id AS double precision) AS artist_id, title, artist_id AS _expr_0 FROM albums WHERE artist_id IS NOT NULL ), table_1 AS ( SELECT artist_id, title, _expr_0 FROM table_2 ), table_0 AS ( SELECT artist_id AS artist_id_right FROM artists ) SELECT table_1.artist_id, table_1.title FROM table_1 LEFT OUTER JOIN table_0 ON table_1.artist_id = table_0.artist_id_right ORDER BY table_1._expr_0 ") } /// Ensures that the sort happens on `table0`.`artist_id` and not on `table0`.`double_artist_id` #[test] fn test_sort_cast_filter_join_select_with_alias() { assert_snapshot!(compile(r###" from albums select { this.`title`, this.`artist_id`, this.`album_id` } sort { this.`artist_id`, this.`album_id` } derive { `double_artist_id` = as `double precision` this.`artist_id` } filter (this.`artist_id` != null) join side:left artists (this.`double_artist_id` == that.`artist_id`) select {this.`double_artist_id`, this.`title`, this.`name`} "### ).unwrap(), @" WITH table_1 AS ( SELECT CAST(artist_id AS double precision) AS double_artist_id, title, artist_id, album_id FROM albums ), table_2 AS ( SELECT double_artist_id, title, artist_id, album_id FROM table_1 WHERE artist_id IS NOT NULL ), table_0 AS ( SELECT double_artist_id, title, artist_id, album_id FROM table_2 ) SELECT table_0.double_artist_id, table_0.title, artists.name FROM table_0 LEFT OUTER JOIN artists ON table_0.double_artist_id = artists.artist_id ORDER BY table_0.artist_id, table_0.album_id ") } #[rstest] #[case::redshift_quotes_only_keywords( sql::Dialect::Redshift, r#" SELECT invoice_id, "time", "timestamp", "identity", "system" FROM invoice "# )] #[case::postgres_does_not_quote_redshift_keywords( sql::Dialect::Postgres, r#" SELECT invoice_id, time, timestamp, identity, system FROM invoice "# )] #[case::generic_does_not_quote_redshift_keywords( sql::Dialect::Generic, r#" SELECT invoice_id, time, timestamp, identity, system FROM invoice "# )] fn test_redshift_keyword_quoting( #[case] dialect: sql::Dialect, #[case] expected_sql: &'static str, ) { let query = r#" from invoice select {invoice_id, invoice.time, invoice.timestamp, invoice.identity, invoice.system} "#; assert_eq!( compile_with_sql_dialect(query, dialect).unwrap(), expected_sql.trim_start() ) } #[test] fn test_redshift_quotes_only_keywords_mixed() { // Test that Redshift quotes ONLY keywords, not all identifiers let query = r#" from invoice select { invoice_id, invoice.time, invoice.timestamp, customer_name, invoice.identity, amount } "#; let expected = r#" SELECT invoice_id, "time", "timestamp", customer_name, "identity", amount FROM invoice "#; assert_eq!( compile_with_sql_dialect(query, sql::Dialect::Redshift).unwrap(), expected.trim_start() ) } // Test that Redshift uses double pipe (||) over CONCAT #[test] fn test_redshift_uses_double_pipe_over_concat() { assert_snapshot!(compile_with_sql_dialect(r###" from invoice derive { concatenated = f"{col_one} + {col_two}" } "###, sql::Dialect::Redshift ).unwrap(), @r" SELECT *, col_one || ' + ' || col_two AS concatenated FROM invoice "); } #[rstest] #[case( "from t | select { inter = 2weeks }", "SELECT\n INTERVAL '2 WEEK' AS inter\nFROM\n t\n" )] #[case( "from t | select { inter = 2months }", "SELECT\n INTERVAL '2' MONTH AS inter\nFROM\n t\n" )] #[case( "from t | select { inter = 2hours }", "SELECT\n INTERVAL '2' HOUR AS inter\nFROM\n t\n" )] fn test_redshift_interval_quoting(#[case] query: &str, #[case] expected: &str) { assert_eq!( compile_with_sql_dialect(query, sql::Dialect::Redshift).unwrap(), expected ) } #[test] fn test_redshift_text_contains_uses_double_pipe() { assert_snapshot!(compile_with_sql_dialect(r###" from employees select { name, has_substring = (name | text.contains "pika") } "###, sql::Dialect::Redshift ).unwrap(), @r" SELECT name, name LIKE '%' || 'pika' || '%' AS has_substring FROM employees "); } #[test] fn test_snowflake_row_number_requires_order_by() { // https://github.com/PRQL/prql/issues/5580 // Snowflake requires ORDER BY for ROW_NUMBER() in window specification assert_snapshot!(compile_with_sql_dialect(r###" from invoices group { customer_id } (take 1) "###, sql::Dialect::Snowflake ).unwrap(), @r#" WITH "table_0" AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY "customer_id" ORDER BY 1 ) AS "_expr_0" FROM "invoices" ) SELECT * EXCLUDE ("_expr_0") FROM "table_0" WHERE "_expr_0" <= 1 "#); } #[test] fn test_snowflake_row_number_with_explicit_sort() { // When user provides explicit sort, it should be used instead of the fallback assert_snapshot!(compile_with_sql_dialect(r###" from invoices group { customer_id } (sort invoice_date | take 1) "###, sql::Dialect::Snowflake ).unwrap(), @r#" WITH "table_0" AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY "customer_id" ORDER BY "invoice_date" ) AS "_expr_0" FROM "invoices" ) SELECT * EXCLUDE ("_expr_0") FROM "table_0" WHERE "_expr_0" <= 1 "#); } #[test] fn test_snowflake_text_length_uses_length() { // Snowflake should use LENGTH instead of CHAR_LENGTH assert_snapshot!(compile_with_sql_dialect(r###" from employees select { name_length = (name | text.length) } "###, sql::Dialect::Snowflake ).unwrap(), @r#" SELECT LENGTH("name") AS "name_length" FROM "employees" "#); } #[test] fn test_group_with_only_sort() { // Issue #5092: group with only sort (no take) should not cause ICE. // The sort inside a group without take is semantically a no-op, // so it should compile to just the table. assert_snapshot!(compile(r###" from a = employees group { a.department } ( sort a.salary ) "###).unwrap(), @r" SELECT * FROM employees AS a "); } #[test] fn test_group_empty_preserves_sort() { // Issue #5100: Empty group {} should preserve inner sort. assert_snapshot!(compile(r###" from foo group {} ( sort a take 1 ) "###).unwrap(), @r" SELECT * FROM foo ORDER BY a LIMIT 1 "); } #[test] fn test_source_column_name() { // Issue #5094: Using `source` as a column name should not cause // "Ambiguous name" error. assert_snapshot!(compile(r###" let table1 = ( from ScrapedData select { sd=SD_Land_Use_Code } derive source="SD" group { sd } (take 1) sort { sd } ) let table2 = ( from SpecialLand select { sd = SL_Code } derive source = "SL" group { sd } (take 1) sort { sd } ) from table1 append table2 "###).unwrap(), @r#" WITH table_0 AS ( SELECT "SD_Land_Use_Code" AS sd, 'SD' AS source, ROW_NUMBER() OVER (PARTITION BY "SD_Land_Use_Code") AS _expr_0 FROM "ScrapedData" ), table1 AS ( SELECT sd, source FROM table_0 WHERE _expr_0 <= 1 ), table_1 AS ( SELECT "SL_Code" AS sd, 'SL' AS source, ROW_NUMBER() OVER (PARTITION BY "SL_Code") AS _expr_1 FROM "SpecialLand" ) SELECT sd, source FROM table1 UNION ALL SELECT * FROM ( SELECT sd, source FROM table_1 WHERE _expr_1 <= 1 ORDER BY sd ) AS table_2 "#); } #[test] fn test_column_inference_with_into() { // Issue #4723: Column inference should work correctly with `into`. assert_snapshot!(compile(r###" from data join side:left other (other.id == data.id) into A from A select { A.val } "###).unwrap(), @r#" WITH "A" AS ( SELECT data.*, other.* FROM data LEFT OUTER JOIN other ON other.id = data.id ) SELECT val FROM "A" "#); } #[test] fn test_distinct_on_columns_propagated() { // Issue #4432: DISTINCT ON should propagate all necessary columns to the CTE. assert_snapshot!(compile(r###" prql target:sql.postgres from src group {grouped_field} ( sort {sort_1} take 2..3 sort {sort_2} take 1 ) select { foo } "###).unwrap(), @" WITH table_0 AS ( SELECT foo, grouped_field, sort_2, ROW_NUMBER() OVER ( PARTITION BY grouped_field ORDER BY sort_1 ) AS _expr_0 FROM src ) SELECT DISTINCT ON (grouped_field) foo FROM table_0 WHERE _expr_0 BETWEEN 2 AND 3 ORDER BY grouped_field, sort_2 "); } #[test] fn test_sort_take_before_aggregate() { // Issue #5401: sort|take before an aggregation should enforce the sort in the CTE. // Previously, the ORDER BY was lost because the sort was embedded in the Take struct // but not emitted before the LIMIT. assert_snapshot!(compile(r###" from my_table sort {-this.Total} take 7 group { this.network } ( aggregate { total_sum = sum this.Total } ) sort {-this.total_sum} "###).unwrap(), @r#" WITH table_0 AS ( SELECT network, "Total" FROM my_table ORDER BY "Total" DESC LIMIT 7 ) SELECT network, COALESCE(SUM("Total"), 0) AS total_sum FROM table_0 GROUP BY network ORDER BY total_sum DESC "#); } #[test] fn test_aggregate_with_operations_and_filter() { // Issue #5130: Filtering on combined aggregates was generating invalid SQL. // The CTE was missing GROUP BY when aggregate expressions had operations. // Previously, this produced a CTE with SUM() but no GROUP BY clause. assert_snapshot!(compile(r###" from invoices group billing_city ( aggregate{ sum_c = (sum customer_id) + (sum customer_id) } ) filter sum_c > 0 "###).unwrap(), @" SELECT billing_city, COALESCE(SUM(customer_id), 0) + COALESCE(SUM(customer_id), 0) AS sum_c FROM invoices GROUP BY billing_city HAVING COALESCE(SUM(customer_id), 0) + COALESCE(SUM(customer_id), 0) > 0 "); // Also test with scalar operations assert_snapshot!(compile(r###" from invoices group billing_city ( aggregate{ sum_c = (sum customer_id) * 2 } ) filter sum_c > 0 "###).unwrap(), @" SELECT billing_city, COALESCE(SUM(customer_id), 0) * 2 AS sum_c FROM invoices GROUP BY billing_city HAVING COALESCE(SUM(customer_id), 0) * 2 > 0 "); } /// Regression test for issue #5661: partial application of transforms in /// user-defined functions. /// /// When a user defines a function that wraps a transform with fewer parameters /// than the transform requires (e.g., `let foo = a -> take a`), the missing /// parameters should be propagated to the wrapper function, allowing it to work /// correctly in pipelines. #[test] fn test_partial_application_of_transform() { // Basic case: wrapping `take` with a single parameter assert_snapshot!(compile(r#" let foo = a -> take a from invoices | foo 10 "#).unwrap(), @r" SELECT * FROM invoices LIMIT 10 "); // Same behavior as explicit two-parameter version assert_snapshot!(compile(r#" let foo = a r -> take a r from invoices | foo 10 "#).unwrap(), @r" SELECT * FROM invoices LIMIT 10 "); } ================================================ FILE: prqlc/prqlc-macros/Cargo.toml ================================================ [package] description = "Macros for PRQL compilation at build time" name = "prqlc-macros" edition.workspace = true license.workspace = true repository.workspace = true rust-version.workspace = true version.workspace = true [lib] bench = false doctest = false proc-macro = true test = false [dependencies] prqlc = {path = "../prqlc", default-features = false, version = "0.13.12" } syn = "2.0.117" [package.metadata.release] tag-name = "{{version}}" tag-prefix = "" ================================================ FILE: prqlc/prqlc-macros/src/lib.rs ================================================ //! Macros for PRQL compilation at build time. //! //! ``` //! use prqlc_macros::prql_to_sql; //! //! let sql: &str = prql_to_sql!("from albums | select {title, artist_id}"); //! assert_eq!(sql, "SELECT title, artist_id FROM albums"); //! ``` //! //! "at build time" means that PRQL will be compiled during Rust compilation, //! producing errors alongside Rust errors. Limited to string literals. use proc_macro::{Literal, TokenStream, TokenTree}; use syn::{Expr, ExprLit, Lit}; #[proc_macro] pub fn prql_to_sql(input: TokenStream) -> TokenStream { let input: Expr = syn::parse(input).unwrap(); let prql_string = match input { Expr::Lit(ExprLit { lit: Lit::Str(lit_str), .. }) => lit_str.value(), _ => panic!("prql! proc macro expected a string"), }; let opts = prqlc::Options::default().no_format().no_signature(); let sql_string = match prqlc::compile(&prql_string, &opts) { Ok(r) => r, Err(err) => { panic!("{}", err); } }; TokenStream::from_iter(vec![TokenTree::Literal(Literal::string(&sql_string))]) } ================================================ FILE: prqlc/prqlc-parser/Cargo.toml ================================================ [package] description = "A parser for the PRQL query language." name = "prqlc-parser" edition.workspace = true license.workspace = true repository.workspace = true rust-version.workspace = true version.workspace = true [lib] bench = false doctest = false [dependencies] chumsky = { version = "0.12.0", default-features = false, features = [ "std", "pratt", ] } enum-as-inner = { workspace = true } itertools = { workspace = true } log = { workspace = true } schemars = { workspace = true } semver = { workspace = true } serde = { workspace = true } serde_yaml = { workspace = true, optional = true } strum = { workspace = true } [dev-dependencies] insta = { workspace = true } serde_json = { workspace = true } [lints.rust] # https://github.com/taiki-e/cargo-llvm-cov/blob/4039500dc7ce5874748769166f1f481be294c90f/README.md#exclude-function-from-coverage unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(coverage,coverage_nightly)', ] } unsafe_code = "forbid" ================================================ FILE: prqlc/prqlc-parser/src/error.rs ================================================ use std::fmt::Debug; use serde::Serialize; use crate::span::Span; /// A prqlc error. Used internally, exposed as prqlc::ErrorMessage. #[derive(Debug, Clone)] pub struct Error { /// Message kind. Currently only Error is implemented. pub kind: MessageKind, pub span: Option, pub reason: Reason, pub hints: Vec, /// Machine readable identifier error code eg, "E0001" pub code: Option<&'static str>, // pub source: ErrorSource } #[derive(Clone, Debug, Default)] pub enum ErrorSource { Lexer(String), #[default] Unknown, NameResolver, TypeResolver, SQL, Internal { message: String, }, } /// Multiple prqlc errors. Used internally, exposed as prqlc::ErrorMessages. #[derive(Debug, Clone)] pub struct Errors(pub Vec); /// Compile message kind. Currently only Error is implemented. #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub enum MessageKind { Error, Warning, Lint, } #[derive(Debug, Clone)] pub enum Reason { Simple(String), Expected { /// Where we were // (could rename to `where` / `location` / `within`?) who: Option, /// What we expected expected: String, /// What we found found: String, }, Unexpected { found: String, }, NotFound { name: String, namespace: String, }, Bug { issue: Option, details: Option, }, Internal { message: String, }, } impl Error { pub fn new(reason: Reason) -> Self { Error { kind: MessageKind::Error, span: None, reason, hints: Vec::new(), code: None, // source: ErrorSource::default() } } pub fn new_simple(reason: S) -> Self { Error::new(Reason::Simple(reason.to_string())) } pub fn new_bug(issue_no: i32) -> Self { Error::new(Reason::Bug { issue: Some(issue_no), details: None, }) } /// Used for things that you *think* should never happen, but are not sure. pub fn new_assert(details: S) -> Self { Error::new(Reason::Bug { issue: None, details: Some(details.to_string()), }) } } impl std::fmt::Display for Reason { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Reason::Simple(text) => f.write_str(text), Reason::Expected { who, expected, found, } => { if let Some(who) = who { write!(f, "{who} ")?; } write!(f, "expected {expected}, but found {found}") } Reason::Unexpected { found } => write!(f, "unexpected {found}"), Reason::NotFound { name, namespace } => write!(f, "{namespace} `{name}` not found"), Reason::Bug { issue, details } => { write!(f, "internal compiler error")?; if let Some(details) = details { write!(f, "; {details}")?; } if let Some(issue_no) = issue { write!( f, "; tracked at https://github.com/PRQL/prql/issues/{issue_no}" )?; } Ok(()) } Reason::Internal { message } => { write!(f, "internal error: {message}") } } } } impl From for Errors { fn from(error: Error) -> Self { Errors(vec![error]) } } // Needed for anyhow impl std::error::Error for Error {} // Needed for anyhow impl std::error::Error for Errors {} // Needed for StdError impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Debug::fmt(&self, f) } } // Needed for StdError impl std::fmt::Display for Errors { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { Debug::fmt(&self, f) } } pub trait WithErrorInfo: Sized { fn push_hint>(self, hint: S) -> Self; fn with_hints, I: IntoIterator>(self, hints: I) -> Self; fn with_span(self, span: Option) -> Self; fn with_span_fallback(self, span: Option) -> Self; fn with_code(self, code: &'static str) -> Self; fn with_source(self, source: ErrorSource) -> Self; } impl WithErrorInfo for Error { fn push_hint>(mut self, hint: S) -> Self { self.hints.push(hint.into()); self } fn with_hints, I: IntoIterator>(mut self, hints: I) -> Self { self.hints = hints.into_iter().map(|x| x.into()).collect(); self } fn with_span(mut self, span: Option) -> Self { self.span = span; self } fn with_code(mut self, code: &'static str) -> Self { self.code = Some(code); self } fn with_span_fallback(mut self, span: Option) -> Self { self.span = self.span.or(span); self } fn with_source(self, _source: ErrorSource) -> Self { // self.source = source; self } } impl WithErrorInfo for Result { fn push_hint>(self, hint: S) -> Self { self.map_err(|e| e.push_hint(hint)) } fn with_hints, I: IntoIterator>(self, hints: I) -> Self { self.map_err(|e| e.with_hints(hints)) } fn with_span(self, span: Option) -> Self { self.map_err(|e| e.with_span(span)) } fn with_span_fallback(self, span: Option) -> Self { self.map_err(|e| e.with_span_fallback(span)) } fn with_code(self, code: &'static str) -> Self { self.map_err(|e| e.with_code(code)) } fn with_source(self, source: ErrorSource) -> Self { self.map_err(|e| e.with_source(source)) } } ================================================ FILE: prqlc/prqlc-parser/src/generic.rs ================================================ /// Generic definitions of various AST items. // // This was added in a big refactor by a generous-but-new contributor, and // hasn't been used much since, and I'm not sure carries its weight. So we // could consider rolling back to only concrete implementations to delayer the // code. use schemars::JsonSchema; use serde::{Deserialize, Serialize}; /// Inclusive-inclusive range. /// Missing bound means unbounded range. #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct Range { pub start: Option, pub end: Option, } impl Range { pub const fn unbounded() -> Self { Range { start: None, end: None, } } pub fn try_map Result>(self, f: F) -> Result, E> { Ok(Range { start: self.start.map(&f).transpose()?, end: self.end.map(f).transpose()?, }) } pub fn map U>(self, f: F) -> Range { Range { start: self.start.map(&f), end: self.end.map(f), } } } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)] pub enum InterpolateItem { String(String), Expr { expr: Box, format: Option, }, } impl InterpolateItem { pub fn map U>(self, f: F) -> InterpolateItem { match self { Self::String(s) => InterpolateItem::String(s), Self::Expr { expr, format } => InterpolateItem::Expr { expr: Box::new(f(*expr)), format, }, } } pub fn try_map Result>(self, f: F) -> Result, E> { Ok(match self { Self::String(s) => InterpolateItem::String(s), Self::Expr { expr, format } => InterpolateItem::Expr { expr: Box::new(f(*expr)?), format, }, }) } } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)] pub struct SwitchCase { pub condition: T, pub value: T, } ================================================ FILE: prqlc/prqlc-parser/src/lexer/lr.rs ================================================ use serde::{Deserialize, Serialize}; use enum_as_inner::EnumAsInner; use schemars::JsonSchema; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct Tokens(pub Vec); #[derive(Clone, PartialEq, Serialize, Deserialize, Eq, JsonSchema)] pub struct Token { pub kind: TokenKind, pub span: std::ops::Range, } #[derive(Clone, PartialEq, Debug, Serialize, Deserialize, JsonSchema)] pub enum TokenKind { NewLine, Ident(String), Keyword(String), #[cfg_attr( feature = "serde_yaml", serde(with = "serde_yaml::with::singleton_map"), schemars(with = "Literal") )] Literal(Literal), /// A parameter such as `$1` Param(String), Range { /// Whether the left side of the range is bound by the previous token /// (but it's not contained in this token) bind_left: bool, bind_right: bool, }, Interpolation(char, String), /// single-char control tokens Control(char), ArrowThin, // -> ArrowFat, // => Eq, // == Ne, // != Gte, // >= Lte, // <= RegexSearch, // ~= And, // && Or, // || Coalesce, // ?? DivInt, // // Pow, // ** Annotate, // @ // Aesthetics only Comment(String), DocComment(String), /// Vec containing comments between the newline and the line wrap // Currently we include the comments with the LineWrap token. This isn't // ideal, but I'm not sure of an easy way of having them be separate. // - The line wrap span technically includes the comments — on a newline, // we need to look ahead to _after_ the comments to see if there's a // line wrap, and exclude the newline if there is. // - We can only pass one token back // // Alternatives: // - Post-process the stream, removing the newline prior to a line wrap. // But requires a whole extra pass. // - Change the functionality. But it's very nice to be able to comment // something out and have line-wraps still work. LineWrap(Vec), /// A token we manually insert at the start of the input, which later stages /// can treat as a newline. Start, } #[derive( Debug, EnumAsInner, PartialEq, Clone, Serialize, Deserialize, strum::AsRefStr, JsonSchema, )] pub enum Literal { Null, Integer(i64), Float(f64), Boolean(bool), String(String), RawString(String), Date(String), Time(String), Timestamp(String), ValueAndUnit(ValueAndUnit), } impl TokenKind { pub fn range(bind_left: bool, bind_right: bool) -> Self { TokenKind::Range { bind_left, bind_right, } } } // Compound units, such as "2 days 3 hours" can be represented as `2days + 3hours` #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] pub struct ValueAndUnit { pub n: i64, // Do any DBs use floats or decimals for this? pub unit: String, // Could be an enum IntervalType, } impl std::fmt::Display for Literal { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Literal::Null => write!(f, "null")?, Literal::Integer(i) => write!(f, "{i}")?, Literal::Float(i) => write!(f, "{i}")?, Literal::String(s) => { write!(f, "{}", quote_string(escape_all_except_quotes(s).as_str()))?; } Literal::RawString(s) => { write!(f, "r{}", quote_string(s))?; } Literal::Boolean(b) => { f.write_str(if *b { "true" } else { "false" })?; } Literal::Date(inner) | Literal::Time(inner) | Literal::Timestamp(inner) => { write!(f, "@{inner}")?; } Literal::ValueAndUnit(i) => { write!(f, "{}{}", i.n, i.unit)?; } } Ok(()) } } fn quote_string(s: &str) -> String { if !s.contains('"') { return format!(r#""{s}""#); } if !s.contains('\'') { return format!("'{s}'"); } // If the string starts or ends with a quote, use the other quote to delimit // the string. Otherwise default to double quotes. // TODO: this doesn't cover a string that starts with a single quote and // ends with a double quote; I think in that case it's necessary to escape // the quote. We need to add tests here. let quote = if s.starts_with('"') || s.ends_with('"') { '\'' } else { '"' }; // When string contains both single and double quotes find the longest // sequence of consecutive quotes, and then use the next highest odd number // of quotes (quotes must be odd; even number of quotes are empty strings). // i.e.: // 0 -> 1 // 1 -> 3 // 2 -> 3 // 3 -> 5 let max_consecutive = s .split(|c| c != quote) .map(|quote_sequence| quote_sequence.len()) .max() .unwrap_or(0); let next_odd = max_consecutive.div_ceil(2) * 2 + 1; let delim = quote.to_string().repeat(next_odd); format!("{delim}{s}{delim}") } fn escape_all_except_quotes(s: &str) -> String { let mut result = String::new(); for ch in s.chars() { if ch == '"' || ch == '\'' { result.push(ch); } else { result.extend(ch.escape_default()); } } result } // This is here because Literal::Float(f64) does not implement Hash, so we cannot simply derive it. // There are reasons for that, but chumsky::Error needs Hash for the TokenKind, so it can deduplicate // tokens in error. // So this hack could lead to duplicated tokens in error messages. Oh no. #[allow(clippy::derived_hash_with_manual_eq)] impl std::hash::Hash for TokenKind { fn hash(&self, state: &mut H) { core::mem::discriminant(self).hash(state); } } impl std::cmp::Eq for TokenKind {} impl std::fmt::Display for TokenKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { TokenKind::NewLine => write!(f, "new line"), TokenKind::Ident(s) => { if s.is_empty() { // FYI this shows up in errors write!(f, "an identifier") } else { write!(f, "{s}") } } TokenKind::Keyword(s) => write!(f, "keyword {s}"), TokenKind::Literal(lit) => write!(f, "{lit}"), TokenKind::Control(c) => write!(f, "{c}"), TokenKind::ArrowThin => f.write_str("->"), TokenKind::ArrowFat => f.write_str("=>"), TokenKind::Eq => f.write_str("=="), TokenKind::Ne => f.write_str("!="), TokenKind::Gte => f.write_str(">="), TokenKind::Lte => f.write_str("<="), TokenKind::RegexSearch => f.write_str("~="), TokenKind::And => f.write_str("&&"), TokenKind::Or => f.write_str("||"), TokenKind::Coalesce => f.write_str("??"), TokenKind::DivInt => f.write_str("//"), TokenKind::Pow => f.write_str("**"), TokenKind::Annotate => f.write_str("@{"), TokenKind::Param(id) => write!(f, "${id}"), TokenKind::Range { bind_left, bind_right, } => write!( f, "'{}..{}'", if *bind_left { "" } else { " " }, if *bind_right { "" } else { " " } ), TokenKind::Interpolation(c, s) => { write!(f, "{c}\"{s}\"") } TokenKind::Comment(s) => { writeln!(f, "#{s}") } TokenKind::DocComment(s) => { writeln!(f, "#!{s}") } TokenKind::LineWrap(comments) => { write!(f, "\n\\ ")?; for comment in comments { write!(f, "{comment}")?; } Ok(()) } TokenKind::Start => write!(f, "start of input"), } } } impl std::fmt::Debug for Token { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}..{}: {:?}", self.span.start, self.span.end, self.kind) } } #[cfg(test)] mod test { use insta::assert_snapshot; use super::*; #[test] fn test_string_quoting() { fn make_str(s: &str) -> Literal { Literal::String(s.to_string()) } assert_snapshot!( make_str("hello").to_string(), @r#""hello""# ); assert_snapshot!( make_str(r#"he's nice"#).to_string(), @r#""he's nice""# ); assert_snapshot!( make_str(r#"he said "what up""#).to_string(), @r#"'he said "what up"'"# ); assert_snapshot!( make_str(r#"he said "what's up""#).to_string(), @r#"'''he said "what's up"'''"# ); assert_snapshot!( make_str(r#" single' three double""" four double"""" "#).to_string(), @r#"""""" single' three double""" four double"""" """"""# ); assert_snapshot!( make_str(r#""Starts with a double quote and ' contains a single quote"#).to_string(), @r#"'''"Starts with a double quote and ' contains a single quote'''"# ); } #[test] fn test_string_escapes() { assert_snapshot!( Literal::String(r#"hello\nworld"#.to_string()).to_string(), @r#""hello\\nworld""# ); assert_snapshot!( Literal::String(r#"hello\tworld"#.to_string()).to_string(), @r#""hello\\tworld""# ); // TODO: one problem here is that we don't remember whether the original // string contained an actual line break or contained an `\n` string, // because we immediately normalize both to `\n`. This means that when // we format the PRQL, we can't retain the original. I think three ways of // resolving this: // - Have different tokens in the lexer and parser; normalize at the // parsing stage, and then use the token in the lexer for writing out // the formatted PRQL. Literals are one of the only data structures we // retain between the lexer and parser. (note that this requires the // current effort to use tokens from the lexer as part of `prqlc fmt`; // ongoing as of 2024-08) // - Don't normalize at all, and then normalize when we use the string. // I think this might be viable and maybe easy, but is a bit less // elegant; the parser is designed to normalize this sort of thing. assert_snapshot!( Literal::String(r#"hello world"#.to_string()).to_string(), @r#""hello\n world""# ); } #[test] fn test_raw_string_quoting() { // TODO: add some test for escapes fn make_str(s: &str) -> Literal { Literal::RawString(s.to_string()) } assert_snapshot!( make_str("hello").to_string(), @r#"r"hello""# ); } } ================================================ FILE: prqlc/prqlc-parser/src/lexer/mod.rs ================================================ //! PRQL Lexer implementation use chumsky; use chumsky::extra; use chumsky::prelude::*; use chumsky::Parser; use self::lr::{Literal, Token, TokenKind, Tokens, ValueAndUnit}; use crate::error::{Error, ErrorSource, Reason, WithErrorInfo}; pub mod lr; #[cfg(test)] mod test; type E = Error; type ParserInput<'a> = &'a str; type ParserError<'a> = extra::Err>; /// Convert a chumsky Simple error to our internal Error type fn convert_lexer_error(source: &str, error: &Simple<'_, char>, source_id: u16) -> E { // Get span information from the Simple error // NOTE: When parsing &str, SimpleSpan uses BYTE offsets, not character offsets! // We need to convert byte offsets to character offsets for compatibility with our error reporting. let byte_span = error.span(); let byte_start = byte_span.start(); let byte_end = byte_span.end(); // Convert byte offsets to character offsets let char_start = source[..byte_start].chars().count(); let char_end = source[..byte_end].chars().count(); // Extract the "found" text using character-based slicing let found: String = source .chars() .skip(char_start) .take(char_end - char_start) .collect(); // If found is empty, report as "end of input", otherwise wrap in quotes let found_display = if found.is_empty() { "end of input".to_string() } else { format!("'{}'", found) }; // Create a new Error with the extracted information let error_source = format!( "Unexpected {} at position {}..{}", found_display, char_start, char_end ); WithErrorInfo::with_span( Error::new(Reason::Unexpected { found: found_display, }), Some(crate::span::Span { start: char_start, end: char_end, source_id, }), ) .with_source(ErrorSource::Lexer(error_source)) } /// Lex PRQL into LR, returning both the LR and any errors encountered pub fn lex_source_recovery(source: &str, source_id: u16) -> (Option>, Vec) { let result = lexer().parse(source).into_result(); match result { Ok(tokens) => (Some(insert_start(tokens.to_vec())), vec![]), Err(errors) => { // Convert chumsky Simple errors to our Error type let errors = errors .into_iter() .map(|error| convert_lexer_error(source, &error, source_id)) .collect(); (None, errors) } } } /// Lex PRQL into LR, returning either the LR or the errors encountered pub fn lex_source(source: &str) -> Result> { let result = lexer().parse(source).into_result(); match result { Ok(tokens) => Ok(Tokens(insert_start(tokens.to_vec()))), Err(errors) => { // Convert chumsky Simple errors to our Error type let errors = errors .into_iter() .map(|error| convert_lexer_error(source, &error, 0)) .collect(); Err(errors) } } } /// Insert a start token so later stages can treat the start of a file like a newline fn insert_start(tokens: Vec) -> Vec { std::iter::once(Token { kind: TokenKind::Start, span: 0..0, }) .chain(tokens) .collect() } /// Lex chars to tokens until the end of the input pub fn lexer<'a>() -> impl Parser<'a, ParserInput<'a>, Vec, ParserError<'a>> { lex_token() .repeated() .collect() .then_ignore(whitespace().or_not()) } /// Lex chars to a single token fn lex_token<'a>() -> impl Parser<'a, ParserInput<'a>, Token, ParserError<'a>> { // Handle range token with proper whitespace // Ranges need special handling since the '..' token needs to know about whitespace // for binding on left and right sides let range = whitespace() .or_not() .then(just("..")) .then(whitespace().or_not()) .map_with(|((left, _), right), extra| { let span: chumsky::span::SimpleSpan = extra.span(); Token { kind: TokenKind::Range { // Check if there was whitespace before/after to determine binding bind_left: left.is_none(), bind_right: right.is_none(), }, span: span.start()..span.end(), } }); // Handle all other token types with proper whitespace let other_tokens = whitespace() .or_not() .ignore_then(token().map_with(|kind, extra| { let span: chumsky::span::SimpleSpan = extra.span(); Token { kind, span: span.start()..span.end(), } })); // Try to match either a range or any other token choice((range, other_tokens)) } /// Parse individual token kinds fn token<'a>() -> impl Parser<'a, ParserInput<'a>, TokenKind, ParserError<'a>> { // Main token parser for all tokens // Strategic .boxed() calls reduce compile times for complex parsers with minimal runtime cost choice(( line_wrap().boxed(), // Line continuation with backslash (complex recursive) newline().to(TokenKind::NewLine), // Newline characters multi_char_operators(), // Multi-character operators (==, !=, etc.) interpolation().boxed(), // String interpolation (complex nested parsing) param(), // Parameters ($name) // Date literals must come before @ handling for annotations date_token().boxed(), // Date literals (complex with multiple branches) // Special handling for @ annotations - must come after date_token just('@').to(TokenKind::Annotate), // @ annotation marker one_of(">() -> impl Parser<'a, ParserInput<'a>, TokenKind, ParserError<'a>> { choice(( just("->").to(TokenKind::ArrowThin), just("=>").to(TokenKind::ArrowFat), just("==").to(TokenKind::Eq), just("!=").to(TokenKind::Ne), just(">=").to(TokenKind::Gte), just("<=").to(TokenKind::Lte), just("~=").to(TokenKind::RegexSearch), just("&&").then_ignore(end_expr()).to(TokenKind::And), just("||").then_ignore(end_expr()).to(TokenKind::Or), just("??").to(TokenKind::Coalesce), just("//").to(TokenKind::DivInt), just("**").to(TokenKind::Pow), )) } fn keyword<'a>() -> impl Parser<'a, ParserInput<'a>, TokenKind, ParserError<'a>> { choice(( just("let"), just("into"), just("case"), just("prql"), just("type"), just("module"), just("internal"), just("func"), just("import"), just("enum"), )) .to_slice() .then_ignore(end_expr()) .map(|s: &str| TokenKind::Keyword(s.to_string())) } fn param<'a>() -> impl Parser<'a, ParserInput<'a>, TokenKind, ParserError<'a>> { just('$') .ignore_then( any() .filter(|c: &char| c.is_alphanumeric() || *c == '_' || *c == '.') .repeated() .to_slice() .map(|s: &str| s.to_string()), ) .map(TokenKind::Param) } fn interpolation<'a>() -> impl Parser<'a, ParserInput<'a>, TokenKind, ParserError<'a>> { // For s-strings and f-strings, use the same multi-quote string parser // Enable escaping so that `\"` in the source becomes a literal `"` in the string // // NOTE: Known limitation in error reporting for unclosed interpolated strings: // When an f-string or s-string is unclosed (e.g., `f"{}`), the error is reported at the // opening quote position (e.g., position 17) rather than at the end of input where the // closing quote should be (e.g., position 20). This is because the `.then()` combinator // modifies error spans during error recovery, and there's no way to prevent this from // custom parsers. one_of("sf") .then(quoted_string(true)) .map(|(c, s)| TokenKind::Interpolation(c, s)) } fn whitespace<'a>() -> impl Parser<'a, ParserInput<'a>, (), ParserError<'a>> { text::inline_whitespace().at_least(1) } // Custom newline parser for Stream fn newline<'a>() -> impl Parser<'a, ParserInput<'a>, (), ParserError<'a>> { just('\n') .or(just('\r').then_ignore(just('\n').or_not())) .ignored() } fn line_wrap<'a>() -> impl Parser<'a, ParserInput<'a>, TokenKind, ParserError<'a>> { newline() .ignore_then( whitespace() .repeated() .ignore_then(comment()) .then_ignore(newline()) .repeated() .collect(), ) .then_ignore(whitespace().repeated()) .then_ignore(just('\\')) .map(TokenKind::LineWrap) } fn comment<'a>() -> impl Parser<'a, ParserInput<'a>, TokenKind, ParserError<'a>> { // Extract the common comment text parser let comment_text = none_of("\n\r").repeated().collect::(); just('#').ignore_then( // One option would be to check that doc comments have new lines in the // lexer (we currently do in the parser); which would give better error // messages? just('!') .ignore_then(comment_text.map(TokenKind::DocComment)) .or(comment_text.map(TokenKind::Comment)), ) } pub fn ident_part<'a>() -> impl Parser<'a, ParserInput<'a>, String, ParserError<'a>> { let plain = any() .filter(|c: &char| c.is_alphabetic() || *c == '_') .then( // this could _almost_ just be, but we don't currently allow numbers // (should we?) // // .then(text::ascii::ident()) any() .filter(|c: &char| c.is_alphanumeric() || *c == '_') .repeated(), ) .to_slice() .map(|s: &str| s.to_string()); let backtick = none_of('`') .repeated() .collect::() .delimited_by(just('`'), just('`')); choice((plain, backtick)) } // Date/time components fn digits<'a>(count: usize) -> impl Parser<'a, ParserInput<'a>, &'a str, ParserError<'a>> { chumsky::text::digits(10).exactly(count).to_slice() } fn date_inner<'a>() -> impl Parser<'a, ParserInput<'a>, String, ParserError<'a>> { // Format: YYYY-MM-DD text::digits(10) .exactly(4) .then(just('-')) .then(text::digits(10).exactly(2)) .then(just('-')) .then(text::digits(10).exactly(2)) .to_slice() // TODO: Returning &str instead of String would require changing Literal::Date // to use Cow<'a, str> or a similar approach, which is a larger refactoring .map(|s: &str| s.to_owned()) } fn time_inner<'a>() -> impl Parser<'a, ParserInput<'a>, String, ParserError<'a>> { // Helper function for parsing time components with separators fn time_component<'p>( separator: char, component_parser: impl Parser<'p, ParserInput<'p>, &'p str, ParserError<'p>>, ) -> impl Parser<'p, ParserInput<'p>, String, ParserError<'p>> { just(separator) .then(component_parser) .map(move |(sep, comp): (char, &str)| format!("{}{}", sep, comp)) .or_not() .map(|opt| opt.unwrap_or_default()) } // Hours (required) let hours = digits(2).map(|s: &str| s.to_string()); // Minutes and seconds (optional) - with colon separator let minutes = time_component(':', digits(2)); let seconds = time_component(':', digits(2)); // Milliseconds (optional) - with dot separator let milliseconds = time_component( '.', any() .filter(|c: &char| c.is_ascii_digit()) .repeated() .at_least(1) .at_most(6) .to_slice(), ); // Timezone (optional): either 'Z' or '+/-HH:MM' let timezone = choice(( just('Z').map(|c| c.to_string()), one_of("-+") .then(digits(2).then(just(':').or_not().then(digits(2))).map( |(hrs, (_opt_colon, mins)): (&str, (Option, &str))| { // Always format as -0800 without colon for SQL compatibility, regardless of input format // We need to handle both -08:00 and -0800 input formats but standardize the output format!("{}{}", hrs, mins) }, )) .map(|(sign, offset)| format!("{}{}", sign, offset)), )) .or_not() .map(|opt| opt.unwrap_or_default()); // Combine all parts hours .then(minutes) .then(seconds) .then(milliseconds) .then(timezone) .map(|((((hours, mins), secs), ms), tz)| format!("{}{}{}{}{}", hours, mins, secs, ms, tz)) } fn date_token<'a>() -> impl Parser<'a, ParserInput<'a>, TokenKind, ParserError<'a>> { // Match digit after @ for date/time literals just('@') // The next character should be a digit .then(any().filter(|c: &char| c.is_ascii_digit()).rewind()) .ignore_then( // Once we know it's a date/time literal (@ followed by a digit), // parse the three possible formats choice(( // Datetime: @2022-01-01T12:00 date_inner() .then(just('T')) .then(time_inner()) .then_ignore(end_expr()) .map(|((date, t), time)| Literal::Timestamp(format!("{}{}{}", date, t, time))), // Date: @2022-01-01 date_inner().then_ignore(end_expr()).map(Literal::Date), // Time: @12:00 time_inner().then_ignore(end_expr()).map(Literal::Time), )), ) .map(TokenKind::Literal) } pub fn literal<'a>() -> impl Parser<'a, ParserInput<'a>, Literal, ParserError<'a>> { choice(( binary_number(), hexadecimal_number(), octal_number(), string(), raw_string(), value_and_unit(), number(), boolean(), null(), )) } // Helper to create number parsers with different bases fn parse_number_with_base<'a>( prefix: &'static str, base: u32, max_digits: usize, valid_digit: impl Fn(&char) -> bool + 'a, ) -> impl Parser<'a, ParserInput<'a>, Literal, ParserError<'a>> { just(prefix) .then_ignore(just("_").or_not()) // Optional underscore after prefix .ignore_then( any() .filter(valid_digit) .repeated() .at_least(1) .at_most(max_digits) .to_slice() .map(move |digits: &str| { i64::from_str_radix(digits, base) .map(Literal::Integer) .unwrap_or(Literal::Integer(0)) }), ) } fn binary_number<'a>() -> impl Parser<'a, ParserInput<'a>, Literal, ParserError<'a>> { parse_number_with_base("0b", 2, 32, |c| *c == '0' || *c == '1') } fn hexadecimal_number<'a>() -> impl Parser<'a, ParserInput<'a>, Literal, ParserError<'a>> { parse_number_with_base("0x", 16, 12, |c| c.is_ascii_hexdigit()) } fn octal_number<'a>() -> impl Parser<'a, ParserInput<'a>, Literal, ParserError<'a>> { parse_number_with_base("0o", 8, 12, |c| ('0'..='7').contains(c)) } fn number<'a>() -> impl Parser<'a, ParserInput<'a>, Literal, ParserError<'a>> { // Helper function to build a string from optional number components fn optional_component<'p, T>( parser: impl Parser<'p, ParserInput<'p>, T, ParserError<'p>>, to_string: impl Fn(T) -> String + 'p, ) -> impl Parser<'p, ParserInput<'p>, String, ParserError<'p>> { parser .map(to_string) .or_not() .map(|opt| opt.unwrap_or_default()) } // Parse integer part let integer = parse_integer(); // Parse fractional part let fraction_digits = any() .filter(|c: &char| c.is_ascii_digit()) .then( any() .filter(|c: &char| c.is_ascii_digit() || *c == '_') .repeated(), ) .to_slice(); let frac = just('.') .then(fraction_digits) .map(|(dot, digits): (char, &str)| format!("{}{}", dot, digits)); // Parse exponent let exp_digits = one_of("+-") .or_not() .then( any() .filter(|c: &char| c.is_ascii_digit()) .repeated() .at_least(1), ) .to_slice(); let exp = one_of("eE") .then(exp_digits) .map(|(e, digits): (char, &str)| format!("{}{}", e, digits)); // Combine all parts into a number using the helper function integer .then(optional_component(frac, |f| f)) .then(optional_component(exp, |e| e)) .map(|((int_part, frac_part), exp_part)| { // Construct the number string and remove underscores let num_str = format!("{}{}{}", int_part, frac_part, exp_part) .chars() .filter(|&c| c != '_') .collect::(); // Try to parse as integer first, then as float if let Ok(i) = num_str.parse::() { Literal::Integer(i) } else if let Ok(f) = num_str.parse::() { Literal::Float(f) } else { Literal::Integer(0) // Fallback } }) } fn parse_integer<'a>() -> impl Parser<'a, ParserInput<'a>, &'a str, ParserError<'a>> { // Handle both multi-digit numbers (can't start with 0) and single digit 0 choice(( any() .filter(|c: &char| c.is_ascii_digit() && *c != '0') .then( any() .filter(|c: &char| c.is_ascii_digit() || *c == '_') .repeated(), ) .to_slice(), just('0').to_slice(), )) } fn string<'a>() -> impl Parser<'a, ParserInput<'a>, Literal, ParserError<'a>> { quoted_string(true).map(Literal::String) } fn raw_string<'a>() -> impl Parser<'a, ParserInput<'a>, Literal, ParserError<'a>> { just("r") .then(choice((just('\''), just('"')))) .then( any() .filter(move |c: &char| *c != '\'' && *c != '"' && *c != '\n' && *c != '\r') .repeated() .to_slice(), ) .then(choice((just('\''), just('"')))) .map( |(((_, _open_quote), s), _close_quote): (((&str, char), &str), char)| { Literal::RawString(s.to_string()) }, ) } fn boolean<'a>() -> impl Parser<'a, ParserInput<'a>, Literal, ParserError<'a>> { choice((just("true").to(true), just("false").to(false))) .then_ignore(end_expr()) .map(Literal::Boolean) } fn null<'a>() -> impl Parser<'a, ParserInput<'a>, Literal, ParserError<'a>> { just("null").to(Literal::Null).then_ignore(end_expr()) } fn value_and_unit<'a>() -> impl Parser<'a, ParserInput<'a>, Literal, ParserError<'a>> { // Supported time units let unit = choice(( just("microseconds"), just("milliseconds"), just("seconds"), just("minutes"), just("hours"), just("days"), just("weeks"), just("months"), just("years"), )); // Parse the integer value followed by a unit parse_integer().then(unit).then_ignore(end_expr()).map( |(number_str, unit_str): (&str, &str)| { // Parse the number (removing underscores), defaulting to 1 if parsing fails let n = number_str.replace('_', "").parse::().unwrap_or(1); Literal::ValueAndUnit(ValueAndUnit { n, unit: unit_str.to_string(), }) }, ) } pub fn quoted_string<'a>( escaped: bool, ) -> impl Parser<'a, ParserInput<'a>, String, ParserError<'a>> { choice(( multi_quoted_string(&'"', escaped), multi_quoted_string(&'\'', escaped), )) .map(|chars| chars.into_iter().collect()) } // Helper function to parse escape sequences // Takes the input and the quote character, returns the escaped character fn parse_escape_sequence<'a>( input: &mut chumsky::input::InputRef<'a, '_, ParserInput<'a>, ParserError<'a>>, quote_char: char, ) -> char { match input.peek() { Some(next_ch) => { input.next(); match next_ch { '\\' => '\\', '/' => '/', 'b' => '\x08', 'f' => '\x0C', 'n' => '\n', 'r' => '\r', 't' => '\t', 'u' if input.peek() == Some('{') => { input.next(); // consume '{' let mut hex = String::new(); while let Some(ch) = input.peek() { if ch == '}' { input.next(); break; } if ch.is_ascii_hexdigit() && hex.len() < 6 { hex.push(ch); input.next(); } else { break; } } char::from_u32(u32::from_str_radix(&hex, 16).unwrap_or(0)).unwrap_or('\u{FFFD}') } 'x' => { let mut hex = String::new(); for _ in 0..2 { if let Some(ch) = input.peek() { if ch.is_ascii_hexdigit() { hex.push(ch); input.next(); } } } if hex.len() == 2 { char::from_u32(u32::from_str_radix(&hex, 16).unwrap_or(0)) .unwrap_or('\u{FFFD}') } else { next_ch // Just use the character after backslash } } c if c == quote_char => quote_char, // Escaped quote other => other, // Unknown escape, keep the character } } None => { // Backslash at end of input '\\' } } } // Implementation of multi-level quoted strings using custom parser // Handles odd number of quotes (1, 3, 5, etc.) for strings with content // and even number of quotes (2, 4, 6, etc.) for empty strings // // This uses a single custom parser that dynamically handles arbitrary quote counts // All quoted strings allow newlines fn multi_quoted_string<'a>( quote: &char, escaping: bool, ) -> impl Parser<'a, ParserInput<'a>, Vec, ParserError<'a>> { let quote_char = *quote; custom(move |input| { let start_cursor = input.save(); // Count opening quotes let mut open_count = 0; while let Some(ch) = input.peek() { if ch == quote_char { input.next(); open_count += 1; } else { break; } } if open_count == 0 { let span = input.span_since(start_cursor.cursor()); return Err(Simple::new(input.peek_maybe(), span)); } // Even number of quotes -> empty string if open_count % 2 == 0 { return Ok(vec![]); } // Odd number of quotes -> parse content until we find the closing delimiter let mut result = Vec::new(); loop { // Save position to potentially rewind let checkpoint = input.save(); // Try to match the closing delimiter (open_count quotes) let mut close_count = 0; while close_count < open_count { match input.peek() { Some(ch) if ch == quote_char => { input.next(); close_count += 1; } _ => break, } } // If we matched the full delimiter, we're done if close_count == open_count { return Ok(result); } // Not the delimiter - rewind and consume one content character input.rewind(checkpoint); match input.next() { Some(ch) => { // Handle escape sequences if escaping is enabled if escaping && ch == '\\' { let escaped = parse_escape_sequence(input, quote_char); result.push(escaped); } else { result.push(ch); } } None => { // Can't find closing delimiter - return error about unclosed string // Create a zero-width span at the current position (end of input) let current_cursor = input.save(); let span = input.span_since(current_cursor.cursor()); return Err(Simple::new(None, span)); } } } }) } fn end_expr<'a>() -> impl Parser<'a, ParserInput<'a>, (), ParserError<'a>> { choice(( end(), one_of(",)]}\t >").to(()), newline(), just("..").to(()), )) .rewind() } ================================================ FILE: prqlc/prqlc-parser/src/lexer/test.rs ================================================ use chumsky; use chumsky::Parser; use insta::assert_debug_snapshot; use insta::assert_snapshot; use crate::lexer::lex_source; use crate::lexer::lr::{Literal, TokenKind, Tokens}; use crate::lexer::{lexer, literal, quoted_string}; #[test] fn line_wrap() { fn test_line_wrap_tokens(input: &str) -> Tokens { Tokens(lexer().parse(input).output().unwrap().to_vec()) } assert_eq!( format!( "{}", TokenKind::LineWrap(vec![TokenKind::Comment(" a comment".to_string())]) ), r#" \ # a comment "# ); // Basic line wrap test assert_debug_snapshot!(test_line_wrap_tokens(r"5 + \ 3 "), @r" Tokens( [ 0..1: Literal(Integer(5)), 2..3: Control('+'), 3..9: LineWrap([]), 10..11: Literal(Integer(3)), ], ) "); // Comments in line wrap test assert_debug_snapshot!(test_line_wrap_tokens(r"5 + # comment # comment with whitespace \ 3 "), @r#" Tokens( [ 0..1: Literal(Integer(5)), 2..3: Control('+'), 3..46: LineWrap([Comment(" comment"), Comment(" comment with whitespace")]), 47..48: Literal(Integer(3)), ], ) "#); } #[test] fn numbers() { fn test_number_parsing(input: &str, expected: Literal) { assert_eq!(literal().parse(input).output().unwrap(), &expected); } // Binary notation test_number_parsing("0b1111000011110000", Literal::Integer(61680)); test_number_parsing("0b_1111000011110000", Literal::Integer(61680)); // Hexadecimal notation test_number_parsing("0xff", Literal::Integer(255)); test_number_parsing("0x_deadbeef", Literal::Integer(3735928559)); // Octal notation test_number_parsing("0o777", Literal::Integer(511)); } #[test] fn debug_display() { fn test_tokens(input: &str) -> Tokens { Tokens(lexer().parse(input).output().unwrap().to_vec()) } assert_debug_snapshot!(test_tokens("5 + 3"), @r" Tokens( [ 0..1: Literal(Integer(5)), 2..3: Control('+'), 4..5: Literal(Integer(3)), ], ) "); } #[test] fn comment() { // The format rendering test can be shared since it's independent of Chumsky assert_snapshot!(TokenKind::Comment(" This is a single-line comment".to_string()), @"# This is a single-line comment"); // For the parser test, we use a unified function fn test_comment_tokens(input: &str) -> Tokens { Tokens(lexer().parse(input).output().unwrap().to_vec()) } assert_debug_snapshot!(test_comment_tokens("# comment\n# second line"), @r#" Tokens( [ 0..9: Comment(" comment"), 9..10: NewLine, 10..23: Comment(" second line"), ], ) "#); } #[test] fn doc_comment() { // Unified function to test doccomment tokens fn test_doc_comment_tokens(input: &str) -> Tokens { Tokens(lexer().parse(input).output().unwrap().to_vec()) } assert_debug_snapshot!(test_doc_comment_tokens("#! docs"), @r#" Tokens( [ 0..7: DocComment(" docs"), ], ) "#); } #[test] fn quotes() { fn test_basic_string(input: &str, escaped: bool, expected_str: &str) { let parse_result = quoted_string(escaped).parse(input); if let Some(result) = parse_result.output() { assert_eq!(result, expected_str); } else { panic!("Failed to parse string: {:?}", input); } } test_basic_string(r#"'aoeu'"#, false, "aoeu"); test_basic_string(r#"''"#, true, ""); test_basic_string(r#""hello""#, true, "hello"); test_basic_string(r#""hello\nworld""#, true, "hello\nworld"); // Test escaped quotes let basic_escaped = r#""hello\\""#; // Test just a backslash escape test_basic_string(basic_escaped, true, "hello\\"); // Triple-quoted string tests test_basic_string(r#"'''aoeu'''"#, false, "aoeu"); test_basic_string(r#""""aoeu""""#, true, "aoeu"); // Add more tests for our implementation test_basic_string(r#""hello world""#, true, "hello world"); } #[test] fn interpolated_strings() { // Helper function to test interpolated string tokens fn test_interpolation_tokens(input: &str) -> Tokens { Tokens(lexer().parse(input).output().unwrap().to_vec()) } // Test s-string and f-string with regular quotes assert_debug_snapshot!(test_interpolation_tokens(r#"s"Hello {name}""#), @r#" Tokens( [ 0..15: Interpolation('s', "Hello {name}"), ], ) "#); // Test s-string with triple quotes (important for multi-line SQL in s-strings) assert_debug_snapshot!(test_interpolation_tokens(r#"s"""SELECT * FROM table WHERE id = {id}""" "#), @r#" Tokens( [ 0..42: Interpolation('s', "SELECT * FROM table WHERE id = {id}"), ], ) "#); // Test s-string with escaped quotes (issue #5494 regression) assert_debug_snapshot!(test_interpolation_tokens(r#"s"SELECT \"col1 foo\"""#), @r#" Tokens( [ 0..22: Interpolation('s', "SELECT \"col1 foo\""), ], ) "#); } #[test] fn timestamp_tests() { // Helper function to test tokens with timestamps fn test_timestamp_tokens(input: &str) -> Tokens { Tokens(lexer().parse(input).output().unwrap().to_vec()) } // Test timestamp with timezone format -08:00 (with colon) assert_debug_snapshot!(test_timestamp_tokens("@2020-01-01T13:19:55-08:00"), @r#" Tokens( [ 0..26: Literal(Timestamp("2020-01-01T13:19:55-0800")), ], ) "#); // Test timestamp with timezone format Z assert_debug_snapshot!(test_timestamp_tokens("@2020-01-02T21:19:55Z"), @r#" Tokens( [ 0..21: Literal(Timestamp("2020-01-02T21:19:55Z")), ], ) "#); } #[test] fn range() { fn test_range_tokens(input: &str) -> Tokens { Tokens(lexer().parse(input).output().unwrap().to_vec()) } assert_debug_snapshot!(test_range_tokens("1..2"), @r" Tokens( [ 0..1: Literal(Integer(1)), 1..3: Range { bind_left: true, bind_right: true }, 3..4: Literal(Integer(2)), ], ) "); assert_debug_snapshot!(test_range_tokens("..2"), @r" Tokens( [ 0..2: Range { bind_left: true, bind_right: true }, 2..3: Literal(Integer(2)), ], ) "); assert_debug_snapshot!(test_range_tokens("1.."), @r" Tokens( [ 0..1: Literal(Integer(1)), 1..3: Range { bind_left: true, bind_right: true }, ], ) "); let result = test_range_tokens("in ..5"); // Just verify we have 3 tokens, with the right types and values assert_eq!(result.0.len(), 3); // Check token types assert!(matches!(result.0[0].kind, TokenKind::Ident(ref s) if s == "in")); assert!(matches!(result.0[1].kind, TokenKind::Range { .. })); assert!(matches!( result.0[2].kind, TokenKind::Literal(Literal::Integer(5)) )); } #[test] fn test_lex_source() { use insta::assert_debug_snapshot; assert_debug_snapshot!(lex_source("5 + 3"), @r" Ok( Tokens( [ 0..0: Start, 0..1: Literal(Integer(5)), 2..3: Control('+'), 4..5: Literal(Integer(3)), ], ), ) "); let result = lex_source("^"); assert!(result.is_err()); } #[test] fn test_annotation_tokens() { use insta::assert_debug_snapshot; // Test basic annotation token let result = super::lex_source("@{binding_strength=1}"); assert_debug_snapshot!(result, @r#" Ok( Tokens( [ 0..0: Start, 0..1: Annotate, 1..2: Control('{'), 2..18: Ident("binding_strength"), 18..19: Control('='), 19..20: Literal(Integer(1)), 20..21: Control('}'), ], ), ) "#); // Test multi-line annotation let result = super::lex_source( r#" @{binding_strength=1} let add = a b -> a + b "#, ); assert_debug_snapshot!(result, @r#" Ok( Tokens( [ 0..0: Start, 0..1: NewLine, 9..10: Annotate, 10..11: Control('{'), 11..27: Ident("binding_strength"), 27..28: Control('='), 28..29: Literal(Integer(1)), 29..30: Control('}'), 30..31: NewLine, 39..42: Keyword("let"), 43..46: Ident("add"), 47..48: Control('='), 49..50: Ident("a"), 51..52: Ident("b"), 53..55: ArrowThin, 56..57: Ident("a"), 58..59: Control('+'), 60..61: Ident("b"), 61..62: NewLine, ], ), ) "#); } #[test] fn test_issue_triple_quoted_with_double_quote() { use insta::assert_debug_snapshot; // The specific test case from ISSUE.md that was failing let input = r#"""" '' Canada " """"#; let result = super::lex_source(input); eprintln!("Result: {:#?}", result); assert_debug_snapshot!(result, @r#" Ok( Tokens( [ 0..0: Start, 0..20: Literal(String("\n''\nCanada\n\"\n\n")), ], ), ) "#); } #[test] fn test_single_curly_quote() { use insta::assert_debug_snapshot; // Test what error we get for a single curly quote character let input = "’"; // U+2019 RIGHT SINGLE QUOTATION MARK eprintln!("\n=== Single Curly Quote Test ==="); eprintln!("Input: {:?}", input); eprintln!("Input bytes: {:?}", input.as_bytes()); eprintln!( "Char 0: {:?} (U+{:04X})", input.chars().next().unwrap(), input.chars().next().unwrap() as u32 ); let result = lex_source(input); eprintln!("Result: {:#?}", result); assert_debug_snapshot!(result, @r#" Err( [ Error { kind: Error, span: Some( 0:0-1, ), reason: Unexpected { found: "'’'", }, hints: [], code: None, }, ], ) "#); } #[test] fn test_mississippi_curly_quotes() { use insta::assert_debug_snapshot; // Test error reporting for curly quotes (U+2019) // This is the Mississippi test case from integration tests // NOTE: The quotes in this string are U+2019 RIGHT SINGLE QUOTATION MARK (curly quotes), // not U+0027 APOSTROPHE. Make sure your editor preserves them! let input = "Mississippi has four S’s and four I’s."; eprintln!("\n=== Mississippi Curly Quotes Test ==="); eprintln!("Input: {:?}", input); eprintln!("Input bytes: {:?}", input.as_bytes()); eprintln!( "Char 22: {:?} (U+{:04X})", input.chars().nth(22).unwrap(), input.chars().nth(22).unwrap() as u32 ); eprintln!( "Char 35: {:?} (U+{:04X})", input.chars().nth(35).unwrap(), input.chars().nth(35).unwrap() as u32 ); let result1 = lex_source(input); eprintln!("{:#?}", result1); let (tokens, errors) = super::lex_source_recovery(input, 1); eprintln!("Tokens: {:#?}", tokens); eprintln!("Errors: {:#?}", errors); assert_debug_snapshot!(result1, @r#" Err( [ Error { kind: Error, span: Some( 0:22-23, ), reason: Unexpected { found: "'’'", }, hints: [], code: None, }, ], ) "#); } #[test] fn test_interpolation_empty() { use insta::assert_debug_snapshot; // Test the f"{}" case that's showing a changed error position let input = r#"from x | select f"{}"#; eprintln!("\n=== Interpolation Empty Test ==="); eprintln!("Input: {:?}", input); eprintln!("Input bytes: {:?}", input.as_bytes()); eprintln!( "Input length: {} bytes, {} chars", input.len(), input.chars().count() ); let result = lex_source(input); eprintln!("lex_source result: {:#?}", result); let (tokens, errors) = super::lex_source_recovery(input, 1); eprintln!("lex_source_recovery tokens: {:#?}", tokens); eprintln!("lex_source_recovery errors: {:#?}", errors); assert_debug_snapshot!(result, @r#" Err( [ Error { kind: Error, span: Some( 0:20-20, ), reason: Unexpected { found: "end of input", }, hints: [], code: None, }, ], ) "#); } ================================================ FILE: prqlc/prqlc-parser/src/lib.rs ================================================ pub mod error; pub mod generic; pub mod lexer; pub mod parser; pub mod span; #[cfg(test)] pub(crate) mod test; ================================================ FILE: prqlc/prqlc-parser/src/parser/expr.rs ================================================ use std::collections::{hash_map::Entry, HashMap}; use chumsky; use chumsky::input::BorrowInput; use chumsky::pratt::*; use chumsky::prelude::*; use itertools::Itertools; use crate::lexer::lr; use crate::lexer::lr::TokenKind; use crate::parser::interpolation; use crate::parser::pr::*; use crate::parser::types::type_expr; use crate::parser::{ctrl, ident_part, keyword, new_line, sequence, with_doc_comment}; use crate::span::Span; use super::pipe; use super::ParserError; pub(crate) fn expr_call<'a, I>() -> impl Parser<'a, I, Expr, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { let expr = expr(); choice(( lambda_func(expr.clone()).boxed(), func_call(expr.clone()).boxed(), pipeline(expr).boxed(), )) .boxed() } pub(crate) fn expr<'a, I>() -> impl Parser<'a, I, Expr, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { recursive(|expr| { let literal = select_ref! { lr::Token { kind: TokenKind::Literal(lit), .. } => ExprKind::Literal(lit.clone()) }; let ident_kind = ident().map(ExprKind::Ident); let internal = keyword("internal") .ignore_then(ident()) .map(|x| x.to_string()) .map(ExprKind::Internal); let nested_expr = with_doc_comment( lambda_func(expr.clone()) .or(func_call(expr.clone())) .boxed(), ); let tuple = tuple(nested_expr.clone()); let array = array(nested_expr.clone()); let pipeline_expr = { use chumsky::recovery::{skip_then_retry_until, via_parser}; pipeline(nested_expr.clone()) .padded_by(new_line().repeated()) .delimited_by( ctrl('('), ctrl(')') .recover_with(via_parser(end())) .recover_with(skip_then_retry_until( any_ref().ignored(), ctrl(')').ignored().or(end()), )), ) }; let interpolation = interpolation(); let case = case(expr.clone()); let param = select_ref! { lr::Token { kind: TokenKind::Param(id), .. } => ExprKind::Param(id.clone()) }; let term = with_doc_comment( choice(( literal, internal, tuple, array, interpolation, ident_kind, case, param, )) .map_with(|kind, extra| ExprKind::into_expr(kind, extra.span())) // No longer used given the TODO in `pipeline`; can remove if we // don't resolve. // .or(aliased(expr.clone())) .or(pipeline_expr), ) .boxed(); let term = unary(term); let term = range(term); // Binary operators using Pratt parsing // Precedence levels (higher = tighter binding): // 6: Pow (right associative) // 5: Mul, Div, Mod (left associative) // 4: Add, Sub (left associative) // 3: Compare operators (left associative) // 2: Coalesce (left associative) // 1: And (left associative) // 0: Or (left associative) term.pratt(( infix(right(6), operator_pow(), |left, op, right, extra| { let span = extra.span(); ExprKind::Binary(BinaryExpr { left: Box::new(left), op, right: Box::new(right), }) .into_expr(span) }), infix(left(5), operator_mul(), |left, op, right, extra| { let span = extra.span(); ExprKind::Binary(BinaryExpr { left: Box::new(left), op, right: Box::new(right), }) .into_expr(span) }), infix(left(4), operator_add(), |left, op, right, extra| { let span = extra.span(); ExprKind::Binary(BinaryExpr { left: Box::new(left), op, right: Box::new(right), }) .into_expr(span) }), infix(left(3), operator_compare(), |left, op, right, extra| { let span = extra.span(); ExprKind::Binary(BinaryExpr { left: Box::new(left), op, right: Box::new(right), }) .into_expr(span) }), infix(left(2), operator_coalesce(), |left, op, right, extra| { let span = extra.span(); ExprKind::Binary(BinaryExpr { left: Box::new(left), op, right: Box::new(right), }) .into_expr(span) }), infix(left(1), operator_and(), |left, op, right, extra| { let span = extra.span(); ExprKind::Binary(BinaryExpr { left: Box::new(left), op, right: Box::new(right), }) .into_expr(span) }), infix(left(0), operator_or(), |left, op, right, extra| { let span = extra.span(); ExprKind::Binary(BinaryExpr { left: Box::new(left), op, right: Box::new(right), }) .into_expr(span) }), )) .boxed() }) } fn tuple<'a, I>( nested_expr: impl Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a, ) -> impl Parser<'a, I, ExprKind, ParserError<'a>> + Clone + 'a where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { use chumsky::recovery::{skip_then_retry_until, via_parser}; sequence(maybe_aliased(nested_expr)) .delimited_by( ctrl('{'), ctrl('}') .recover_with(via_parser(end())) .recover_with(skip_then_retry_until( any_ref().ignored(), ctrl('}').ignored().or(ctrl(',').ignored()).or(end()), )), ) .map(ExprKind::Tuple) .labelled("tuple") .boxed() } fn array<'a, I>( expr: impl Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a, ) -> impl Parser<'a, I, ExprKind, ParserError<'a>> + Clone + 'a where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { use chumsky::recovery::{skip_then_retry_until, via_parser}; sequence(expr) .delimited_by( ctrl('['), ctrl(']') .recover_with(via_parser(end())) .recover_with(skip_then_retry_until( any_ref().ignored(), ctrl(']').ignored().or(ctrl(',').ignored()).or(end()), )), ) .map(ExprKind::Array) .labelled("array") .boxed() } fn interpolation<'a, I>() -> impl Parser<'a, I, ExprKind, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { select_ref! { lr::Token { kind: TokenKind::Interpolation('s', string), .. } => (ExprKind::SString as fn(_) -> _, string.clone()), lr::Token { kind: TokenKind::Interpolation('f', string), .. } => (ExprKind::FString as fn(_) -> _, string.clone()), } .validate(|(finish, string), extra, emit| { let span = extra.span(); match interpolation::parse(string, span + 2) { Ok(items) => finish(items), Err(errors) => { for err in errors { // Convert Error to Rich for emission let err_span = err.span.unwrap_or(span); // Use the reason's Display impl, not Error's Debug let message = err.reason.to_string(); emit.emit(Rich::custom(err_span, message)); } finish(vec![]) } } }) .labelled("interpolated string") } fn case<'a, I>( expr: impl Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a, ) -> impl Parser<'a, I, ExprKind, ParserError<'a>> + Clone + 'a where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { // The `nickname != null => nickname,` part let mapping = func_call(expr.clone()) .map(Box::new) .then_ignore(select_ref! { lr::Token { kind: TokenKind::ArrowFat, .. } => () }) .then(func_call(expr).map(Box::new)) .map(|(condition, value)| SwitchCase { condition, value }); keyword("case") .ignore_then(sequence(mapping).delimited_by(ctrl('['), ctrl(']'))) .map(ExprKind::Case) } fn unary<'a, I, E>(expr: E) -> impl Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, E: Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a, { expr.clone() .or(operator_unary() .then(expr.map(Box::new)) .map(|(op, expr)| ExprKind::Unary(UnaryExpr { op, expr })) .map_with(|kind, extra| ExprKind::into_expr(kind, extra.span()))) .boxed() } fn range<'a, I, E>(expr: E) -> impl Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, E: Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a, { // Ranges have five cases we need to parse: // x..y (bounded) // x.. (only start bound) // x (no-op) // ..y (only end bound) // .. (unbounded) #[derive(Clone)] enum RangeCase { NoOp(Expr), Range(Option, Option), } choice(( // with start bound (first 3 cases) expr.clone() .then(choice(( // range and end bound select_ref! { lr::Token { kind: TokenKind::Range { bind_left: true, bind_right: true }, .. } => () } .ignore_then(expr.clone()) .map(|x| Some(Some(x))), // range and no end bound select_ref! { lr::Token { kind: TokenKind::Range { bind_left: true, .. }, .. } => Some(None) }, // no range empty().to(None), ))) .map(|(start, range)| { if let Some(end) = range { RangeCase::Range(Some(start), end) } else { RangeCase::NoOp(start) } }), // only end bound select_ref! { lr::Token { kind: TokenKind::Range { bind_right: true, .. }, .. } => () } .ignore_then(expr) .map(|range| RangeCase::Range(None, Some(range))), // unbounded select_ref! { lr::Token { kind: TokenKind::Range { .. }, .. } => RangeCase::Range(None, None) }, )) .map_with(|case, extra| { let span = extra.span(); match case { RangeCase::NoOp(x) => x, RangeCase::Range(start, end) => { let kind = ExprKind::Range(Range { start: start.map(Box::new), end: end.map(Box::new), }); kind.into_expr(span) } } }) .boxed() } /// A pipeline of `expr`, separated by pipes. Doesn't require parentheses. pub(crate) fn pipeline<'a, I, E>(expr: E) -> impl Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, E: Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a, { // expr has to be a param, because it can be either a normal expr() or a // recursive expr called from within expr(), which causes a stack overflow // TODO: do we need the `maybe_aliased` here rather than in `expr`? We had // tried `with_doc_comment(expr)` in #4775 (and push an aliased expr into // `expr`) but couldn't get it work. with_doc_comment(maybe_aliased(expr)) .separated_by(pipe()) .at_least(1) .collect::>() .map_with(|exprs: Vec, extra| { let span = extra.span(); // If there's only one expr, then we don't need to wrap it // in a pipeline — just return the lone expr. Otherwise, // wrap them in a pipeline. exprs.into_iter().exactly_one().unwrap_or_else(|exprs| { ExprKind::Pipeline(Pipeline { exprs: exprs.collect(), }) .into_expr(span) }) }) .labelled("pipeline") } // Can remove if we don't end up using this #[allow(dead_code)] #[cfg(not(coverage))] fn aliased<'a, I, E>(expr: E) -> impl Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, E: Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a, { let aliased = ident_part() .then_ignore(ctrl('=')) .then(expr) .map(|(alias, mut expr)| { expr.alias = Some(alias); expr }); // Because `expr` accounts for parentheses, and aliased is `x=$expr`, we // need to allow another layer of parentheses here. aliased .clone() .or(aliased.delimited_by(ctrl('('), ctrl(')'))) } fn maybe_aliased<'a, I, E>(expr: E) -> impl Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, E: Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a, { let aliased = ident_part() .then_ignore(ctrl('=')) // This is added for `maybe_aliased`; possibly we should integrate // the funcs .or_not() .then(expr) .map(|(alias, mut expr)| { expr.alias = alias.or(expr.alias); expr }); // Because `expr` accounts for parentheses, and aliased is `x=$expr`, we // need to allow another layer of parentheses here. aliased .clone() .or(aliased.delimited_by(ctrl('('), ctrl(')'))) } fn func_call<'a, I, E>(expr: E) -> impl Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, E: Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a, { let func_name = expr.clone(); let named_arg = ident_part() .map(Some) .then_ignore(ctrl(':')) .then(expr.clone()); // TODO: I think this possibly should be restructured. Currently in the case // of `derive x = 5`, the `x` is an alias of a single positional argument. // That then means we incorrectly allow something like `derive x = 5 y = 6`, // since there are two positional arguments each with an alias. This then // leads to quite confusing error messages. // // Instead, we could only allow a single alias per function call as the // first positional argument? (I worry that not simple though...). // Alternatively we could change the language to enforce tuples, so `derive // {x = 5}` were required. But we still need to account for the `join` // example below, which doesn't work so well in a tuple; so I'm not sure // this helps much. // // As a reminder, we need to account for `derive x = 5` and `join a=artists // (id==album_id)`. let positional_arg = maybe_aliased(expr.clone()).map(|e| (None, e)); func_name .then(named_arg.or(positional_arg).repeated().collect::>()) .validate( |(name, args): (Expr, Vec<(Option, Expr)>), extra, emit| { let span = extra.span(); if args.is_empty() { return name.kind; } let mut named_args = HashMap::new(); let mut positional = Vec::new(); for (name, arg) in args { if let Some(name) = name { match named_args.entry(name) { Entry::Occupied(entry) => { emit.emit(Rich::custom( span, format!("argument '{}' is used multiple times", entry.key()), )); } Entry::Vacant(entry) => { entry.insert(arg); } } } else { positional.push(arg); } } ExprKind::FuncCall(FuncCall { name: Box::new(name), args: positional, named_args, }) }, ) .map_with(|kind, extra| ExprKind::into_expr(kind, extra.span())) .labelled("function call") .boxed() } fn lambda_func<'a, I, E>(expr: E) -> impl Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, E: Parser<'a, I, Expr, ParserError<'a>> + Clone + 'a, { let param = ident_part() .then(type_expr().delimited_by(ctrl('<'), ctrl('>')).or_not()) .then(ctrl(':').ignore_then(expr.clone().map(Box::new)).or_not()); choice(( // func keyword("func").ignore_then( param .clone() .separated_by(new_line().repeated()) .allow_leading() .allow_trailing() .collect::>(), ), // plain param.repeated().collect(), )) .then_ignore(select_ref! { lr::Token { kind: TokenKind::ArrowThin, .. } => () }) // return type .then(type_expr().delimited_by(ctrl('<'), ctrl('>')).or_not()) // body .then(func_call(expr)) .map(|((params, return_ty), body)| { let (pos, name) = params .into_iter() .map(|((name, ty), default_value)| FuncParam { name, ty, default_value, }) .partition(|p| p.default_value.is_none()); Box::new(Func { params: pos, named_params: name, body: Box::new(body), return_ty, }) }) .map(ExprKind::Func) .map_with(|kind, extra| ExprKind::into_expr(kind, extra.span())) .labelled("function definition") .boxed() } pub(crate) fn ident<'a, I>() -> impl Parser<'a, I, Ident, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { ident_part() .then_ignore(ctrl('.')) .repeated() .collect() .then(choice((ident_part(), ctrl('*').map(|_| "*".to_string())))) .map(|(mut parts, last): (Vec, String)| { parts.push(last); Ident::from_path(parts) }) } fn operator_unary<'a, I>() -> impl Parser<'a, I, UnOp, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { (ctrl('+').to(UnOp::Add)) .or(ctrl('-').to(UnOp::Neg)) .or(ctrl('!').to(UnOp::Not)) .or(select_ref! { lr::Token { kind: TokenKind::Eq, .. } => UnOp::EqSelf }) } fn operator_pow<'a, I>() -> impl Parser<'a, I, BinOp, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { select_ref! { lr::Token { kind: TokenKind::Pow, .. } => BinOp::Pow } } fn operator_mul<'a, I>() -> impl Parser<'a, I, BinOp, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { (select_ref! { lr::Token { kind: TokenKind::DivInt, .. } => BinOp::DivInt }) .or(ctrl('*').to(BinOp::Mul)) .or(ctrl('/').to(BinOp::DivFloat)) .or(ctrl('%').to(BinOp::Mod)) } fn operator_add<'a, I>() -> impl Parser<'a, I, BinOp, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { (ctrl('+').to(BinOp::Add)).or(ctrl('-').to(BinOp::Sub)) } fn operator_compare<'a, I>() -> impl Parser<'a, I, BinOp, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { choice(( select_ref! { lr::Token { kind: TokenKind::Eq, .. } => BinOp::Eq }, select_ref! { lr::Token { kind: TokenKind::Ne, .. } => BinOp::Ne }, select_ref! { lr::Token { kind: TokenKind::Lte, .. } => BinOp::Lte }, select_ref! { lr::Token { kind: TokenKind::Gte, .. } => BinOp::Gte }, select_ref! { lr::Token { kind: TokenKind::RegexSearch, .. } => BinOp::RegexSearch }, ctrl('<').to(BinOp::Lt), ctrl('>').to(BinOp::Gt), )) } fn operator_and<'a, I>() -> impl Parser<'a, I, BinOp, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { select_ref! { lr::Token { kind: TokenKind::And, .. } => BinOp::And } } fn operator_or<'a, I>() -> impl Parser<'a, I, BinOp, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { select_ref! { lr::Token { kind: TokenKind::Or, .. } => BinOp::Or } } fn operator_coalesce<'a, I>() -> impl Parser<'a, I, BinOp, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { select_ref! { lr::Token { kind: TokenKind::Coalesce, .. } => BinOp::Coalesce } } #[cfg(test)] mod tests { use insta::{assert_debug_snapshot, assert_yaml_snapshot}; use super::*; use crate::error::Error; fn parse_expr_call(source: &str) -> Result> { crate::parse_test!( source, new_line() .repeated() .collect::>() .ignore_then(expr_call()) .then_ignore(new_line().repeated()) .then_ignore(end()) ) } fn parse_tuple(source: &str) -> Result> { crate::parse_test!( source, new_line() .repeated() .collect::>() .ignore_then(tuple(expr())) .map_with(|kind, extra| ExprKind::into_expr(kind, extra.span())) .then_ignore(new_line().repeated()) .then_ignore(end()) ) } fn parse_any_expr(source: &str) -> Result> { crate::parse_test!( source, new_line() .repeated() .collect::>() .ignore_then(expr()) ) } fn parse_pipeline(source: &str) -> Result> { crate::parse_test!( source, new_line() .repeated() .collect::>() .ignore_then(pipeline(expr_call())) .then_ignore(new_line().repeated()) .then_ignore(end()) ) } fn parse_case(source: &str) -> Result> { crate::parse_test!( source, new_line() .repeated() .collect::>() .ignore_then(case(expr())) .map_with(|kind, extra| ExprKind::into_expr(kind, extra.span())) .then_ignore(new_line().repeated()) .then_ignore(end()) ) } fn parse_expr_call_complete(source: &str) -> Result> { crate::parse_test!( source, new_line() .repeated() .collect::>() .ignore_then(expr_call()) .then_ignore(end()) ) } #[test] fn test_expr_call() { assert_yaml_snapshot!( parse_expr_call(r#"derive x = 5"#).unwrap(), @r#" FuncCall: name: Ident: - derive span: "0:0-6" args: - Literal: Integer: 5 span: "0:11-12" alias: x span: "0:0-12" "#); assert_yaml_snapshot!( parse_expr_call(r#"aggregate {sum salary}"#).unwrap(), @r#" FuncCall: name: Ident: - aggregate span: "0:0-9" args: - Tuple: - FuncCall: name: Ident: - sum span: "0:11-14" args: - Ident: - salary span: "0:15-21" span: "0:11-21" span: "0:10-22" span: "0:0-22" "#); } // The behavior that expr() doesn't parse aliases is tested by test_tuple #[test] fn test_tuple() { assert_yaml_snapshot!( parse_tuple(r#"{a = 5, b = 6}"#).unwrap(), @r#" Tuple: - Literal: Integer: 5 span: "0:5-6" alias: a - Literal: Integer: 6 span: "0:12-13" alias: b span: "0:0-14" "#); assert_debug_snapshot!( parse_tuple(r#" {a = 5 b = 6}"#).unwrap_err(), @r#" [ Error { kind: Error, span: Some( 0:33-34, ), reason: Expected { who: None, expected: "new line or something else", found: "b", }, hints: [], code: None, }, ] "#); assert_yaml_snapshot!(parse_tuple(r#"{d_str = (d | date.to_text "%Y/%m/%d")}"#).unwrap(), @r#" Tuple: - Pipeline: exprs: - Ident: - d span: "0:10-11" - FuncCall: name: Ident: - date - to_text span: "0:14-26" args: - Literal: String: "%Y/%m/%d" span: "0:27-37" span: "0:14-37" span: "0:10-37" alias: d_str span: "0:0-39" "#); } #[test] fn test_expr() { assert_yaml_snapshot!( parse_any_expr(r#"5+5"#).unwrap(), @r#" Binary: left: Literal: Integer: 5 span: "0:0-1" op: Add right: Literal: Integer: 5 span: "0:2-3" span: "0:0-3" "#); } #[test] fn test_pipeline() { assert_yaml_snapshot!( parse_pipeline(r#" ( from artists derive x = 5 ) "#).unwrap(), @r#" Pipeline: exprs: - FuncCall: name: Ident: - from span: "0:29-33" args: - Ident: - artists span: "0:34-41" span: "0:29-41" - FuncCall: name: Ident: - derive span: "0:56-62" args: - Literal: Integer: 5 span: "0:67-68" alias: x span: "0:56-68" span: "0:13-82" "#); } #[test] fn test_case() { assert_yaml_snapshot!( parse_case(r#" case [ nickname != null => nickname, true => null ] "#).unwrap(), @r#" Case: - condition: Binary: left: Ident: - nickname span: "0:30-38" op: Ne right: Literal: "Null" span: "0:42-46" span: "0:30-46" value: Ident: - nickname span: "0:50-58" - condition: Literal: Boolean: true span: "0:72-76" value: Literal: "Null" span: "0:80-84" span: "0:0-95" "#); } // this should return an error but doesn't yet #[should_panic] #[test] fn should_error_01() { assert_debug_snapshot!( parse_expr_call_complete(r#"derive {x = y z = 3}"#).unwrap_err(), @r###" "###); } #[test] fn tuple_missing_comma() { assert_debug_snapshot!( parse_expr_call_complete(r#"{ x = y z = 3 }"#).unwrap_err(), @r#" [ Error { kind: Error, span: Some( 0:36-37, ), reason: Expected { who: None, expected: "new line or something else", found: "z", }, hints: [], code: None, }, ] "#); } #[test] fn args_in_parens() { // Ensure function arguments allow parentheses assert_yaml_snapshot!( parse_expr_call_complete(r#"f (a) b"#).unwrap(), @r#" FuncCall: name: Ident: - f span: "0:0-1" args: - Ident: - a span: "0:3-4" - Ident: - b span: "0:6-7" span: "0:0-7" "#); assert_yaml_snapshot!( parse_expr_call_complete(r#"f (a=2) b"#).unwrap(), @r#" FuncCall: name: Ident: - f span: "0:0-1" args: - Literal: Integer: 2 span: "0:5-6" alias: a - Ident: - b span: "0:8-9" span: "0:0-9" "#); assert_yaml_snapshot!( parse_expr_call_complete(r#"f (a b)"#).unwrap(), @r#" FuncCall: name: Ident: - f span: "0:0-1" args: - FuncCall: name: Ident: - a span: "0:3-4" args: - Ident: - b span: "0:5-6" span: "0:3-6" span: "0:0-7" "#); } #[test] fn pipeline_starting_with_alias_expr() { let source = r#" ( tbl select t.date ) "#; assert_yaml_snapshot!(parse_pipeline(source).unwrap(), @r#" Pipeline: exprs: - Ident: - tbl span: "0:13-16" - FuncCall: name: Ident: - select span: "0:23-29" args: - Ident: - t - date span: "0:30-36" span: "0:23-36" span: "0:5-42" "#); let source = r#" ( t = tbl select t.date ) "#; assert_yaml_snapshot!(parse_pipeline(source).unwrap(), @r#" Pipeline: exprs: - Ident: - tbl span: "0:17-20" alias: t - FuncCall: name: Ident: - select span: "0:27-33" args: - Ident: - t - date span: "0:34-40" span: "0:27-40" span: "0:5-46" "#); } } ================================================ FILE: prqlc/prqlc-parser/src/parser/interpolation.rs ================================================ use chumsky::input::{SliceInput, ValueInput}; use chumsky::prelude::*; use crate::error::{Error, WithErrorInfo}; use crate::parser::pr::*; use crate::span::Span; /// Parses interpolated strings pub(crate) fn parse(string: String, span_base: Span) -> Result, Vec> { let res = interpolated_parser().parse(string.as_str()); let (output, errors) = res.into_output_errors(); if !errors.is_empty() { return Err(errors .into_iter() .map(|e| { // Adjust span to be relative to span_base let span = Span { start: span_base.start + e.span().start, end: span_base.start + e.span().end, source_id: span_base.source_id, }; // Convert Rich error to our Error format // Custom error formatting for consistent user experience across all PRQL errors. // Chumsky's default format varies between versions and doesn't match our // "{label} expected {X}, but found {Y}" pattern used elsewhere. let message = { // Get the label from contexts (most specific one) let label = e.contexts().last().map(|(pat, _)| pat.to_string()); // Build expected list let expected: Vec<_> = e.expected().map(|e| format!("{e}")).collect(); let expected_str = match expected.len() { 0 => String::new(), 1 => expected[0].clone(), 2 => format!("{} or {}", expected[0], expected[1]), _ => format!( "{}, or {}", expected[..expected.len() - 1].join(", "), expected.last().unwrap() ), }; // Format the found token consistently: quote actual tokens, but not "end of input" let found = if let Some(f) = e.found() { format!("\"{}\"", f) } else { "end of input".to_string() }; if let Some(label) = label { if expected_str.is_empty() { format!("unexpected {found}") } else { format!("{label} expected {expected_str}, but found {found}") } } else if expected_str.is_empty() { format!("unexpected {found}") } else { format!("expected {expected_str}, but found {found}") } }; WithErrorInfo::with_span(Error::new_simple(message), Some(span)) }) .collect()); } // Adjust spans in the output to be relative to span_base let adjusted_output = output .unwrap_or_default() .into_iter() .map(|item| match item { InterpolateItem::Expr { expr, format } => { let adjusted_expr = Box::new(Expr { span: expr.span.map(|s| Span { start: span_base.start + s.start, end: span_base.start + s.end, source_id: span_base.source_id, }), ..(*expr) }); InterpolateItem::Expr { expr: adjusted_expr, format, } } InterpolateItem::String(s) => InterpolateItem::String(s), }) .collect(); Ok(adjusted_output) } fn interpolated_parser<'a, I>( ) -> impl Parser<'a, I, Vec, extra::Err>> where I: ValueInput<'a, Token = char, Span = SimpleSpan> + SliceInput<'a, Slice = &'a str>, { let expr = interpolate_ident_part() .separated_by(just('.')) .at_least(1) .collect() .map(Ident::from_path) .map(ExprKind::Ident) .map_with(|kind, extra| { // Convert SimpleSpan to our Span type (will be adjusted in parse() function) let simple_span: SimpleSpan = extra.span(); let span = Span { start: simple_span.start, end: simple_span.end, source_id: 0, }; ExprKind::into_expr(kind, span) }) .map(Box::new) .labelled("interpolated string variable") .then( just(':') .ignore_then(none_of('}').repeated().collect::()) .or_not(), ) .delimited_by(just('{'), just('}')) .map(|(expr, format)| InterpolateItem::Expr { expr, format }); // Convert double braces to single braces, and fail on any single braces. let string = just("{{") .to('{') .or(just("}}").to('}')) .or(none_of("{}")) .repeated() .at_least(1) .collect::() .map(InterpolateItem::String); expr.or(string).repeated().collect().then_ignore(end()) } pub(crate) fn interpolate_ident_part<'a, I>( ) -> impl Parser<'a, I, String, extra::Err>> + Clone where I: ValueInput<'a, Token = char, Span = SimpleSpan> + SliceInput<'a, Slice = &'a str>, { let plain = any() .filter(|c: &char| c.is_alphabetic() || *c == '_') .then( any() .filter(|c: &char| c.is_alphanumeric() || *c == '_') .repeated(), ) .to_slice() .map(|s: &str| s.to_string()) .labelled("interpolated string"); let backticks = none_of('`') .repeated() .to_slice() .map(|s: &str| s.to_string()) .delimited_by(just('`'), just('`')); plain.or(backticks.labelled("interp:backticks")) } #[test] fn parse_interpolate() { use insta::assert_debug_snapshot; let span_base = Span::new(0, 0..0); assert_debug_snapshot!( parse("concat({a})".to_string(), span_base).unwrap(), @r#" [ String( "concat(", ), Expr { expr: Expr { kind: Ident( [ "a", ], ), span: Some( 0:8-9, ), alias: None, doc_comment: None, }, format: None, }, String( ")", ), ] "#); assert_debug_snapshot!( parse("print('{{hello}}')".to_string(), span_base).unwrap(), @r#" [ String( "print('{hello}')", ), ] "#); assert_debug_snapshot!( parse("concat('{{', a, '}}')".to_string(), span_base).unwrap(), @r#" [ String( "concat('{', a, '}')", ), ] "#); assert_debug_snapshot!( parse("concat('{{', {a}, '}}')".to_string(), span_base).unwrap(), @r#" [ String( "concat('{', ", ), Expr { expr: Expr { kind: Ident( [ "a", ], ), span: Some( 0:14-15, ), alias: None, doc_comment: None, }, format: None, }, String( ", '}')", ), ] "#); } ================================================ FILE: prqlc/prqlc-parser/src/parser/mod.rs ================================================ use chumsky; use chumsky::input::BorrowInput; use chumsky::prelude::*; use chumsky::span::SimpleSpan; use self::pr::{Annotation, Stmt, StmtKind}; use crate::error::Error; use crate::lexer::lr; use crate::lexer::lr::TokenKind; use crate::span::Span; // Type alias for parser error type to reduce verbosity pub(crate) type ParserError<'a> = extra::Err>; mod expr; mod interpolation; pub(crate) mod perror; pub mod pr; pub(crate) mod stmt; #[cfg(test)] mod test; mod types; // Note that `parse_source` is in `prqlc` crate, not in `prqlc-parser` crate, // because it logs using the logging framework in `prqlc`. pub fn parse_lr_to_pr(source_id: u16, lr: Vec) -> (Option>, Vec) { // Filter out comments - we don't want them in the AST let semantic_tokens: Vec<_> = lr .into_iter() .filter(|token| { !matches!( token.kind, lr::TokenKind::Comment(_) | lr::TokenKind::LineWrap(_) ) }) .collect(); // Use built-in Input impl for &[Token], then map_span to convert token indices to byte spans let input = semantic_tokens .as_slice() .map_span(|simple_span: SimpleSpan| { let start_idx = simple_span.start(); let end_idx = simple_span.end(); // Convert token indices to byte offsets in the source file let start = semantic_tokens .get(start_idx) .map(|t| t.span.start) .unwrap_or(0); let end = semantic_tokens .get(end_idx.saturating_sub(1)) .map(|t| t.span.end) .unwrap_or(start); Span { start, end, source_id, } }); let parse_result = stmt::source().parse(input); let (pr, parse_errors) = parse_result.into_output_errors(); let errors = parse_errors.into_iter().map(|e| e.into()).collect(); log::debug!("parse errors: {errors:?}"); (pr, errors) } fn ident_part<'a, I>() -> impl Parser<'a, I, String, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { select_ref! { lr::Token { kind: TokenKind::Ident(ident), .. } => ident.clone(), } } fn keyword<'a, I>(kw: &'static str) -> impl Parser<'a, I, (), ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { select_ref! { lr::Token { kind: TokenKind::Keyword(k), .. } if k == kw => (), } } /// Our approach to new lines is each item consumes new lines _before_ itself, /// but not newlines after itself. This allows us to enforce new lines between /// some items. The only place we handle new lines after an item is in the root /// parser. pub(crate) fn new_line<'a, I>() -> impl Parser<'a, I, (), ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { select_ref! { lr::Token { kind: TokenKind::NewLine, .. } => (), lr::Token { kind: TokenKind::Start, .. } => (), } .labelled("new line") } fn ctrl<'a, I>(char: char) -> impl Parser<'a, I, (), ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { select_ref! { lr::Token { kind: TokenKind::Control(c), .. } if *c == char => (), } } fn into_stmt((annotations, kind): (Vec, StmtKind), span: Span) -> Stmt { Stmt { kind, span: Some(span), annotations, doc_comment: None, } } fn doc_comment<'a, I>() -> impl Parser<'a, I, String, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { // doc comments must start on a new line, so we enforce a new line (which // can also be a file start) before the doc comment // // TODO: we currently lose any empty newlines between doc comments; // eventually we want to retain or restrict them (new_line().repeated().at_least(1).ignore_then(select_ref! { lr::Token { kind: TokenKind::DocComment(dc), .. } => dc.clone(), })) .repeated() .at_least(1) .collect() .map(|lines: Vec| lines.join("\n")) .labelled("doc comment") } fn with_doc_comment<'a, I, P, O>(parser: P) -> impl Parser<'a, I, O, ParserError<'a>> + Clone + 'a where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, P: Parser<'a, I, O, ParserError<'a>> + Clone + 'a, O: SupportsDocComment + 'a, { doc_comment() .or_not() .then(parser) .map(|(doc_comment, inner)| inner.with_doc_comment(doc_comment)) } /// Allows us to surround a parser by `with_doc_comment` and for a doc comment /// to be added to the result, as long as the result implements `SupportsDocComment`. /// /// (In retrospect, we could manage without it, though probably not worth the /// effort to remove it. We could also use it to also support Span items.) trait SupportsDocComment { fn with_doc_comment(self, doc_comment: Option) -> Self; } /// Parse a sequence, allowing commas and new lines between items. Doesn't /// include the surrounding delimiters. fn sequence<'a, I, P, O>(parser: P) -> impl Parser<'a, I, Vec, ParserError<'a>> + Clone + 'a where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, P: Parser<'a, I, O, ParserError<'a>> + Clone + 'a, O: 'a, { parser .separated_by(ctrl(',').then_ignore(new_line().repeated())) .allow_trailing() .collect() // Note because we pad rather than only take the ending new line, we // can't put items that require a new line in a tuple, like: // // ``` // { // !# doc comment // a, // } // ``` // ...but I'm not sure there's a way around it, since we do need to // consume newlines in tuples... .padded_by(new_line().repeated()) } fn pipe<'a, I>() -> impl Parser<'a, I, (), ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { ctrl('|') .ignored() .or(new_line().repeated().at_least(1).ignored()) } #[cfg(test)] mod tests { use insta::assert_debug_snapshot; use super::*; use crate::error::Error; fn parse_doc_comment(source: &str) -> Result> { let tokens = crate::lexer::lex_source(source)?; let semantic_tokens: Vec<_> = tokens .0 .into_iter() .filter(|token| { !matches!( token.kind, crate::lexer::lr::TokenKind::Comment(_) | crate::lexer::lr::TokenKind::LineWrap(_) ) }) .collect(); let input = semantic_tokens .as_slice() .map_span(|simple_span: SimpleSpan| { let start_idx = simple_span.start(); let end_idx = simple_span.end(); let start = semantic_tokens .get(start_idx) .map(|t| t.span.start) .unwrap_or(0); let end = semantic_tokens .get(end_idx.saturating_sub(1)) .map(|t| t.span.end) .unwrap_or(start); Span { start, end, source_id: 0, } }); let parser = doc_comment() .then_ignore(new_line().repeated()) .then_ignore(end()); let (ast, errors) = parser.parse(input).into_output_errors(); if !errors.is_empty() { return Err(errors.into_iter().map(Into::into).collect()); } Ok(ast.unwrap()) } #[test] fn test_doc_comment() { assert_debug_snapshot!(parse_doc_comment(r#" #! doc comment #! another line "#), @r#" Ok( " doc comment\n another line", ) "#); } // Doc comment functionality is tested in stmt.rs tests #[cfg(test)] impl SupportsDocComment for String { fn with_doc_comment(self, _doc_comment: Option) -> Self { self } } } ================================================ FILE: prqlc/prqlc-parser/src/parser/perror.rs ================================================ use chumsky; use chumsky::error::Rich; use crate::error::WithErrorInfo; use crate::error::{Error, Reason}; use crate::lexer::lr::TokenKind; use crate::span::Span; // Helper function to convert Rich errors to our Error type fn rich_error_to_error( span: Span, reason: &chumsky::error::RichReason, token_to_string: impl Fn(&T) -> String, is_whitespace_token: impl Fn(&T) -> bool, ) -> Error where T: std::fmt::Debug, { use chumsky::error::RichReason; let error = match reason { RichReason::ExpectedFound { expected, found } => { use chumsky::error::RichPattern; let expected_strs: Vec = expected .iter() .filter(|p| { // Filter out whitespace tokens unless that's all we're expecting let is_whitespace = match p { RichPattern::EndOfInput => true, RichPattern::Token(t) => is_whitespace_token(t), _ => false, }; !is_whitespace || expected.iter().all(|p| match p { RichPattern::EndOfInput => true, RichPattern::Token(t) => is_whitespace_token(t), _ => false, }) }) .map(|p| match p { RichPattern::Token(t) => token_to_string(t), RichPattern::EndOfInput => "end of input".to_string(), _ => format!("{:?}", p), }) .collect(); let found_str = match found { Some(t) => token_to_string(t), None => "end of input".to_string(), }; if expected_strs.is_empty() || expected_strs.len() > 10 { Error::new_simple(format!("unexpected {found_str}")) } else { let mut expected_strs = expected_strs; expected_strs.sort(); let expected_str = match expected_strs.len() { 1 => expected_strs[0].clone(), 2 => expected_strs.join(" or "), _ => { let last = expected_strs.pop().unwrap(); format!("one of {} or {last}", expected_strs.join(", ")) } }; match found { Some(_) => Error::new(Reason::Expected { who: None, expected: expected_str, found: found_str, }), None => Error::new(Reason::Simple(format!( "Expected {expected_str}, but didn't find anything before the end." ))), } } } RichReason::Custom(msg) => Error::new_simple(msg.to_string()), }; error.with_span(Some(span)) } impl<'a> From> for Error { fn from(rich: Rich<'a, crate::lexer::lr::Token, Span>) -> Error { rich_error_to_error( *rich.span(), rich.reason(), |token| format!("{}", token.kind), |token| matches!(token.kind, TokenKind::NewLine | TokenKind::Start), ) } } impl<'a> From> for Error { fn from(rich: Rich<'a, TokenKind, Span>) -> Error { rich_error_to_error( *rich.span(), rich.reason(), |kind| format!("{}", kind), |kind| matches!(kind, TokenKind::NewLine | TokenKind::Start), ) } } #[cfg(test)] mod tests { use insta::{assert_debug_snapshot, assert_snapshot}; use crate::error::{Error, WithErrorInfo}; // Helper function to create a simple Error object fn simple_error(message: &str) -> Error { Error::new_simple(message) } #[test] fn test_error_messages() { let error1 = simple_error("test error"); assert_snapshot!(error1.to_string(), @r#"Error { kind: Error, span: None, reason: Simple("test error"), hints: [], code: None }"#); let error2 = simple_error("another error").with_span(Some(crate::span::Span { start: 0, end: 5, source_id: 0, })); assert_debug_snapshot!(error2, @r#" Error { kind: Error, span: Some( 0:0-5, ), reason: Simple( "another error", ), hints: [], code: None, } "#); } } ================================================ FILE: prqlc/prqlc-parser/src/parser/pr/expr.rs ================================================ use std::collections::HashMap; use enum_as_inner::EnumAsInner; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use crate::lexer::lr::Literal; use crate::parser::pr::ops::{BinOp, UnOp}; use crate::parser::pr::{Ident, Ty}; use crate::span::Span; use crate::{generic, parser::SupportsDocComment}; impl Expr { pub fn new>(kind: K) -> Self { Expr { kind: kind.into(), span: None, alias: None, doc_comment: None, } } } // The following code is tested by the tests_misc crate to match expr.rs in prqlc. /// Expr is anything that has a value and thus a type. /// Most of these can contain other [Expr] themselves; literals should be [ExprKind::Literal]. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct Expr { #[serde(flatten)] pub kind: ExprKind, #[serde(skip_serializing_if = "Option::is_none")] pub span: Option, #[serde(skip_serializing_if = "Option::is_none")] pub alias: Option, #[serde(skip_serializing_if = "Option::is_none")] pub doc_comment: Option, } impl SupportsDocComment for Expr { fn with_doc_comment(self, doc_comment: Option) -> Self { Self { doc_comment, ..self } } } #[derive( Debug, EnumAsInner, PartialEq, Clone, Serialize, Deserialize, strum::AsRefStr, JsonSchema, )] pub enum ExprKind { Ident(Ident), #[cfg_attr( feature = "serde_yaml", serde(with = "serde_yaml::with::singleton_map"), schemars(with = "Literal") )] Literal(Literal), Pipeline(Pipeline), Tuple(Vec), Array(Vec), Range(Range), Binary(BinaryExpr), Unary(UnaryExpr), FuncCall(FuncCall), Func(Box), SString(Vec), FString(Vec), Case(Vec), /// placeholder for values provided after query is compiled Param(String), /// When used instead of function body, the function will be translated to a RQ operator. /// Contains ident of the RQ operator. Internal(String), } impl ExprKind { pub fn into_expr(self, span: Span) -> Expr { Expr { span: Some(span), kind: self, alias: None, doc_comment: None, } } } #[derive(Debug, EnumAsInner, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub enum IndirectionKind { Name(String), Position(i64), Star, } /// Expression with two operands and an operator, such as `1 + 2`. #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct BinaryExpr { pub left: Box, pub op: BinOp, pub right: Box, } /// Expression with one operand and an operator, such as `-1`. #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct UnaryExpr { pub op: UnOp, pub expr: Box, } /// Function call. #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct FuncCall { pub name: Box, pub args: Vec, #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub named_args: HashMap, } /// Function called with possibly missing positional arguments. /// May also contain environment that is needed to evaluate the body. #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct Func { /// Type requirement for the function body expression. pub return_ty: Option, /// Expression containing parameter (and environment) references. pub body: Box, /// Positional function parameters. pub params: Vec, /// Named function parameters. pub named_params: Vec, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct FuncParam { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub ty: Option, pub default_value: Option>, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct GenericTypeParam { /// Assigned name of this generic type argument. pub name: String, pub domain: Vec, } /// A value and a series of functions that are to be applied to that value one after another. #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct Pipeline { pub exprs: Vec, } pub type Range = generic::Range>; pub type InterpolateItem = generic::InterpolateItem; pub type SwitchCase = generic::SwitchCase>; impl From for ExprKind { fn from(value: Literal) -> Self { ExprKind::Literal(value) } } impl From for ExprKind { fn from(value: Func) -> Self { ExprKind::Func(Box::new(value)) } } ================================================ FILE: prqlc/prqlc-parser/src/parser/pr/ident.rs ================================================ use std::fmt::Write; use schemars::JsonSchema; use serde::{ser::SerializeSeq, Deserialize, Deserializer, Serialize, Serializer}; /// A name. Generally columns, tables, functions, variables. /// This is glorified way of writing a "vec with at least one element". #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, JsonSchema)] pub struct Ident { pub path: Vec, pub name: String, } impl Ident { pub fn from_name(name: S) -> Self { Ident { path: Vec::new(), name: name.to_string(), } } /// Creates a new ident from a non-empty path. /// /// Panics if path is empty. pub fn from_path(mut path: Vec) -> Self { let name = path.pop().unwrap().to_string(); Ident { path: path.into_iter().map(|x| x.to_string()).collect(), name, } } pub fn len(&self) -> usize { self.path.len() + 1 } pub fn is_empty(&self) -> bool { false } /// Remove last part of the ident. /// Result will generally refer to the parent of this ident. pub fn pop(self) -> Option { let mut path = self.path; path.pop().map(|name| Ident { path, name }) } pub fn pop_front(mut self) -> (String, Option) { if self.path.is_empty() { (self.name, None) } else { let first = self.path.remove(0); (first, Some(self)) } } pub fn prepend(self, mut parts: Vec) -> Ident { parts.extend(self); Ident::from_path(parts) } pub fn push(&mut self, name: String) { self.path.push(std::mem::take(&mut self.name)); self.name = name; } pub fn with_name(mut self, name: S) -> Self { self.name = name.to_string(); self } pub fn iter(&self) -> impl Iterator { self.path.iter().chain(std::iter::once(&self.name)) } pub fn starts_with(&self, prefix: &Ident) -> bool { if prefix.len() > self.len() { return false; } prefix .iter() .zip(self.iter()) .all(|(prefix_component, self_component)| prefix_component == self_component) } pub fn starts_with_path>(&self, prefix: &[S]) -> bool { // self is an I if prefix.len() > self.len() { return false; } prefix .iter() .zip(self.iter()) .all(|(prefix_component, self_component)| prefix_component.as_ref() == self_component) } pub fn starts_with_part(&self, prefix: &str) -> bool { self.starts_with_path(&[prefix]) } } impl std::fmt::Debug for Ident { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_list() .entries(&self.path) .entry(&self.name) .finish() } } impl std::fmt::Display for Ident { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { display_ident(f, self) } } impl IntoIterator for Ident { type Item = String; type IntoIter = std::iter::Chain< std::vec::IntoIter, std::option::IntoIter, >; fn into_iter(self) -> Self::IntoIter { self.path.into_iter().chain(Some(self.name)) } } impl std::ops::Add for Ident { type Output = Ident; fn add(self, rhs: Ident) -> Self::Output { Ident { path: self.into_iter().chain(rhs.path).collect(), name: rhs.name, } } } impl Serialize for Ident { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut seq = serializer.serialize_seq(Some(self.len()))?; for part in &self.path { seq.serialize_element(part)?; } seq.serialize_element(&self.name)?; seq.end() } } impl<'de> Deserialize<'de> for Ident { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { as Deserialize>::deserialize(deserializer).map(Ident::from_path) } } pub fn display_ident(f: &mut std::fmt::Formatter, ident: &Ident) -> Result<(), std::fmt::Error> { let path = &ident.path[..]; for part in path { display_ident_part(f, part)?; f.write_char('.')?; } display_ident_part(f, &ident.name)?; Ok(()) } pub fn display_ident_part(f: &mut std::fmt::Formatter, s: &str) -> Result<(), std::fmt::Error> { fn forbidden_start(c: char) -> bool { !(c.is_ascii_alphabetic() || matches!(c, '_' | '$')) } fn forbidden_subsequent(c: char) -> bool { !(c.is_ascii_alphabetic() || c.is_ascii_digit() || c == '_') } let needs_escape = s.is_empty() || s.starts_with(forbidden_start) || (s.len() > 1 && s.chars().skip(1).any(forbidden_subsequent)); if needs_escape { write!(f, "`{s}`") } else { write!(f, "{s}") } } ================================================ FILE: prqlc/prqlc-parser/src/parser/pr/mod.rs ================================================ //! PR, or "Parser Representation" is an AST representation of parsed PRQL. It //! takes LR tokens and converts them into a more structured form which //! understands expressions, such as tuples & functions. pub use expr::*; pub use ident::*; pub use ops::*; pub use stmt::*; pub use types::*; // re-export Literal from LR, since it's encapsulated in TyKind pub use crate::lexer::lr::Literal; pub use crate::span::Span; mod expr; mod ident; mod ops; mod stmt; mod types; ================================================ FILE: prqlc/prqlc-parser/src/parser/pr/ops.rs ================================================ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[derive( Debug, PartialEq, Eq, Clone, Copy, Hash, Serialize, Deserialize, strum::Display, strum::EnumString, JsonSchema, )] pub enum UnOp { #[strum(to_string = "-")] Neg, #[strum(to_string = "+")] Add, // TODO: rename to Pos #[strum(to_string = "!")] Not, #[strum(to_string = "==")] EqSelf, } #[derive( Debug, PartialEq, Eq, Clone, Copy, Hash, Serialize, Deserialize, strum::Display, strum::EnumString, JsonSchema, )] pub enum BinOp { #[strum(to_string = "*")] Mul, #[strum(to_string = "//")] DivInt, #[strum(to_string = "/")] DivFloat, #[strum(to_string = "%")] Mod, #[strum(to_string = "**")] Pow, #[strum(to_string = "+")] Add, #[strum(to_string = "-")] Sub, #[strum(to_string = "==")] Eq, #[strum(to_string = "!=")] Ne, #[strum(to_string = ">")] Gt, #[strum(to_string = "<")] Lt, #[strum(to_string = ">=")] Gte, #[strum(to_string = "<=")] Lte, #[strum(to_string = "~=")] RegexSearch, #[strum(to_string = "&&")] And, #[strum(to_string = "||")] Or, #[strum(to_string = "??")] Coalesce, } ================================================ FILE: prqlc/prqlc-parser/src/parser/pr/stmt.rs ================================================ use std::collections::HashMap; use enum_as_inner::EnumAsInner; use schemars::JsonSchema; use semver::VersionReq; use serde::{Deserialize, Serialize}; use crate::parser::pr::ident::Ident; use crate::parser::pr::{Expr, Ty}; use crate::parser::SupportsDocComment; use crate::span::Span; #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, Default, JsonSchema)] pub struct QueryDef { #[schemars(with = "String")] pub version: Option, #[serde(default)] pub other: HashMap, } #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)] pub enum VarDefKind { Let, Into, Main, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct Stmt { #[serde(flatten)] pub kind: StmtKind, #[serde(skip_serializing_if = "Option::is_none")] pub span: Option, #[serde(skip_serializing_if = "Vec::is_empty", default)] pub annotations: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub doc_comment: Option, } impl SupportsDocComment for Stmt { fn with_doc_comment(self, doc_comment: Option) -> Self { Stmt { doc_comment, ..self } } } #[derive(Debug, EnumAsInner, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub enum StmtKind { QueryDef(Box), VarDef(VarDef), TypeDef(TypeDef), ModuleDef(ModuleDef), ImportDef(ImportDef), } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct VarDef { pub kind: VarDefKind, pub name: String, pub value: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub ty: Option, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct TypeDef { pub name: String, pub value: Ty, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct ModuleDef { pub name: String, pub stmts: Vec, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)] pub struct ImportDef { pub alias: Option, pub name: Ident, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct Annotation { pub expr: Box, } impl Stmt { pub fn new(kind: StmtKind) -> Stmt { Stmt { kind, span: None, annotations: Vec::new(), doc_comment: None, } } } ================================================ FILE: prqlc/prqlc-parser/src/parser/pr/types.rs ================================================ use enum_as_inner::EnumAsInner; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use strum::AsRefStr; use crate::parser::pr::ident::Ident; use crate::span::Span; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct Ty { pub kind: TyKind, pub span: Option, /// Name inferred from the type declaration. pub name: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, EnumAsInner, AsRefStr, JsonSchema)] pub enum TyKind { /// Identifier that still needs to be resolved. Ident(Ident), /// Type of a built-in primitive type Primitive(PrimitiveSet), /// Type of tuples (product) Tuple(Vec), /// Type of arrays Array(Option>), /// Type of functions with defined params and return types. Function(Option), } impl TyKind { pub fn into_ty(self: TyKind, span: Span) -> Ty { Ty { kind: self, span: Some(span), name: None, } } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, EnumAsInner, JsonSchema)] pub enum TyTupleField { /// Named tuple element. Single(Option, Option), /// Placeholder for possibly many elements. /// Means "and other unmentioned columns". Does not mean "all columns". Wildcard(Option), } /// Built-in sets. #[derive( Debug, Clone, Serialize, Deserialize, PartialEq, Eq, strum::EnumString, strum::Display, JsonSchema, )] pub enum PrimitiveSet { #[strum(to_string = "int")] Int, #[strum(to_string = "float")] Float, #[strum(to_string = "bool")] Bool, #[strum(to_string = "text")] Text, #[strum(to_string = "date")] Date, #[strum(to_string = "time")] Time, #[strum(to_string = "timestamp")] Timestamp, } // Type of a function #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct TyFunc { pub name_hint: Option, pub params: Vec>, pub return_ty: Option>, } impl Ty { pub fn new>(kind: K) -> Ty { Ty { kind: kind.into(), span: None, name: None, } } pub fn relation(tuple_fields: Vec) -> Self { let tuple = Ty::new(TyKind::Tuple(tuple_fields)); Ty::new(TyKind::Array(Some(Box::new(tuple)))) } pub fn as_relation(&self) -> Option<&Vec> { self.kind.as_array()?.as_ref()?.kind.as_tuple() } pub fn as_relation_mut(&mut self) -> Option<&mut Vec> { self.kind.as_array_mut()?.as_mut()?.kind.as_tuple_mut() } pub fn into_relation(self) -> Option> { self.kind.into_array().ok()??.kind.into_tuple().ok() } pub fn is_relation(&self) -> bool { match &self.kind { TyKind::Array(Some(elem)) => { matches!(elem.kind, TyKind::Tuple(_)) } _ => false, } } } impl TyTupleField { pub fn ty(&self) -> Option<&Ty> { match self { TyTupleField::Single(_, ty) => ty.as_ref(), TyTupleField::Wildcard(ty) => ty.as_ref(), } } } impl From for TyKind { fn from(value: PrimitiveSet) -> Self { TyKind::Primitive(value) } } impl From for TyKind { fn from(value: TyFunc) -> Self { TyKind::Function(Some(value)) } } ================================================ FILE: prqlc/prqlc-parser/src/parser/stmt.rs ================================================ use std::collections::HashMap; use chumsky; use chumsky::input::BorrowInput; use chumsky::prelude::*; use itertools::Itertools; use semver::VersionReq; use super::expr::{expr, expr_call, ident, pipeline}; use super::{ctrl, ident_part, into_stmt, keyword, new_line, pipe, with_doc_comment}; use crate::lexer::lr; use crate::lexer::lr::{Literal, TokenKind}; use crate::parser::pr::*; use crate::parser::types::type_expr; use crate::span::Span; use super::ParserError; /// The top-level parser for a PRQL file pub fn source<'a, I>() -> impl Parser<'a, I, Vec, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a> + chumsky::input::ValueInput<'a>, { with_doc_comment(query_def()) .or_not() .map(|opt| opt.into_iter().collect::>()) .then(module_contents()) .map(|(mut first, mut second)| { first.append(&mut second); first }) // This is the only instance we can consume newlines at the end of something, since // this is the end of the file .then_ignore(new_line().repeated().collect::>()) .then_ignore(end()) .boxed() } fn module_contents<'a, I>() -> impl Parser<'a, I, Vec, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a> + chumsky::input::ValueInput<'a>, { recursive(|module_contents| { let module_def = keyword("module") .ignore_then(ident_part()) .then( module_contents .then_ignore(new_line().repeated().collect::>()) .delimited_by(ctrl('{'), ctrl('}')), ) .map(|(name, stmts)| StmtKind::ModuleDef(ModuleDef { name, stmts })) .labelled("module definition"); let annotation = new_line() .repeated() .at_least(1) .collect::>() .ignore_then( select_ref! { lr::Token { kind: TokenKind::Annotate, .. } => () } .ignore_then(expr()) .map(|expr| Annotation { expr: Box::new(expr), }), ) .labelled("annotation"); // TODO: we want to confirm that we're not allowing things on the same // line that should't be; e.g. `let foo = 5 let bar = 6`. We can't // enforce a new line here because then `module two {let houses = // both.alike}` fails (though we could force a new line after the // `module` if we wanted to?) // // let stmt_kind = new_line().repeated().at_least(1).ignore_then(choice(( let stmt_kind = new_line() .repeated() .collect::>() .ignore_then(choice((module_def, type_def(), import_def(), var_def()))); // Currently doc comments need to be before the annotation; probably // should relax this? with_doc_comment( annotation .repeated() .collect::>() .then(stmt_kind) .map_with(|(annotations, kind), extra| { into_stmt((annotations, kind), extra.span()) }), ) .repeated() .collect() }) .boxed() } fn query_def<'a, I>() -> impl Parser<'a, I, Stmt, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a> + chumsky::input::ValueInput<'a>, { new_line() .repeated() .at_least(1) .collect::>() .ignore_then(keyword("prql")) .ignore_then( // named arg ident_part() .then_ignore(ctrl(':')) .then(expr()) .repeated() .collect::>(), ) .then_ignore(new_line()) .validate(|args, extra, emit| { let span = extra.span(); let mut args: HashMap<_, _> = args.into_iter().collect(); let version = args.remove("version").and_then(|v| match v.kind { ExprKind::Literal(Literal::String(v)) => match VersionReq::parse(&v) { Ok(ver) => Some(ver), Err(e) => { emit.emit(Rich::custom(span, e.to_string())); None } }, _ => { emit.emit(Rich::custom(span, "version must be a string literal")); None } }); // TODO: `QueryDef` is currently implemented as `version` & `other` // fields. We want to raise an error if an unsupported field is // used, to avoid confusion (e.g. if someone passes `dialect`). So // at the moment we implement this as having a HashMap with 0 or 1 // entries... We can decide how to implement `QueryDef` later, and // have this awkward construction in the meantime. let other = args .remove("target") .and_then(|v| { if let ExprKind::Ident(name) = v.kind { Some(name.to_string()) } else { emit.emit(Rich::custom(span, "target must be a string literal")); None } }) .map_or_else(HashMap::new, |x| { HashMap::from_iter(vec![("target".to_string(), x)]) }); if !args.is_empty() { emit.emit(Rich::custom( span, format!( "unknown query definition arguments {}", args.keys().map(|x| format!("`{x}`")).join(", ") ), )); } StmtKind::QueryDef(Box::new(QueryDef { version, other })) }) .map(|kind| (Vec::new(), kind)) .map_with(|(annotations, kind), extra| into_stmt((annotations, kind), extra.span())) .labelled("query header") .boxed() } /// A variable definition could be any of: /// - `let foo = 5` /// - `from artists` — captured as a "main" /// - `from artists | into x` — captured as an "into"` fn var_def<'a, I>() -> impl Parser<'a, I, StmtKind, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a> + chumsky::input::ValueInput<'a>, { let let_ = new_line() .repeated() .collect::>() .ignore_then(keyword("let")) .ignore_then(ident_part()) .then(type_expr().delimited_by(ctrl('<'), ctrl('>')).or_not()) .then(ctrl('=').ignore_then(expr_call()).map(Box::new).or_not()) .map(|((name, ty), value)| { StmtKind::VarDef(VarDef { name, value, ty, kind: VarDefKind::Let, }) }) .labelled("variable definition"); let main_or_into = new_line() .repeated() .collect::>() .ignore_then(pipeline(expr_call())) .map(Box::new) .then( pipe() .ignore_then(keyword("into").ignore_then(ident_part())) .or_not(), ) .map(|(value, name)| { let kind = if name.is_none() { VarDefKind::Main } else { VarDefKind::Into }; let name = name.unwrap_or_else(|| "main".to_string()); StmtKind::VarDef(VarDef { name, kind, value: Some(value), ty: None, }) }); let_.or(main_or_into).boxed() } fn type_def<'a, I>() -> impl Parser<'a, I, StmtKind, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a> + chumsky::input::ValueInput<'a>, { keyword("type") .ignore_then(ident_part()) .then(ctrl('=').ignore_then(type_expr())) .map(|(name, value)| StmtKind::TypeDef(TypeDef { name, value })) .labelled("type definition") } fn import_def<'a, I>() -> impl Parser<'a, I, StmtKind, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a> + chumsky::input::ValueInput<'a>, { keyword("import") .ignore_then(ident_part().then_ignore(ctrl('=')).or_not()) .then(ident()) .map(|(alias, name)| StmtKind::ImportDef(ImportDef { name, alias })) .labelled("import statement") } #[cfg(test)] mod tests { use chumsky::prelude::*; use insta::{assert_debug_snapshot, assert_yaml_snapshot}; use super::*; use crate::error::Error; fn parse_module_contents(source: &str) -> Result, Vec> { crate::parse_test!( source, module_contents() .then_ignore(new_line().repeated()) .then_ignore(end()) ) } fn parse_var_def(source: &str) -> Result> { crate::parse_test!( source, var_def() .then_ignore(new_line().repeated()) .then_ignore(end()) ) } fn parse_module_contents_complete(source: &str) -> Result, Vec> { crate::parse_test!(source, module_contents().then_ignore(end())) } #[test] fn test_module_contents() { assert_yaml_snapshot!(parse_module_contents(r#" let world = 1 let man = module.world "#).unwrap(), @r#" - VarDef: kind: Let name: world value: Literal: Integer: 1 span: "0:25-26" span: "0:0-26" - VarDef: kind: Let name: man value: Ident: - module - world span: "0:49-61" span: "0:26-61" "#); } #[test] fn into() { assert_yaml_snapshot!(parse_var_def(r#" from artists into x "#).unwrap(), @r#" VarDef: kind: Into name: x value: FuncCall: name: Ident: - from span: "0:13-17" args: - Ident: - artists span: "0:18-25" span: "0:13-25" "#); assert_yaml_snapshot!(parse_var_def(r#" from artists | into x "#).unwrap(), @r#" VarDef: kind: Into name: x value: FuncCall: name: Ident: - from span: "0:13-17" args: - Ident: - artists span: "0:18-25" span: "0:13-25" "#); } #[test] fn let_into() { assert_debug_snapshot!(parse_module_contents_complete(r#" let y = ( from artists into x ) "#).unwrap_err(), @r#" [ Error { kind: Error, span: Some( 0:56-60, ), reason: Expected { who: None, expected: "one of doc comment, function call, function definition, new line or something else", found: "keyword into", }, hints: [], code: None, }, Error { kind: Error, span: Some( 0:0-73, ), reason: Simple( "Expected one of import statement, module definition, new line, pipeline, something else, type definition or variable definition, but didn't find anything before the end.", ), hints: [], code: None, }, ] "#); } #[test] fn test_module() { let parse_module = |s| parse_module_contents(s).unwrap(); let module_ast = parse_module( r#" module hello { let world = 1 let man = module.world } "#, ); assert_yaml_snapshot!(module_ast, @r#" - ModuleDef: name: hello stmts: - VarDef: kind: Let name: world value: Literal: Integer: 1 span: "0:50-51" span: "0:25-51" - VarDef: kind: Let name: man value: Ident: - module - world span: "0:74-86" span: "0:51-86" span: "0:0-98" "#); // Check this parses OK. (We tried comparing it to the AST of the result // above, but the span information was different, so we just check it. // Would be nice to be able to strip spans...) parse_module( r#" module hello { let world = 1 let man = module.world } "#, ); } #[test] fn test_module_def() { // Same line assert_yaml_snapshot!(parse_module_contents(r#"module two {let houses = both.alike} "#).unwrap(), @r#" - ModuleDef: name: two stmts: - VarDef: kind: Let name: houses value: Ident: - both - alike span: "0:25-35" span: "0:12-35" span: "0:0-36" "#); assert_yaml_snapshot!(parse_module_contents(r#" module dignity { let fair = 1 let verona = we.lay } "#).unwrap(), @r#" - ModuleDef: name: dignity stmts: - VarDef: kind: Let name: fair value: Literal: Integer: 1 span: "0:51-52" span: "0:27-52" - VarDef: kind: Let name: verona value: Ident: - we - lay span: "0:78-84" span: "0:52-84" span: "0:0-95" "#); } #[test] fn doc_comment_module() { assert_yaml_snapshot!(parse_module_contents(r#" #! first doc comment from foo "#).unwrap(), @r#" - VarDef: kind: Main name: main value: FuncCall: name: Ident: - from span: "0:39-43" args: - Ident: - foo span: "0:44-47" span: "0:39-47" span: "0:30-47" doc_comment: " first doc comment" "#); assert_yaml_snapshot!(parse_module_contents(r#" #! first doc comment from foo into x #! second doc comment from bar "#).unwrap(), @r#" - VarDef: kind: Into name: x value: FuncCall: name: Ident: - from span: "0:40-44" args: - Ident: - foo span: "0:45-48" span: "0:40-48" span: "0:31-63" doc_comment: " first doc comment" - VarDef: kind: Main name: main value: FuncCall: name: Ident: - from span: "0:103-107" args: - Ident: - bar span: "0:108-111" span: "0:103-111" span: "0:94-111" doc_comment: " second doc comment" "#); } #[test] fn doc_comment_inline_module() { // Check the newline doesn't get eated by the `{}` of the module // TODO: could give a better error when we forget the module name assert_yaml_snapshot!(parse_module_contents(r#" module bar { #! first doc comment from foo } "#).unwrap(), @r#" - ModuleDef: name: bar stmts: - VarDef: kind: Main name: main value: FuncCall: name: Ident: - from span: "0:63-67" args: - Ident: - foo span: "0:68-71" span: "0:63-71" span: "0:52-71" doc_comment: " first doc comment" span: "0:0-81" "#); } #[test] fn lambdas() { assert_yaml_snapshot!(parse_module_contents(r#" let first = column -> internal std.first "#).unwrap(), @r#" - VarDef: kind: Let name: first value: Func: return_ty: ~ body: Internal: std.first span: "0:39-57" params: - name: column ty: kind: Ident: - array span: "0:29-34" name: ~ default_value: ~ named_params: [] span: "0:21-57" span: "0:0-57" "#); assert_yaml_snapshot!(parse_module_contents(r#" module defs { let first = column -> internal std.first let last = column -> internal std.last } "#).unwrap(), @r#" - ModuleDef: name: defs stmts: - VarDef: kind: Let name: first value: Func: return_ty: ~ body: Internal: std.first span: "0:59-77" params: - name: column ty: kind: Ident: - array span: "0:49-54" name: ~ default_value: ~ named_params: [] span: "0:41-77" span: "0:20-77" - VarDef: kind: Let name: last value: Func: return_ty: ~ body: Internal: std.last span: "0:116-133" params: - name: column ty: kind: Ident: - array span: "0:106-111" name: ~ default_value: ~ named_params: [] span: "0:98-133" span: "0:77-133" span: "0:0-139" "#); } } ================================================ FILE: prqlc/prqlc-parser/src/parser/test.rs ================================================ use insta::assert_yaml_snapshot; use super::pr::{Expr, FuncCall}; use crate::error::Error; /// Macro to eliminate test helper boilerplate. /// Converts source code to the parsed input format that our parsers expect. #[macro_export] macro_rules! parse_test { ($source:expr, $parser:expr) => {{ #[allow(unused_imports)] use chumsky::input::Input as _; #[allow(unused_imports)] use chumsky::IterParser as _; #[allow(unused_imports)] use chumsky::Parser as _; let tokens = $crate::lexer::lex_source($source)?; let semantic_tokens: Vec<_> = tokens .0 .into_iter() .filter(|token| { !matches!( token.kind, $crate::lexer::lr::TokenKind::Comment(_) | $crate::lexer::lr::TokenKind::LineWrap(_) ) }) .collect(); let input = semantic_tokens .as_slice() .map_span(|simple_span: chumsky::span::SimpleSpan| { let start_idx = simple_span.start; let end_idx = simple_span.end; let start = semantic_tokens .get(start_idx) .map(|t| t.span.start) .unwrap_or(0); let end = semantic_tokens .get(end_idx.saturating_sub(1)) .map(|t| t.span.end) .unwrap_or(start); $crate::span::Span { start, end, source_id: 0, } }); let (ast, errors) = $parser.parse(input).into_output_errors(); if !errors.is_empty() { return Err(errors.into_iter().map(Into::into).collect()); } Ok(ast.unwrap()) }}; } fn parse_expr(source: &str) -> Result> { parse_test!( source, super::new_line() .repeated() .collect::>() .ignore_then(super::expr::expr_call()) ) } #[test] fn test_ranges() { assert_yaml_snapshot!(parse_expr(r#"3..5"#).unwrap(), @r#" Range: start: Literal: Integer: 3 span: "0:0-1" end: Literal: Integer: 5 span: "0:3-4" span: "0:0-4" "#); assert_yaml_snapshot!(parse_expr(r#"-2..-5"#).unwrap(), @r#" Range: start: Unary: op: Neg expr: Literal: Integer: 2 span: "0:1-2" span: "0:0-2" end: Unary: op: Neg expr: Literal: Integer: 5 span: "0:5-6" span: "0:4-6" span: "0:0-6" "#); assert_yaml_snapshot!(parse_expr(r#"(-2..(-5 | abs))"#).unwrap(), @r#" Range: start: Unary: op: Neg expr: Literal: Integer: 2 span: "0:2-3" span: "0:1-3" end: Pipeline: exprs: - Unary: op: Neg expr: Literal: Integer: 5 span: "0:7-8" span: "0:6-8" - Ident: - abs span: "0:11-14" span: "0:6-14" span: "0:0-16" "#); assert_yaml_snapshot!(parse_expr(r#"(2 + 5)..'a'"#).unwrap(), @r#" Range: start: Binary: left: Literal: Integer: 2 span: "0:1-2" op: Add right: Literal: Integer: 5 span: "0:5-6" span: "0:1-6" end: Literal: String: a span: "0:9-12" span: "0:0-12" "#); assert_yaml_snapshot!(parse_expr(r#"1.6..rel.col"#).unwrap(), @r#" Range: start: Literal: Float: 1.6 span: "0:0-3" end: Ident: - rel - col span: "0:5-12" span: "0:0-12" "#); assert_yaml_snapshot!(parse_expr(r#"6.."#).unwrap(), @r#" Range: start: Literal: Integer: 6 span: "0:0-1" end: ~ span: "0:0-3" "#); assert_yaml_snapshot!(parse_expr(r#"..7"#).unwrap(), @r#" Range: start: ~ end: Literal: Integer: 7 span: "0:2-3" span: "0:0-3" "#); assert_yaml_snapshot!(parse_expr(r#".."#).unwrap(), @r#" Range: start: ~ end: ~ span: "0:0-2" "#); assert_yaml_snapshot!(parse_expr(r#"@2020-01-01..@2021-01-01"#).unwrap(), @r#" Range: start: Literal: Date: 2020-01-01 span: "0:0-11" end: Literal: Date: 2021-01-01 span: "0:13-24" span: "0:0-24" "#); } #[test] fn test_basic_exprs() { assert_yaml_snapshot!(parse_expr(r#"country == "USA""#).unwrap(), @r#" Binary: left: Ident: - country span: "0:0-7" op: Eq right: Literal: String: USA span: "0:11-16" span: "0:0-16" "#); assert_yaml_snapshot!(parse_expr("select {a, b, c}").unwrap(), @r#" FuncCall: name: Ident: - select span: "0:0-6" args: - Tuple: - Ident: - a span: "0:8-9" - Ident: - b span: "0:11-12" - Ident: - c span: "0:14-15" span: "0:7-16" span: "0:0-16" "#); assert_yaml_snapshot!(parse_expr( "group {title, country} ( aggregate {sum salary} )" ).unwrap(), @r#" FuncCall: name: Ident: - group span: "0:0-5" args: - Tuple: - Ident: - title span: "0:7-12" - Ident: - country span: "0:14-21" span: "0:6-22" - FuncCall: name: Ident: - aggregate span: "0:41-50" args: - Tuple: - FuncCall: name: Ident: - sum span: "0:52-55" args: - Ident: - salary span: "0:56-62" span: "0:52-62" span: "0:51-63" span: "0:41-63" span: "0:0-77" "#); assert_yaml_snapshot!(parse_expr( r#" filter country == "USA""# ).unwrap(), @r#" FuncCall: name: Ident: - filter span: "0:4-10" args: - Binary: left: Ident: - country span: "0:11-18" op: Eq right: Literal: String: USA span: "0:22-27" span: "0:11-27" span: "0:4-27" "#); assert_yaml_snapshot!(parse_expr("{a, b, c,}").unwrap(), @r#" Tuple: - Ident: - a span: "0:1-2" - Ident: - b span: "0:4-5" - Ident: - c span: "0:7-8" span: "0:0-10" "#); assert_yaml_snapshot!(parse_expr( r#"{ gross_salary = salary + payroll_tax, gross_cost = gross_salary + benefits_cost }"# ).unwrap(), @r#" Tuple: - Binary: left: Ident: - salary span: "0:19-25" op: Add right: Ident: - payroll_tax span: "0:28-39" span: "0:19-39" alias: gross_salary - Binary: left: Ident: - gross_salary span: "0:58-70" op: Add right: Ident: - benefits_cost span: "0:73-86" span: "0:58-86" alias: gross_cost span: "0:0-88" "#); assert_yaml_snapshot!(parse_expr( "join side:left country (id==employee_id)" ).unwrap(), @r#" FuncCall: name: Ident: - join span: "0:0-4" args: - Ident: - country span: "0:15-22" - Binary: left: Ident: - id span: "0:24-26" op: Eq right: Ident: - employee_id span: "0:28-39" span: "0:24-39" named_args: side: Ident: - left span: "0:10-14" span: "0:0-40" "#); assert_yaml_snapshot!(parse_expr("1 + 2").unwrap(), @r#" Binary: left: Literal: Integer: 1 span: "0:0-1" op: Add right: Literal: Integer: 2 span: "0:5-6" span: "0:0-6" "#) } #[test] fn test_string() { let double_quoted_ast = parse_expr(r#"" U S A ""#).unwrap(); assert_yaml_snapshot!(double_quoted_ast, @r#" Literal: String: " U S A " span: "0:0-9" "#); let single_quoted_ast = parse_expr(r#"' U S A '"#).unwrap(); assert_eq!(single_quoted_ast, double_quoted_ast); // Single quotes within double quotes should produce a string containing // the single quotes (and vice versa). assert_yaml_snapshot!(parse_expr(r#""' U S A '""#).unwrap(), @r#" Literal: String: "' U S A '" span: "0:0-11" "#); assert_yaml_snapshot!(parse_expr(r#"'" U S A "'"#).unwrap(), @r#" Literal: String: "\" U S A \"" span: "0:0-11" "#); parse_expr(r#"" U S A"#).unwrap_err(); parse_expr(r#"" U S A '"#).unwrap_err(); assert_yaml_snapshot!(parse_expr(r#"" \nU S A ""#).unwrap(), @r#" Literal: String: " \nU S A " span: "0:0-11" "#); assert_yaml_snapshot!(parse_expr(r#"r" \nU S A ""#).unwrap(), @r#" Literal: RawString: " \\nU S A " span: "0:0-12" "#); let multi_double = parse_expr( r#"""" '' Canada " """"#, ) .unwrap(); assert_yaml_snapshot!(multi_double, @r#" Literal: String: "\n''\nCanada\n\"\n\n" span: "0:0-20" "#); let multi_single = parse_expr( r#"''' Canada " """ '''"#, ) .unwrap(); assert_yaml_snapshot!(multi_single, @r#" Literal: String: "\nCanada\n\"\n\"\"\"\n\n" span: "0:0-21" "#); assert_yaml_snapshot!( parse_expr("''").unwrap(), @r#" Literal: String: "" span: "0:0-2" "#); } #[test] fn test_s_string() { assert_yaml_snapshot!(parse_expr(r#"s"SUM({col})""#).unwrap(), @r#" SString: - String: SUM( - Expr: expr: Ident: - col span: "0:7-10" format: ~ - String: ) span: "0:0-13" "#); assert_yaml_snapshot!(parse_expr(r#"s"SUM({rel.`Col name`})""#).unwrap(), @r#" SString: - String: SUM( - Expr: expr: Ident: - rel - Col name span: "0:7-21" format: ~ - String: ) span: "0:0-24" "#) } #[test] fn test_s_string_braces() { assert_yaml_snapshot!(parse_expr(r#"s"{{?crystal_var}}""#).unwrap(), @r#" SString: - String: "{?crystal_var}" span: "0:0-19" "#); assert_yaml_snapshot!(parse_expr(r#"s"foo{{bar""#).unwrap(), @r#" SString: - String: "foo{bar" span: "0:0-11" "#); parse_expr(r#"s"foo{{bar}""#).unwrap_err(); } #[test] fn test_tuple() { assert_yaml_snapshot!(parse_expr(r#"{1 + 1, 2}"#).unwrap(), @r#" Tuple: - Binary: left: Literal: Integer: 1 span: "0:1-2" op: Add right: Literal: Integer: 1 span: "0:5-6" span: "0:1-6" - Literal: Integer: 2 span: "0:8-9" span: "0:0-10" "#); assert_yaml_snapshot!(parse_expr(r#"{1 + (f 1), 2}"#).unwrap(), @r#" Tuple: - Binary: left: Literal: Integer: 1 span: "0:1-2" op: Add right: FuncCall: name: Ident: - f span: "0:6-7" args: - Literal: Integer: 1 span: "0:8-9" span: "0:6-9" span: "0:1-10" - Literal: Integer: 2 span: "0:12-13" span: "0:0-14" "#); // Line breaks assert_yaml_snapshot!(parse_expr( r#"{1, 2}"# ).unwrap(), @r#" Tuple: - Literal: Integer: 1 span: "0:1-2" - Literal: Integer: 2 span: "0:21-22" span: "0:0-23" "#); // Function call in a tuple let ab = parse_expr(r#"{a b}"#).unwrap(); let a_comma_b = parse_expr(r#"{a, b}"#).unwrap(); assert_yaml_snapshot!(ab, @r#" Tuple: - FuncCall: name: Ident: - a span: "0:1-2" args: - Ident: - b span: "0:3-4" span: "0:1-4" span: "0:0-5" "#); assert_yaml_snapshot!(a_comma_b, @r#" Tuple: - Ident: - a span: "0:1-2" - Ident: - b span: "0:4-5" span: "0:0-6" "#); assert_ne!(ab, a_comma_b); assert_yaml_snapshot!(parse_expr(r#"{amount, +amount, -amount}"#).unwrap(), @r#" Tuple: - Ident: - amount span: "0:1-7" - Unary: op: Add expr: Ident: - amount span: "0:10-16" span: "0:9-16" - Unary: op: Neg expr: Ident: - amount span: "0:19-25" span: "0:18-25" span: "0:0-26" "#); // Operators in tuple items assert_yaml_snapshot!(parse_expr(r#"{amount, +amount, -amount}"#).unwrap(), @r#" Tuple: - Ident: - amount span: "0:1-7" - Unary: op: Add expr: Ident: - amount span: "0:10-16" span: "0:9-16" - Unary: op: Neg expr: Ident: - amount span: "0:19-25" span: "0:18-25" span: "0:0-26" "#); } #[test] fn test_number() { assert_yaml_snapshot!(parse_expr(r#"23"#).unwrap(), @r#" Literal: Integer: 23 span: "0:0-2" "#); assert_yaml_snapshot!(parse_expr(r#"2_3_4.5_6"#).unwrap(), @r#" Literal: Float: 234.56 span: "0:0-9" "#); assert_yaml_snapshot!(parse_expr(r#"23.6"#).unwrap(), @r#" Literal: Float: 23.6 span: "0:0-4" "#); assert_yaml_snapshot!(parse_expr(r#"23.0"#).unwrap(), @r#" Literal: Float: 23 span: "0:0-4" "#); assert_yaml_snapshot!(parse_expr(r#"2 + 2"#).unwrap(), @r#" Binary: left: Literal: Integer: 2 span: "0:0-1" op: Add right: Literal: Integer: 2 span: "0:4-5" span: "0:0-5" "#); // Underscores at the beginning are parsed as ident assert!(parse_expr("_2").unwrap().kind.into_ident().is_ok()); assert!(parse_expr("_").unwrap().kind.into_ident().is_ok()); assert!(parse_expr("_2._3").unwrap().kind.is_ident()); assert_yaml_snapshot!(parse_expr(r#"2e3"#).unwrap(), @r#" Literal: Float: 2000 span: "0:0-3" "#); // expr_of_string("2_").unwrap_err(); // TODO // expr_of_string("2.3_").unwrap_err(); // TODO } #[test] fn test_derive() { assert_yaml_snapshot!( parse_expr(r#"derive {x = 5, y = (-x)}"#).unwrap() , @r#" FuncCall: name: Ident: - derive span: "0:0-6" args: - Tuple: - Literal: Integer: 5 span: "0:12-13" alias: x - Unary: op: Neg expr: Ident: - x span: "0:21-22" span: "0:19-23" alias: y span: "0:7-24" span: "0:0-24" "#); } #[test] fn test_select() { assert_yaml_snapshot!( parse_expr(r#"select x"#).unwrap() , @r#" FuncCall: name: Ident: - select span: "0:0-6" args: - Ident: - x span: "0:7-8" span: "0:0-8" "#); assert_yaml_snapshot!( parse_expr(r#"select !{x}"#).unwrap() , @r#" FuncCall: name: Ident: - select span: "0:0-6" args: - Unary: op: Not expr: Tuple: - Ident: - x span: "0:9-10" span: "0:8-11" span: "0:7-11" span: "0:0-11" "#); assert_yaml_snapshot!( parse_expr(r#"select {x, y}"#).unwrap() , @r#" FuncCall: name: Ident: - select span: "0:0-6" args: - Tuple: - Ident: - x span: "0:8-9" - Ident: - y span: "0:11-12" span: "0:7-13" span: "0:0-13" "#); } #[test] fn test_expr() { assert_yaml_snapshot!( parse_expr(r#"country == "USA""#).unwrap() , @r#" Binary: left: Ident: - country span: "0:0-7" op: Eq right: Literal: String: USA span: "0:11-16" span: "0:0-16" "#); assert_yaml_snapshot!(parse_expr( r#"{ gross_salary = salary + payroll_tax, gross_cost = gross_salary + benefits_cost, }"#).unwrap(), @r#" Tuple: - Binary: left: Ident: - salary span: "0:19-25" op: Add right: Ident: - payroll_tax span: "0:28-39" span: "0:19-39" alias: gross_salary - Binary: left: Ident: - gross_salary span: "0:58-70" op: Add right: Ident: - benefits_cost span: "0:73-86" span: "0:58-86" alias: gross_cost span: "0:0-89" "#); assert_yaml_snapshot!( parse_expr( "(salary + payroll_tax) * (1 + tax_rate)" ).unwrap(), @r#" Binary: left: Binary: left: Ident: - salary span: "0:1-7" op: Add right: Ident: - payroll_tax span: "0:10-21" span: "0:1-21" op: Mul right: Binary: left: Literal: Integer: 1 span: "0:26-27" op: Add right: Ident: - tax_rate span: "0:30-38" span: "0:26-38" span: "0:0-39" "#); } #[test] fn test_regex() { assert_yaml_snapshot!( parse_expr( "'oba' ~= 'foobar'" ).unwrap(), @r#" Binary: left: Literal: String: oba span: "0:0-5" op: RegexSearch right: Literal: String: foobar span: "0:9-17" span: "0:0-17" "#); } #[test] fn test_func_call() { // Function without argument let ast = parse_expr(r#"count"#).unwrap(); let ident = ast.kind.into_ident().unwrap(); assert_yaml_snapshot!( ident, @"- count"); let ast = parse_expr(r#"s 'foo'"#).unwrap(); assert_yaml_snapshot!( ast, @r#" FuncCall: name: Ident: - s span: "0:0-1" args: - Literal: String: foo span: "0:2-7" span: "0:0-7" "#); // A non-friendly option for #154 let ast = parse_expr(r#"count s'*'"#).unwrap(); let func_call: FuncCall = ast.kind.into_func_call().unwrap(); assert_yaml_snapshot!( func_call, @r#" name: Ident: - count span: "0:0-5" args: - SString: - String: "*" span: "0:6-10" "#); parse_expr("plus_one x:0 x:0 ").unwrap_err(); let ast = parse_expr(r#"add bar to=3"#).unwrap(); assert_yaml_snapshot!( ast, @r#" FuncCall: name: Ident: - add span: "0:0-3" args: - Ident: - bar span: "0:4-7" - Literal: Integer: 3 span: "0:11-12" alias: to span: "0:0-12" "#); } #[test] fn test_right_assoc() { assert_yaml_snapshot!(parse_expr(r#"2 ** 3 ** 4"#).unwrap(), @r#" Binary: left: Literal: Integer: 2 span: "0:0-1" op: Pow right: Binary: left: Literal: Integer: 3 span: "0:5-6" op: Pow right: Literal: Integer: 4 span: "0:10-11" span: "0:5-11" span: "0:0-11" "#); assert_yaml_snapshot!(parse_expr(r#"1 + 2 ** (3 + 4) ** 4"#).unwrap(), @r#" Binary: left: Literal: Integer: 1 span: "0:0-1" op: Add right: Binary: left: Literal: Integer: 2 span: "0:4-5" op: Pow right: Binary: left: Binary: left: Literal: Integer: 3 span: "0:10-11" op: Add right: Literal: Integer: 4 span: "0:14-15" span: "0:10-15" op: Pow right: Literal: Integer: 4 span: "0:20-21" span: "0:9-21" span: "0:4-21" span: "0:0-21" "#); } #[test] fn test_op_precedence() { assert_yaml_snapshot!(parse_expr(r#"1 + 2 - 3 - 4"#).unwrap(), @r#" Binary: left: Binary: left: Binary: left: Literal: Integer: 1 span: "0:0-1" op: Add right: Literal: Integer: 2 span: "0:4-5" span: "0:0-5" op: Sub right: Literal: Integer: 3 span: "0:8-9" span: "0:0-9" op: Sub right: Literal: Integer: 4 span: "0:12-13" span: "0:0-13" "#); assert_yaml_snapshot!(parse_expr(r#"1 / (3 * 4)"#).unwrap(), @r#" Binary: left: Literal: Integer: 1 span: "0:0-1" op: DivFloat right: Binary: left: Literal: Integer: 3 span: "0:5-6" op: Mul right: Literal: Integer: 4 span: "0:9-10" span: "0:5-10" span: "0:0-11" "#); assert_yaml_snapshot!(parse_expr(r#"1 / 2 - 3 * 4 + 1"#).unwrap(), @r#" Binary: left: Binary: left: Binary: left: Literal: Integer: 1 span: "0:0-1" op: DivFloat right: Literal: Integer: 2 span: "0:4-5" span: "0:0-5" op: Sub right: Binary: left: Literal: Integer: 3 span: "0:8-9" op: Mul right: Literal: Integer: 4 span: "0:12-13" span: "0:8-13" span: "0:0-13" op: Add right: Literal: Integer: 1 span: "0:16-17" span: "0:0-17" "#); assert_yaml_snapshot!(parse_expr(r#"a && b || !c && d"#).unwrap(), @r#" Binary: left: Binary: left: Ident: - a span: "0:0-1" op: And right: Ident: - b span: "0:5-6" span: "0:0-6" op: Or right: Binary: left: Unary: op: Not expr: Ident: - c span: "0:11-12" span: "0:10-12" op: And right: Ident: - d span: "0:16-17" span: "0:10-17" span: "0:0-17" "#); assert_yaml_snapshot!(parse_expr(r#"a && b + c || (d e) && f"#).unwrap(), @r#" Binary: left: Binary: left: Ident: - a span: "0:0-1" op: And right: Binary: left: Ident: - b span: "0:5-6" op: Add right: Ident: - c span: "0:9-10" span: "0:5-10" span: "0:0-10" op: Or right: Binary: left: FuncCall: name: Ident: - d span: "0:15-16" args: - Ident: - e span: "0:17-18" span: "0:15-18" op: And right: Ident: - f span: "0:23-24" span: "0:14-24" span: "0:0-24" "#); } #[test] fn test_inline_pipeline() { assert_yaml_snapshot!(parse_expr("(salary | percentile 50)").unwrap(), @r#" Pipeline: exprs: - Ident: - salary span: "0:1-7" - FuncCall: name: Ident: - percentile span: "0:10-20" args: - Literal: Integer: 50 span: "0:21-23" span: "0:10-23" span: "0:0-24" "#); } #[test] fn test_dates() { assert_yaml_snapshot!(parse_expr("@2011-02-01").unwrap(), @r#" Literal: Date: 2011-02-01 span: "0:0-11" "#); assert_yaml_snapshot!(parse_expr("@2011-02-01T10:00").unwrap(), @r#" Literal: Timestamp: "2011-02-01T10:00" span: "0:0-17" "#); assert_yaml_snapshot!(parse_expr("@14:00").unwrap(), @r#" Literal: Time: "14:00" span: "0:0-6" "#); // assert_yaml_snapshot!(parse_expr("@2011-02-01T10:00").unwrap(), @""); parse_expr("@2020-01-0").unwrap_err(); parse_expr("@2020-01-011").unwrap_err(); parse_expr("@2020-01-01T111").unwrap_err(); } #[test] fn test_ident_with_keywords() { assert_yaml_snapshot!(parse_expr(r"select {andrew, orion, lettuce, falsehood, null0}").unwrap(), @r#" FuncCall: name: Ident: - select span: "0:0-6" args: - Tuple: - Ident: - andrew span: "0:8-14" - Ident: - orion span: "0:16-21" - Ident: - lettuce span: "0:23-30" - Ident: - falsehood span: "0:32-41" - Ident: - null0 span: "0:43-48" span: "0:7-49" span: "0:0-49" "#); assert_yaml_snapshot!(parse_expr(r"{false}").unwrap(), @r#" Tuple: - Literal: Boolean: false span: "0:1-6" span: "0:0-7" "#); } // Removed: test_case - duplicate of parser::expr::tests::test_case which uses proper EOF handling #[test] fn test_params() { assert_yaml_snapshot!(parse_expr(r#"$2"#).unwrap(), @r#" Param: "2" span: "0:0-2" "#); assert_yaml_snapshot!(parse_expr(r#"$2_any_text"#).unwrap(), @r#" Param: 2_any_text span: "0:0-11" "#); } #[test] fn test_lookup_01() { assert_yaml_snapshot!(parse_expr( r#"{a = {x = 2}}"#, ).unwrap(), @r#" Tuple: - Tuple: - Literal: Integer: 2 span: "0:10-11" alias: x span: "0:5-12" alias: a span: "0:0-13" "#); } #[test] fn test_lookup_02() { assert_yaml_snapshot!(parse_expr( r#"hello.*"#, ).unwrap(), @r#" Ident: - hello - "*" span: "0:0-7" "#); } ================================================ FILE: prqlc/prqlc-parser/src/parser/types.rs ================================================ use chumsky; use chumsky::input::BorrowInput; use super::expr::ident; use super::pr::*; use super::*; use crate::lexer::lr; use crate::lexer::lr::TokenKind; use super::ParserError; pub(crate) fn type_expr<'a, I>() -> impl Parser<'a, I, Ty, ParserError<'a>> + Clone where I: Input<'a, Token = lr::Token, Span = Span> + BorrowInput<'a>, { recursive(|nested_type_expr| { let basic = select_ref! { lr::Token { kind: TokenKind::Ident(i), .. } if i == "int"=> TyKind::Primitive(PrimitiveSet::Int), lr::Token { kind: TokenKind::Ident(i), .. } if i == "float"=> TyKind::Primitive(PrimitiveSet::Float), lr::Token { kind: TokenKind::Ident(i), .. } if i == "bool"=> TyKind::Primitive(PrimitiveSet::Bool), lr::Token { kind: TokenKind::Ident(i), .. } if i == "text"=> TyKind::Primitive(PrimitiveSet::Text), lr::Token { kind: TokenKind::Ident(i), .. } if i == "date"=> TyKind::Primitive(PrimitiveSet::Date), lr::Token { kind: TokenKind::Ident(i), .. } if i == "time"=> TyKind::Primitive(PrimitiveSet::Time), lr::Token { kind: TokenKind::Ident(i), .. } if i == "timestamp"=> TyKind::Primitive(PrimitiveSet::Timestamp), }; let ident = ident().map(TyKind::Ident); let func = keyword("func") .ignore_then( nested_type_expr .clone() .map(Some) .repeated() .collect() .then_ignore(select_ref! { lr::Token { kind: TokenKind::ArrowThin, .. } => () }) .then(nested_type_expr.clone().map(Box::new).map(Some)) .map(|(params, return_ty)| TyFunc { name_hint: None, params, return_ty, }) .or_not(), ) .map(TyKind::Function); let tuple = { use chumsky::recovery::{skip_then_retry_until, via_parser}; sequence(choice(( select_ref! { lr::Token { kind: TokenKind::Range { bind_right: false, bind_left: _ }, .. } => () } .to(TyTupleField::Wildcard(None)), select_ref! { lr::Token { kind: TokenKind::Range { bind_right: true, bind_left: _ }, .. } => () } .ignore_then(nested_type_expr.clone().or_not()) .map(TyTupleField::Wildcard), ident_part() .then_ignore(ctrl('=')) .or_not() .then(ctrl('*').to(None).or(nested_type_expr.clone().map(Some))) .map(|(name, ty)| TyTupleField::Single(name, ty)), ))) .delimited_by( ctrl('{'), ctrl('}') .recover_with(via_parser(end())) .recover_with(skip_then_retry_until( any_ref().ignored(), ctrl('}').ignored().or(ctrl(',').ignored()).or(end()), )), ) .try_map(|fields, span| { let without_last = &fields[0..fields.len().saturating_sub(1)]; if let Some(unpack) = without_last.iter().find_map(|f| f.as_wildcard()) { let err_span = unpack.as_ref().and_then(|s| s.span).unwrap_or(span); return Err(Rich::custom( err_span, "unpacking must come after all other fields", )); } Ok(fields) }) .map(TyKind::Tuple) .labelled("tuple") }; let array = { use chumsky::recovery::{skip_then_retry_until, via_parser}; nested_type_expr .map(Box::new) .or_not() .padded_by(new_line().repeated()) .delimited_by( ctrl('['), ctrl(']') .recover_with(via_parser(end())) .recover_with(skip_then_retry_until( any_ref().ignored(), ctrl(']').ignored().or(ctrl(',').ignored()).or(end()), )), ) .map(TyKind::Array) .labelled("array") }; choice((basic, ident, func, tuple, array)) .map_with(|kind, extra| TyKind::into_ty(kind, extra.span())) // exclude // term.clone() // .then(ctrl('-').ignore_then(term).repeated()) // .foldl(|left, right| { // let left_span = left.span.as_ref().unwrap(); // let right_span = right.span.as_ref().unwrap(); // let span = Span { // start: left_span.start, // end: right_span.end, // source_id: left_span.source_id, // }; // let kind = TyKind::Exclude { // base: Box::new(left), // except: Box::new(right), // }; // into_ty(kind, span) // }); }) .boxed() .labelled("type expression") } ================================================ FILE: prqlc/prqlc-parser/src/snapshots/prqlc_parser__test__pipeline_parse_tree.snap ================================================ --- source: prqlc/prqlc-parser/src/test.rs expression: "parse_source(r#\"\nfrom employees\nfilter country == \"USA\" # Each line transforms the previous result.\nderive { # This adds columns / variables.\n gross_salary = salary + payroll_tax,\n gross_cost = gross_salary + benefits_cost # Variables can use other variables.\n}\nfilter gross_cost > 0\ngroup {title, country} ( # For each group use a nested pipeline\n aggregate { # Aggregate each group to a single row\n average salary,\n average gross_salary,\n sum salary,\n sum gross_salary,\n average gross_cost,\n sum_gross_cost = sum gross_cost,\n ct = count salary,\n }\n)\nsort sum_gross_cost\nfilter ct > 200\ntake 20\n \"#).unwrap()" --- - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: "0:1-5" args: - Ident: - employees span: "0:6-15" span: "0:1-15" - FuncCall: name: Ident: - filter span: "0:16-22" args: - Binary: left: Ident: - country span: "0:23-30" op: Eq right: Literal: String: USA span: "0:34-39" span: "0:23-39" span: "0:16-39" - FuncCall: name: Ident: - derive span: "0:105-111" args: - Tuple: - Binary: left: Ident: - salary span: "0:200-206" op: Add right: Ident: - payroll_tax span: "0:209-220" span: "0:200-220" alias: gross_salary - Binary: left: Ident: - gross_salary span: "0:237-249" op: Add right: Ident: - benefits_cost span: "0:252-265" span: "0:237-265" alias: gross_cost span: "0:112-305" span: "0:105-305" - FuncCall: name: Ident: - filter span: "0:306-312" args: - Binary: left: Ident: - gross_cost span: "0:313-323" op: Gt right: Literal: Integer: 0 span: "0:326-327" span: "0:313-327" span: "0:306-327" - FuncCall: name: Ident: - group span: "0:328-333" args: - Tuple: - Ident: - title span: "0:335-340" - Ident: - country span: "0:342-349" span: "0:334-350" - FuncCall: name: Ident: - aggregate span: "0:414-423" args: - Tuple: - FuncCall: name: Ident: - average span: "0:500-507" args: - Ident: - salary span: "0:508-514" span: "0:500-514" - FuncCall: name: Ident: - average span: "0:520-527" args: - Ident: - gross_salary span: "0:528-540" span: "0:520-540" - FuncCall: name: Ident: - sum span: "0:546-549" args: - Ident: - salary span: "0:550-556" span: "0:546-556" - FuncCall: name: Ident: - sum span: "0:562-565" args: - Ident: - gross_salary span: "0:566-578" span: "0:562-578" - FuncCall: name: Ident: - average span: "0:584-591" args: - Ident: - gross_cost span: "0:592-602" span: "0:584-602" - FuncCall: name: Ident: - sum span: "0:625-628" args: - Ident: - gross_cost span: "0:629-639" span: "0:625-639" alias: sum_gross_cost - FuncCall: name: Ident: - count span: "0:650-655" args: - Ident: - salary span: "0:656-662" span: "0:650-662" alias: ct span: "0:424-667" span: "0:414-667" span: "0:328-669" - FuncCall: name: Ident: - sort span: "0:670-674" args: - Ident: - sum_gross_cost span: "0:675-689" span: "0:670-689" - FuncCall: name: Ident: - filter span: "0:690-696" args: - Binary: left: Ident: - ct span: "0:697-699" op: Gt right: Literal: Integer: 200 span: "0:702-705" span: "0:697-705" span: "0:690-705" - FuncCall: name: Ident: - take span: "0:706-710" args: - Literal: Integer: 20 span: "0:711-713" span: "0:706-713" span: "0:1-713" span: "0:0-713" ================================================ FILE: prqlc/prqlc-parser/src/span.rs ================================================ use std::fmt::{self, Debug, Formatter}; use std::ops::{Add, Range, Sub}; use chumsky; use schemars::JsonSchema; use serde::de::Visitor; use serde::{Deserialize, Serialize}; #[derive(Clone, PartialEq, Eq, Copy, JsonSchema)] pub struct Span { pub start: usize, pub end: usize, /// A key representing the path of the source. Value is stored in prqlc's SourceTree::source_ids. pub source_id: u16, } // Implement Chumsky's Span trait for our custom Span impl chumsky::span::Span for Span { type Context = u16; // source_id type Offset = usize; fn new(context: Self::Context, range: Range) -> Self { Span { start: range.start, end: range.end, source_id: context, } } fn context(&self) -> Self::Context { self.source_id } fn start(&self) -> Self::Offset { self.start } fn end(&self) -> Self::Offset { self.end } fn to_end(&self) -> Self { Span { start: self.end, end: self.end, source_id: self.source_id, } } } impl From for Range { fn from(a: Span) -> Self { a.start..a.end } } impl Debug for Span { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}:{}-{}", self.source_id, self.start, self.end) } } impl Serialize for Span { fn serialize(&self, serializer: S) -> std::result::Result where S: serde::Serializer, { let str = format!("{self:?}"); serializer.serialize_str(&str) } } impl PartialOrd for Span { fn partial_cmp(&self, other: &Self) -> Option { // We could expand this to compare source_id too, starting with minimum surprise match other.source_id.partial_cmp(&self.source_id) { Some(std::cmp::Ordering::Equal) => { debug_assert!((self.start <= other.start) == (self.end <= other.end)); self.start.partial_cmp(&other.start) } _ => None, } } } impl<'de> Deserialize<'de> for Span { fn deserialize(deserializer: D) -> std::result::Result where D: serde::Deserializer<'de>, { struct SpanVisitor {} impl Visitor<'_> for SpanVisitor { type Value = Span; fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "A span string of form `file_id:x-y`") } fn visit_str(self, v: &str) -> std::result::Result where E: serde::de::Error, { use serde::de; if let Some((file_id, char_span)) = v.split_once(':') { let file_id = file_id .parse::() .map_err(|e| de::Error::custom(e.to_string()))?; if let Some((start, end)) = char_span.split_once('-') { let start = start .parse::() .map_err(|e| de::Error::custom(e.to_string()))?; let end = end .parse::() .map_err(|e| de::Error::custom(e.to_string()))?; return Ok(Span { start, end, source_id: file_id, }); } } Err(de::Error::custom("malformed span")) } fn visit_string(self, v: String) -> std::result::Result where E: serde::de::Error, { self.visit_str(&v) } } deserializer.deserialize_string(SpanVisitor {}) } } impl Add for Span { type Output = Span; fn add(self, rhs: usize) -> Span { Self { start: self.start + rhs, end: self.end + rhs, source_id: self.source_id, } } } impl Sub for Span { type Output = Span; fn sub(self, rhs: usize) -> Span { Self { start: self.start - rhs, end: self.end - rhs, source_id: self.source_id, } } } #[cfg(test)] mod test { use super::*; #[test] fn test_span_serde() { let span = Span { start: 12, end: 15, source_id: 45, }; let span_serialized = serde_json::to_string(&span).unwrap(); insta::assert_snapshot!(span_serialized, @r#""45:12-15""#); let span_deserialized: Span = serde_json::from_str(&span_serialized).unwrap(); assert_eq!(span_deserialized, span); } #[test] fn test_span_partial_cmp() { let span1 = Span { start: 10, end: 20, source_id: 1, }; let span2 = Span { start: 15, end: 25, source_id: 1, }; let span3 = Span { start: 5, end: 15, source_id: 2, }; // span1 and span2 have the same source_id, so their start values are compared assert_eq!(span1.partial_cmp(&span2), Some(std::cmp::Ordering::Less)); assert_eq!(span2.partial_cmp(&span1), Some(std::cmp::Ordering::Greater)); // span1 and span3 have different source_id, so their source_id values are compared assert_eq!(span1.partial_cmp(&span3), None); assert_eq!(span3.partial_cmp(&span1), None); } } ================================================ FILE: prqlc/prqlc-parser/src/test.rs ================================================ use chumsky::prelude::*; use chumsky::span::SimpleSpan; use insta::{assert_debug_snapshot, assert_yaml_snapshot}; use crate::error::Error; use crate::parser::pr::Stmt; use crate::parser::stmt; use crate::span::Span; /// Parse into statements pub(crate) fn parse_source(source: &str) -> Result, Vec> { let tokens = crate::lexer::lex_source(source)?; // Filter out comments let semantic_tokens: Vec<_> = tokens .0 .into_iter() .filter(|token| { !matches!( token.kind, crate::lexer::lr::TokenKind::Comment(_) | crate::lexer::lr::TokenKind::LineWrap(_) ) }) .collect(); let input = semantic_tokens .as_slice() .map_span(|simple_span: SimpleSpan| { let start_idx = simple_span.start(); let end_idx = simple_span.end(); let start = semantic_tokens .get(start_idx) .map(|t| t.span.start) .unwrap_or(0); let end = semantic_tokens .get(end_idx.saturating_sub(1)) .map(|t| t.span.end) .unwrap_or(start); Span { start, end, source_id: 0, } }); let parse_result = stmt::source().parse(input); let (ast, parse_errors) = parse_result.into_output_errors(); if !parse_errors.is_empty() { log::info!("ast: {ast:?}"); return Err(parse_errors.into_iter().map(|e| e.into()).collect()); } Ok(ast.unwrap()) } #[test] fn test_error_unicode_string() { // Test various unicode strings successfully parse errors. We were // getting loops in the lexer before. parse_source("s’ ").unwrap_err(); parse_source("s’").unwrap_err(); parse_source(" s’").unwrap_err(); parse_source(" ’ s").unwrap_err(); parse_source("’s").unwrap_err(); parse_source("👍 s’").unwrap_err(); let source = "Mississippi has four S’s and four I’s."; // LEXER output for comparison (what the lexer sees): assert_debug_snapshot!(crate::lexer::lex_source(source).unwrap_err(), @r#" [ Error { kind: Error, span: Some( 0:22-23, ), reason: Unexpected { found: "'’'", }, hints: [], code: None, }, ] "#); // PARSER output (what happens after lexing): assert_debug_snapshot!(parse_source(source).unwrap_err(), @r#" [ Error { kind: Error, span: Some( 0:22-23, ), reason: Unexpected { found: "'’'", }, hints: [], code: None, }, ] "#); } #[test] fn test_error_unexpected() { assert_debug_snapshot!(parse_source("Answer: T-H-A-T!").unwrap_err(), @r#" [ Error { kind: Error, span: Some( 0:15-16, ), reason: Expected { who: None, expected: "something else", found: "!", }, hints: [], code: None, }, ] "#); } #[test] fn test_pipeline_parse_tree() { assert_yaml_snapshot!(parse_source( r#" from employees filter country == "USA" # Each line transforms the previous result. derive { # This adds columns / variables. gross_salary = salary + payroll_tax, gross_cost = gross_salary + benefits_cost # Variables can use other variables. } filter gross_cost > 0 group {title, country} ( # For each group use a nested pipeline aggregate { # Aggregate each group to a single row average salary, average gross_salary, sum salary, sum gross_salary, average gross_cost, sum_gross_cost = sum gross_cost, ct = count salary, } ) sort sum_gross_cost filter ct > 200 take 20 "# ) .unwrap()); } #[test] fn test_take() { parse_source("take 10").unwrap(); assert_yaml_snapshot!(parse_source(r#"take 10"#).unwrap(), @r#" - VarDef: kind: Main name: main value: FuncCall: name: Ident: - take span: "0:0-4" args: - Literal: Integer: 10 span: "0:5-7" span: "0:0-7" span: "0:0-7" "#); assert_yaml_snapshot!(parse_source(r#"take ..10"#).unwrap(), @r#" - VarDef: kind: Main name: main value: FuncCall: name: Ident: - take span: "0:0-4" args: - Range: start: ~ end: Literal: Integer: 10 span: "0:7-9" span: "0:4-9" span: "0:0-9" span: "0:0-9" "#); assert_yaml_snapshot!(parse_source(r#"take 1..10"#).unwrap(), @r#" - VarDef: kind: Main name: main value: FuncCall: name: Ident: - take span: "0:0-4" args: - Range: start: Literal: Integer: 1 span: "0:5-6" end: Literal: Integer: 10 span: "0:8-10" span: "0:5-10" span: "0:0-10" span: "0:0-10" "#); } #[test] fn test_filter() { assert_yaml_snapshot!( parse_source(r#"filter country == "USA""#).unwrap(), @r#" - VarDef: kind: Main name: main value: FuncCall: name: Ident: - filter span: "0:0-6" args: - Binary: left: Ident: - country span: "0:7-14" op: Eq right: Literal: String: USA span: "0:18-23" span: "0:7-23" span: "0:0-23" span: "0:0-23" "#); assert_yaml_snapshot!( parse_source(r#"filter (text.upper country) == "USA""#).unwrap(), @r#" - VarDef: kind: Main name: main value: FuncCall: name: Ident: - filter span: "0:0-6" args: - Binary: left: FuncCall: name: Ident: - text - upper span: "0:8-18" args: - Ident: - country span: "0:19-26" span: "0:8-26" op: Eq right: Literal: String: USA span: "0:31-36" span: "0:7-36" span: "0:0-36" span: "0:0-36" "# ); } #[test] fn test_aggregate() { let aggregate = parse_source( r"group {title} ( aggregate {sum salary, count} )", ) .unwrap(); assert_yaml_snapshot!( aggregate, @r#" - VarDef: kind: Main name: main value: FuncCall: name: Ident: - group span: "0:0-5" args: - Tuple: - Ident: - title span: "0:7-12" span: "0:6-13" - FuncCall: name: Ident: - aggregate span: "0:32-41" args: - Tuple: - FuncCall: name: Ident: - sum span: "0:43-46" args: - Ident: - salary span: "0:47-53" span: "0:43-53" - Ident: - count span: "0:55-60" span: "0:42-61" span: "0:32-61" span: "0:0-77" span: "0:0-77" "#); let aggregate = parse_source( r"group {title} ( aggregate {sum salary} )", ) .unwrap(); assert_yaml_snapshot!( aggregate, @r#" - VarDef: kind: Main name: main value: FuncCall: name: Ident: - group span: "0:0-5" args: - Tuple: - Ident: - title span: "0:7-12" span: "0:6-13" - FuncCall: name: Ident: - aggregate span: "0:32-41" args: - Tuple: - FuncCall: name: Ident: - sum span: "0:43-46" args: - Ident: - salary span: "0:47-53" span: "0:43-53" span: "0:42-54" span: "0:32-54" span: "0:0-70" span: "0:0-70" "#); } #[test] fn test_basic_exprs() { // Currently not putting comments in our parse tree, so this is blank. assert_yaml_snapshot!(parse_source( r#"# this is a comment select a"# ).unwrap(), @r#" - VarDef: kind: Main name: main value: FuncCall: name: Ident: - select span: "0:28-34" args: - Ident: - a span: "0:35-36" span: "0:28-36" span: "0:0-36" "#); } #[test] fn test_function() { assert_yaml_snapshot!(parse_source("let plus_one = x -> x + 1\n").unwrap(), @r#" - VarDef: kind: Let name: plus_one value: Func: return_ty: ~ body: Binary: left: Ident: - x span: "0:21-22" op: Add right: Literal: Integer: 1 span: "0:25-26" span: "0:21-26" params: - name: x default_value: ~ named_params: [] span: "0:15-26" span: "0:0-26" "#); assert_yaml_snapshot!(parse_source("let identity = x -> x\n").unwrap() , @r#" - VarDef: kind: Let name: identity value: Func: return_ty: ~ body: Ident: - x span: "0:21-22" params: - name: x default_value: ~ named_params: [] span: "0:15-22" span: "0:0-22" "#); assert_yaml_snapshot!(parse_source("let plus_one = x -> (x + 1)\n").unwrap() , @r#" - VarDef: kind: Let name: plus_one value: Func: return_ty: ~ body: Binary: left: Ident: - x span: "0:22-23" op: Add right: Literal: Integer: 1 span: "0:26-27" span: "0:21-28" params: - name: x default_value: ~ named_params: [] span: "0:15-28" span: "0:0-28" "#); assert_yaml_snapshot!(parse_source("let plus_one = x -> x + 1\n").unwrap() , @r#" - VarDef: kind: Let name: plus_one value: Func: return_ty: ~ body: Binary: left: Ident: - x span: "0:21-22" op: Add right: Literal: Integer: 1 span: "0:25-26" span: "0:21-26" params: - name: x default_value: ~ named_params: [] span: "0:15-26" span: "0:0-26" "#); assert_yaml_snapshot!(parse_source("let foo = x -> some_func (foo bar + 1) (plax) - baz\n").unwrap() , @r#" - VarDef: kind: Let name: foo value: Func: return_ty: ~ body: FuncCall: name: Ident: - some_func span: "0:15-24" args: - FuncCall: name: Ident: - foo span: "0:26-29" args: - Binary: left: Ident: - bar span: "0:30-33" op: Add right: Literal: Integer: 1 span: "0:36-37" span: "0:30-37" span: "0:26-37" - Binary: left: Ident: - plax span: "0:40-44" op: Sub right: Ident: - baz span: "0:48-51" span: "0:39-51" span: "0:15-51" params: - name: x default_value: ~ named_params: [] span: "0:10-51" span: "0:0-51" "#); assert_yaml_snapshot!(parse_source("func return_constant -> 42\n").unwrap(), @r#" - VarDef: kind: Main name: main value: Func: return_ty: ~ body: Literal: Integer: 42 span: "0:25-27" params: - name: return_constant default_value: ~ named_params: [] span: "0:0-27" span: "0:0-27" "#); assert_yaml_snapshot!(parse_source(r#"let count = X -> s"SUM({X})" "#).unwrap(), @r#" - VarDef: kind: Let name: count value: Func: return_ty: ~ body: SString: - String: SUM( - Expr: expr: Ident: - X span: "0:24-25" format: ~ - String: ) span: "0:17-28" params: - name: X default_value: ~ named_params: [] span: "0:12-28" span: "0:0-28" "#); assert_yaml_snapshot!(parse_source( r#" let lag_day = x -> ( window x by sec_id sort date lag 1 ) "# ) .unwrap(), @r#" - VarDef: kind: Let name: lag_day value: Func: return_ty: ~ body: Pipeline: exprs: - FuncCall: name: Ident: - window span: "0:51-57" args: - Ident: - x span: "0:58-59" span: "0:51-59" - FuncCall: name: Ident: - by span: "0:76-78" args: - Ident: - sec_id span: "0:79-85" span: "0:76-85" - FuncCall: name: Ident: - sort span: "0:102-106" args: - Ident: - date span: "0:107-111" span: "0:102-111" - FuncCall: name: Ident: - lag span: "0:128-131" args: - Literal: Integer: 1 span: "0:132-133" span: "0:128-133" span: "0:33-147" params: - name: x default_value: ~ named_params: [] span: "0:27-147" span: "0:0-147" "#); assert_yaml_snapshot!(parse_source("let add = x to:a -> x + to\n").unwrap(), @r#" - VarDef: kind: Let name: add value: Func: return_ty: ~ body: Binary: left: Ident: - x span: "0:21-22" op: Add right: Ident: - to span: "0:25-27" span: "0:21-27" params: - name: x default_value: ~ named_params: - name: to default_value: Ident: - a span: "0:15-16" span: "0:10-27" span: "0:0-27" "#); } #[test] fn test_var_def() { assert_yaml_snapshot!(parse_source( "let newest_employees = (from employees)" ).unwrap(), @r#" - VarDef: kind: Let name: newest_employees value: FuncCall: name: Ident: - from span: "0:24-28" args: - Ident: - employees span: "0:29-38" span: "0:23-39" span: "0:0-39" "#); assert_yaml_snapshot!(parse_source( r#" let newest_employees = ( from employees group country ( aggregate { average_country_salary = average salary } ) sort tenure take 50 )"#.trim()).unwrap(), @r#" - VarDef: kind: Let name: newest_employees value: Pipeline: exprs: - FuncCall: name: Ident: - from span: "0:35-39" args: - Ident: - employees span: "0:40-49" span: "0:35-49" - FuncCall: name: Ident: - group span: "0:60-65" args: - Ident: - country span: "0:66-73" - FuncCall: name: Ident: - aggregate span: "0:88-97" args: - Tuple: - FuncCall: name: Ident: - average span: "0:141-148" args: - Ident: - salary span: "0:149-155" span: "0:141-155" alias: average_country_salary span: "0:98-169" span: "0:88-169" span: "0:60-181" - FuncCall: name: Ident: - sort span: "0:192-196" args: - Ident: - tenure span: "0:197-203" span: "0:192-203" - FuncCall: name: Ident: - take span: "0:214-218" args: - Literal: Integer: 50 span: "0:219-221" span: "0:214-221" span: "0:23-231" span: "0:0-231" "#); assert_yaml_snapshot!(parse_source(r#" let e = s"SELECT * FROM employees" "#).unwrap(), @r#" - VarDef: kind: Let name: e value: SString: - String: SELECT * FROM employees span: "0:21-47" span: "0:0-47" "#); assert_yaml_snapshot!(parse_source( "let x = ( from x_table select only_in_x = foo ) from x" ).unwrap(), @r#" - VarDef: kind: Let name: x value: Pipeline: exprs: - FuncCall: name: Ident: - from span: "0:23-27" args: - Ident: - x_table span: "0:28-35" span: "0:23-35" - FuncCall: name: Ident: - select span: "0:49-55" args: - Ident: - foo span: "0:68-71" alias: only_in_x span: "0:49-71" span: "0:8-84" span: "0:0-84" - VarDef: kind: Main name: main value: FuncCall: name: Ident: - from span: "0:96-100" args: - Ident: - x span: "0:101-102" span: "0:96-102" span: "0:84-102" "#); } #[test] fn test_inline_pipeline() { assert_yaml_snapshot!(parse_source("let median = x -> (x | percentile 50)\n").unwrap(), @r#" - VarDef: kind: Let name: median value: Func: return_ty: ~ body: Pipeline: exprs: - Ident: - x span: "0:19-20" - FuncCall: name: Ident: - percentile span: "0:23-33" args: - Literal: Integer: 50 span: "0:34-36" span: "0:23-36" span: "0:18-37" params: - name: x default_value: ~ named_params: [] span: "0:13-37" span: "0:0-37" "#); } #[test] fn test_sql_parameters() { assert_yaml_snapshot!(parse_source(r#" from mytable filter { first_name == $1, last_name == $2.name } "#).unwrap(), @r#" - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: "0:9-13" args: - Ident: - mytable span: "0:14-21" span: "0:9-21" - FuncCall: name: Ident: - filter span: "0:30-36" args: - Tuple: - Binary: left: Ident: - first_name span: "0:49-59" op: Eq right: Param: "1" span: "0:63-65" span: "0:49-65" - Binary: left: Ident: - last_name span: "0:77-86" op: Eq right: Param: 2.name span: "0:90-97" span: "0:77-97" span: "0:37-107" span: "0:30-107" span: "0:9-107" span: "0:0-107" "#); } #[test] fn test_tab_characters() { // #284 parse_source( "from c_invoice join doc:c_doctype (==c_invoice_id) select [ \tinvoice_no, \tdocstatus ]", ) .unwrap(); } #[test] fn test_backticks() { let prql = " from `a/*.parquet` aggregate {max c} join `schema.table` (==id) join `my-proj.dataset.table` join `my-proj`.`dataset`.`table` "; assert_yaml_snapshot!(parse_source(prql).unwrap(), @r#" - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: "0:1-5" args: - Ident: - a/*.parquet span: "0:6-19" span: "0:1-19" - FuncCall: name: Ident: - aggregate span: "0:20-29" args: - Tuple: - FuncCall: name: Ident: - max span: "0:31-34" args: - Ident: - c span: "0:35-36" span: "0:31-36" span: "0:30-37" span: "0:20-37" - FuncCall: name: Ident: - join span: "0:38-42" args: - Ident: - schema.table span: "0:43-57" - Unary: op: EqSelf expr: Ident: - id span: "0:61-63" span: "0:59-63" span: "0:38-64" - FuncCall: name: Ident: - join span: "0:65-69" args: - Ident: - my-proj.dataset.table span: "0:70-93" span: "0:65-93" - FuncCall: name: Ident: - join span: "0:94-98" args: - Ident: - my-proj - dataset - table span: "0:99-126" span: "0:94-126" span: "0:1-126" span: "0:0-126" "#); } #[test] fn test_sort() { assert_yaml_snapshot!(parse_source(" from invoices sort issued_at sort (-issued_at) sort {issued_at} sort {-issued_at} sort {issued_at, -amount, +num_of_articles} ").unwrap(), @r#" - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: "0:9-13" args: - Ident: - invoices span: "0:14-22" span: "0:9-22" - FuncCall: name: Ident: - sort span: "0:31-35" args: - Ident: - issued_at span: "0:36-45" span: "0:31-45" - FuncCall: name: Ident: - sort span: "0:54-58" args: - Unary: op: Neg expr: Ident: - issued_at span: "0:61-70" span: "0:60-70" span: "0:54-71" - FuncCall: name: Ident: - sort span: "0:80-84" args: - Tuple: - Ident: - issued_at span: "0:86-95" span: "0:85-96" span: "0:80-96" - FuncCall: name: Ident: - sort span: "0:105-109" args: - Tuple: - Unary: op: Neg expr: Ident: - issued_at span: "0:112-121" span: "0:111-121" span: "0:110-122" span: "0:105-122" - FuncCall: name: Ident: - sort span: "0:131-135" args: - Tuple: - Ident: - issued_at span: "0:137-146" - Unary: op: Neg expr: Ident: - amount span: "0:149-155" span: "0:148-155" - Unary: op: Add expr: Ident: - num_of_articles span: "0:158-173" span: "0:157-173" span: "0:136-174" span: "0:131-174" span: "0:9-174" span: "0:0-174" "#); } #[test] fn test_dates() { assert_yaml_snapshot!(parse_source(" from employees derive {age_plus_two_years = (age + 2years)} ").unwrap(), @r#" - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: "0:9-13" args: - Ident: - employees span: "0:14-23" span: "0:9-23" - FuncCall: name: Ident: - derive span: "0:32-38" args: - Tuple: - Binary: left: Ident: - age span: "0:62-65" op: Add right: Literal: ValueAndUnit: n: 2 unit: years span: "0:68-74" span: "0:61-75" alias: age_plus_two_years span: "0:39-76" span: "0:32-76" span: "0:9-76" span: "0:0-76" "#); } #[test] fn test_multiline_string() { assert_yaml_snapshot!(parse_source(r##" derive x = r"r-string test" "##).unwrap(), @r#" - VarDef: kind: Main name: main value: FuncCall: name: Ident: - derive span: "0:9-15" args: - Literal: RawString: r-string test span: "0:20-36" alias: x span: "0:9-36" span: "0:0-36" "# ) } #[test] fn test_empty_lines() { // The span of the Pipeline shouldn't include the empty lines; the VarDef // should have a larger span assert_yaml_snapshot!(parse_source(r#" from artists derive x = 5 "#).unwrap(), @r#" - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: "0:1-5" args: - Ident: - artists span: "0:6-13" span: "0:1-13" - FuncCall: name: Ident: - derive span: "0:14-20" args: - Literal: Integer: 5 span: "0:25-26" alias: x span: "0:14-26" span: "0:1-26" span: "0:0-26" "# ) } #[test] fn test_coalesce() { assert_yaml_snapshot!(parse_source(r###" from employees derive amount = amount ?? 0 "###).unwrap(), @r#" - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: "0:9-13" args: - Ident: - employees span: "0:14-23" span: "0:9-23" - FuncCall: name: Ident: - derive span: "0:32-38" args: - Binary: left: Ident: - amount span: "0:48-54" op: Coalesce right: Literal: Integer: 0 span: "0:58-59" span: "0:48-59" alias: amount span: "0:32-59" span: "0:9-59" span: "0:0-59" "# ) } #[test] fn test_literal() { assert_yaml_snapshot!(parse_source(r###" derive x = true "###).unwrap(), @r#" - VarDef: kind: Main name: main value: FuncCall: name: Ident: - derive span: "0:9-15" args: - Literal: Boolean: true span: "0:20-24" alias: x span: "0:9-24" span: "0:0-24" "#) } #[test] fn test_allowed_idents() { assert_yaml_snapshot!(parse_source(r###" from employees join _salary (==employee_id) # table with leading underscore filter first_name == $1 select {_employees._underscored_column} "###).unwrap(), @r#" - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: "0:9-13" args: - Ident: - employees span: "0:14-23" span: "0:9-23" - FuncCall: name: Ident: - join span: "0:32-36" args: - Ident: - _salary span: "0:37-44" - Unary: op: EqSelf expr: Ident: - employee_id span: "0:48-59" span: "0:46-59" span: "0:32-60" - FuncCall: name: Ident: - filter span: "0:101-107" args: - Binary: left: Ident: - first_name span: "0:108-118" op: Eq right: Param: "1" span: "0:122-124" span: "0:108-124" span: "0:101-124" - FuncCall: name: Ident: - select span: "0:133-139" args: - Tuple: - Ident: - _employees - _underscored_column span: "0:141-171" span: "0:140-172" span: "0:133-172" span: "0:9-172" span: "0:0-172" "#) } #[test] fn test_gt_lt_gte_lte() { assert_yaml_snapshot!(parse_source(r###" from people filter age >= 100 filter num_grandchildren <= 10 filter salary > 0 filter num_eyes < 2 "###).unwrap(), @r#" - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: "0:9-13" args: - Ident: - people span: "0:14-20" span: "0:9-20" - FuncCall: name: Ident: - filter span: "0:29-35" args: - Binary: left: Ident: - age span: "0:36-39" op: Gte right: Literal: Integer: 100 span: "0:43-46" span: "0:36-46" span: "0:29-46" - FuncCall: name: Ident: - filter span: "0:55-61" args: - Binary: left: Ident: - num_grandchildren span: "0:62-79" op: Lte right: Literal: Integer: 10 span: "0:83-85" span: "0:62-85" span: "0:55-85" - FuncCall: name: Ident: - filter span: "0:94-100" args: - Binary: left: Ident: - salary span: "0:101-107" op: Gt right: Literal: Integer: 0 span: "0:110-111" span: "0:101-111" span: "0:94-111" - FuncCall: name: Ident: - filter span: "0:120-126" args: - Binary: left: Ident: - num_eyes span: "0:127-135" op: Lt right: Literal: Integer: 2 span: "0:138-139" span: "0:127-139" span: "0:120-139" span: "0:9-139" span: "0:0-139" "#) } #[test] fn test_assign() { assert_yaml_snapshot!(parse_source(r###" from employees join s=salaries (==id) "###).unwrap(), @r#" - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: "0:1-5" args: - Ident: - employees span: "0:6-15" span: "0:1-15" - FuncCall: name: Ident: - join span: "0:16-20" args: - Ident: - salaries span: "0:23-31" alias: s - Unary: op: EqSelf expr: Ident: - id span: "0:35-37" span: "0:33-37" span: "0:16-38" span: "0:1-38" span: "0:0-38" "#); } #[test] fn test_unicode() { let source = "from tète"; assert_yaml_snapshot!(parse_source(source).unwrap(), @r#" - VarDef: kind: Main name: main value: FuncCall: name: Ident: - from span: "0:0-4" args: - Ident: - tète span: "0:5-10" span: "0:0-10" span: "0:0-10" "#); } #[test] fn test_var_defs() { assert_yaml_snapshot!(parse_source(r#" let a = ( x ) "#).unwrap(), @r#" - VarDef: kind: Let name: a value: Ident: - x span: "0:17-42" span: "0:0-42" "#); assert_yaml_snapshot!(parse_source(r#" x into a "#).unwrap(), @r#" - VarDef: kind: Into name: a value: Ident: - x span: "0:9-10" span: "0:0-25" "#); assert_yaml_snapshot!(parse_source(r#" x "#).unwrap(), @r#" - VarDef: kind: Main name: main value: Ident: - x span: "0:9-10" span: "0:0-10" "#); } #[test] fn test_array() { assert_yaml_snapshot!(parse_source(r#" let a = [1, 2,] let a = [false, "hello"] "#).unwrap(), @r#" - VarDef: kind: Let name: a value: Array: - Literal: Integer: 1 span: "0:18-19" - Literal: Integer: 2 span: "0:21-22" span: "0:17-24" span: "0:0-24" - VarDef: kind: Let name: a value: Array: - Literal: Boolean: false span: "0:42-47" - Literal: String: hello span: "0:49-56" span: "0:41-57" span: "0:24-57" "#); } #[test] fn test_annotation() { assert_yaml_snapshot!(parse_source(r#" @{binding_strength=1} let add = a b -> a + b "#).unwrap(), @r#" - VarDef: kind: Let name: add value: Func: return_ty: ~ body: Binary: left: Ident: - a span: "0:56-57" op: Add right: Ident: - b span: "0:60-61" span: "0:56-61" params: - name: a default_value: ~ - name: b default_value: ~ named_params: [] span: "0:49-61" span: "0:0-61" annotations: - expr: Tuple: - Literal: Integer: 1 span: "0:28-29" alias: binding_strength span: "0:10-30" "#); parse_source( r#" @{binding_strength=1} let add = a b -> a + b "#, ) .unwrap(); parse_source( r#" @{binding_strength=1} # comment let add = a b -> a + b "#, ) .unwrap(); parse_source( r#" @{binding_strength=1} let add = a b -> a + b "#, ) .unwrap(); parse_source( r#" @{binding_strength=1}@{binding_strength=2} let add = a b -> a + b "#, ) .unwrap_err(); } #[test] fn check_valid_version() { let stmt = format!( r#" prql version:"{}" "#, env!("CARGO_PKG_VERSION_MAJOR") ); assert!(parse_source(&stmt).is_ok()); let stmt = format!( r#" prql version:"{}.{}" "#, env!("CARGO_PKG_VERSION_MAJOR"), env!("CARGO_PKG_VERSION_MINOR") ); assert!(parse_source(&stmt).is_ok()); let stmt = format!( r#" prql version:"{}.{}.{}" "#, env!("CARGO_PKG_VERSION_MAJOR"), env!("CARGO_PKG_VERSION_MINOR"), env!("CARGO_PKG_VERSION_PATCH"), ); assert!(parse_source(&stmt).is_ok()); } #[test] fn check_invalid_version() { let stmt = format!( "prql version:{}\n", env!("CARGO_PKG_VERSION_MAJOR").parse::().unwrap() + 1 ); assert!(parse_source(&stmt).is_err()); } #[test] fn test_target() { assert_yaml_snapshot!(parse_source( r#" prql target:sql.sqlite from film remove film2 "#, ) .unwrap(), @r#" - QueryDef: version: ~ other: target: sql.sqlite span: "0:0-34" - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: "0:45-49" args: - Ident: - film span: "0:50-54" span: "0:45-54" - FuncCall: name: Ident: - remove span: "0:65-71" args: - Ident: - film2 span: "0:72-77" span: "0:65-77" span: "0:45-77" span: "0:34-77" "#); } #[test] fn test_number() { // We don't allow trailing periods assert!(parse_source( r#" from artists derive x = 1."# ) .is_err()); } #[test] fn doc_comment() { use insta::assert_yaml_snapshot; assert_yaml_snapshot!(parse_source(r###" from artists derive x = 5 "###).unwrap(), @r#" - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: "0:5-9" args: - Ident: - artists span: "0:10-17" span: "0:5-17" - FuncCall: name: Ident: - derive span: "0:22-28" args: - Literal: Integer: 5 span: "0:33-34" alias: x span: "0:22-34" span: "0:5-34" span: "0:0-34" "#); assert_yaml_snapshot!(parse_source(r###" from artists #! This is a doc comment derive x = 5 "###).unwrap(), @r#" - VarDef: kind: Main name: main value: FuncCall: name: Ident: - from span: "0:5-9" args: - Ident: - artists span: "0:10-17" span: "0:5-17" span: "0:0-17" - VarDef: kind: Main name: main value: FuncCall: name: Ident: - derive span: "0:53-59" args: - Literal: Integer: 5 span: "0:64-65" alias: x span: "0:53-65" span: "0:47-65" doc_comment: " This is a doc comment" "#); assert_yaml_snapshot!(parse_source(r###" #! This is a doc comment from artists derive x = 5 "###).unwrap(), @r#" - VarDef: kind: Main name: main value: Pipeline: exprs: - FuncCall: name: Ident: - from span: "0:34-38" args: - Ident: - artists span: "0:39-46" span: "0:34-46" - FuncCall: name: Ident: - derive span: "0:51-57" args: - Literal: Integer: 5 span: "0:62-63" alias: x span: "0:51-63" span: "0:34-63" span: "0:29-63" doc_comment: " This is a doc comment" "#); assert_debug_snapshot!(parse_source(r###" from artists #! This is a doc comment "###).unwrap_err(), @r#" [ Error { kind: Error, span: Some( 0:18-42, ), reason: Simple( "unexpected #! This is a doc comment\n", ), hints: [], code: None, }, ] "#); } ================================================ FILE: rust-toolchain.toml ================================================ [toolchain] # Generally run 1 behind latest channel = "1.93.1" components = ["rustfmt", "clippy"] # We want two targets: wasm32, and the default target for the platform, which we # don't list here. (i.e. we use each platform to test each platform) targets = ["wasm32-unknown-unknown"] ================================================ FILE: web/.gitignore ================================================ build # the old build dir book/book ================================================ FILE: web/Taskfile.yaml ================================================ # yaml-language-server: $schema=https://json.schemastore.org/taskfile.json version: "3" tasks: build: desc: Build the web products for distribution - Website, Book, and Playground. cmds: - cd website && hugo --minify - cd book && mdbook build - mkdir -p website/public # Copy the book into the website path, using rsync so it only copies new files - rsync -ai --checksum --delete --no-times book/book/ website/public/book/ # (we don't use `build-playground-dependencies`, since that uses the dev profile) - cd playground && npm ci && npm run build # We place the playground app in a nested path, because we want to use # prql-lang.org/playground with an iframe containing the playground. # Possibly there's a more elegant way of doing this... - rsync -ai --checksum --delete --no-times playground/dist/ website/public/playground/playground/ run-website: desc: Build & serve the static website for interactive development. dir: website cmds: - hugo server --bind 0.0.0.0 run-book: desc: Build & serve the book for interactive development. dir: book cmds: - mdbook serve --port=3000 -n 0.0.0.0 run-playground: desc: Build & serve the playground for interactive development. dir: playground env: PROFILE: dev cmds: - task: build-playground-dependencies - npm run dev build-playground-dependencies: # Check if npm dependencies for the playground need to be updated # Use task's sources/generates to see if package.json, # or anything in crates or bindings was updated after the # node_modules was rebuilt desc: Install deps, checking whether a dependency recently changed dir: playground env: PROFILE: dev cmds: - npm ci # Note that now we have `PROFILE: dev`, the build is much much faster, and # we could remove this sources check if it became inconvenient. sources: - package.json - package-lock.json - ../../prqlc/**/*.rs - ../../prqlc/bindings/**/*.rs # These tasks have been factored out in favor of the remaining tasks # run-web: # desc: Build & serve the website & playground. # summary: # Note that this only live-reloads the static website; and requires # rerunning to pick up playground & book changes. # dir: web/website # cmds: # - task: build # # Then start web server with rendering to disk, so it picks up the playground files. # - hugo server --renderToDisk # run-playground-cached: # desc: Build & serve the playground, without rebuilding rust code. # dir: web/playground # cmds: # - task: install-playground-dependencies # - npm start ================================================ FILE: web/book/Cargo.toml ================================================ [package] name = "mdbook-prql" publish = false edition.workspace = true license.workspace = true repository.workspace = true version.workspace = true [lib] bench = false doc = false doctest = false [[bin]] bench = false name = "mdbook-prql" test = false [target.'cfg(not(target_family="wasm"))'.dependencies] ansi-to-html = "0.2.2" anyhow = { workspace = true } itertools = { workspace = true } mdbook-core = { version = "0.5.2", default-features = false } mdbook-preprocessor = { version = "0.5.2", default-features = false } prqlc = { path = "../../prqlc/prqlc", default-features = false } pulldown-cmark = { version = "0.13.0", default-features = false } pulldown-cmark-to-cmark = "22.0.0" serde_json = { workspace = true } strum = { workspace = true } strum_macros = { workspace = true } [target.'cfg(not(target_family="wasm"))'.dev-dependencies] anstream = { version = "1.0.0" } globset = "0.4.18" insta = { workspace = true } log = { workspace = true } regex = "1.12.3" serde_json = { workspace = true } serde_yaml = { workspace = true } similar-asserts = { workspace = true } walkdir = "2.5.0" [package.metadata.release] tag-name = "{{version}}" tag-prefix = "" ================================================ FILE: web/book/README.md ================================================ # PRQL language book These docs serve as a language book, for users of the language. They should be friendly & accessible, at a minimum to those who understand basic SQL. ## Running Install all required PRQL dev tools with: ```sh task setup-dev ``` ...or for the precise cargo command, run `cargo install --locked mdbook`. For the complete build, add any `mdbook` crates listed in the `Taskfile.yaml`. And then to build & serve locally[^1]: ```sh task web:run-book ``` [^1]: ...which is equivalent to: ```sh cd book mdbook serve ``` ================================================ FILE: web/book/book.toml ================================================ [book] description = "Modern language for transforming data — a simple, powerful, pipelined SQL replacement" language = "en" title = "PRQL language book" [output.html] additional-css = ["comparison-table.css"] additional-js = ["highlight-prql.js"] git-repository-url = "https://github.com/PRQL/prql" [preprocessor.prql] # This is required because mdbook-prql isn't necessarily installed; maybe # there's a better way. # command = "cargo run --bin mdbook-prql" [preprocessor.footnote] ================================================ FILE: web/book/comparison-table.css ================================================ /* Styling for Tables */ .comparison { display: flex; flex-wrap: wrap; gap: 1rem; width: 100%; padding-bottom: 3rem; } .comparison > div { flex: 1 0 0; display: flex; flex-direction: column; padding-bottom: 0.5rem; } .comparison h4 { margin: 0; } .comparison pre { margin: 0; flex-grow: 1; } .comparison code { padding: 0.5rem; min-height: calc(100% - 1rem); } ================================================ FILE: web/book/highlight-prql.js ================================================ /* Language: PRQL Description: PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement. Category: common, database Requires: markdown.js Website: https://prql-lang.org/ */ // Syntax highlighting for PRQL. // Keep consistent with // https://github.com/PRQL/prql/blob/main/web/website/themes/prql-theme/static/highlight/prql.js // TODO: can we import one from the other at build time? // Inspired by [Pest's book](https://github.com/pest-parser/book) // mdBook exposes a minified version of highlight.js, so the language // definition objects below have abbreviated property names: // "b" => begin // "e" => end // "c" => contains // "k" => keywords // "cN" => className // TODO: // - Can we represent strings with the actual rule of >= 3 quotes? // - Aliases seem a bit strong? // - Can we represent the inner s & f string items? formatting = function (hljs) { const BUILTIN_FUNCTIONS = [ // Aggregate functions "any", "average", "concat_array", "count", "every", "max", "min", "stddev", "sum", // File reading functions "read_csv", "read_json", "read_parquet", // List functions "all", "map", "zip", "_eq", "_is_null", // Misc functions "from_text", // Window functions "lag", "lead", "first", "last", "rank", "rank_dense", "row_number", ]; const MODULES = ["date", "math", "text"]; const DATATYPES = [ "bool", "float", "int", "int8", "int16", "int32", "int64", "text", "timestamp", ]; const TRANSFORMS = [ "aggregate", "append", "derive", "filter", "from", "group", "join", "select", "sort", "take", "union", "window", ]; const KEYWORDS = ["let", "prql", "into", "case", "in", "as", "module"]; const CHAR_ESCAPE = { scope: "char.escape", match: /\\\\|\\([bfnrt]|u{[0-9A-Fa-f]{1,6}}|x[0-9A-Fa-f]{2})/, }; return { name: "PRQL", case_insensitive: true, keywords: { built_in: BUILTIN_FUNCTIONS, module: MODULES, keyword: [...TRANSFORMS, ...BUILTIN_FUNCTIONS, ...KEYWORDS], literal: "false true null", type: DATATYPES, }, contains: [ { // docblock begin: "#!", end: "$", subLanguage: "markdown", relevance: 0, }, hljs.COMMENT("#", "$"), { // named arg scope: "params", begin: /\w+\s*:/, end: "", relevance: 10, }, { // meta prql for target and version scope: "meta", match: /^prql/, }, // This seems much too strong at the moment, so disabling. I think ideally // we'd have it for aliases but not assigns. // { // // assign // scope: { 1: "variable" }, // match: [/\w+\s*/, /=[^=]/], // relevance: 10, // }, { // date scope: "string", match: /@(\d*|-|\.\d|:|T)+Z?/, relevance: 10, }, { // interval scope: "string", // Add more as needed match: /\d+(years|months|weeks|days|hours|minutes|seconds|milliseconds|microseconds)/, relevance: 10, }, { scope: "string", relevance: 10, variants: [ { begin: 'r"""', end: '"""', }, { begin: 'r"', end: '"', }, ], }, { // interpolation strings: s-strings are variables and f-strings are // strings? (Though possibly that's too cute, open to adjusting) // scope: "variable", relevance: 10, variants: [ { begin: 's"""', end: '"""', }, { begin: 's"', end: '"', }, ], contains: [ // I tried having the `f` / `s` be marked differently, but I don't // think it's possible to have a subscope within the begin / end. { // I think `variable` is the right scope rather than defaulting to // white, but not 100% sure; using `subst` is suggested in the docs. scope: "variable", begin: /\{/, end: /\}/, }, ], }, { scope: "string", relevance: 10, variants: [ { begin: 'f"""', end: '"""', }, { begin: 'f"', end: '"', }, ], contains: [ CHAR_ESCAPE, { scope: "variable", begin: "f", end: '"', // excludesEnd: true, }, // TODO: would be nice to have this be a different color, but I don't // think it's possible to have a subscope within the begin / end. // { // scope: "punctuation", // match: /{|}/, // }, { scope: "variable", begin: /\{/, end: /\}/, }, ], }, { // normal string scope: "string", relevance: 10, variants: [ // TODO: is there a way of encoding the actual rule here? Otherwise // we're just adding the variants we use... { begin: '"""""', end: '"""""', }, { begin: '"""', end: '"""', }, { begin: '"', end: '"', }, { begin: "'", end: "'", }, ], contains: [CHAR_ESCAPE], }, { scope: "punctuation", match: /[\[\]{}(),]/ }, { scope: "operator", match: /==|~=|\+|\-|\/|\*|!=|->|=>|<=|>=|&&|\|\||<|>/, relevance: 10, }, { scope: "number", // Regex explanation: // 1. `\b`: asserts a word boundary. This ensures that the pattern matches numbers that are distinct words or at the boundaries of words. // 2. `(\d[_\d]*(e|E)\d[_\d]*)`: This is the first alternative in the main group and matches numbers in scientific notation: // - `\d`: matches a digit (0-9). // - `[_\d]*`: matches zero or more underscores or digits, representing the numbers before the `e` in scientific notation. // - `(e|E)`: matches the letter 'e' or 'E' for scientific notation. // - `\d`: matches a digit (0-9), the beginning of the exponent. // - `[_\d]*`: matches zero or more underscores or digits, representing the numbers after the `e` in scientific notation. // 3. `(\d[_\d]*|(\d\.[\d_]*\d))`: This is the second alternative in the main group and matches standard numbers without the scientific notation: // - `\d[_\d]*`: matches a sequence starting with a digit and followed by zero or more digits or underscores. // - `|`: OR // - `(\d\.[\d_]*\d)`: matches numbers with a decimal point: // - `\d`: matches the digit(s) before the decimal point. // - `\.`: matches the decimal point. // - `[\d_]*\d`: matches digits after the decimal point, ensuring the sequence ends in a digit and not a trailing underscore. // 4. `(\.[\d_]+)`: This is the third alternative in the main group: // - `\.`: matches a literal dot, so this alternative captures numbers that begin with a decimal point. // - `[\d_]+`: matches one or more digits or underscores, for the sequence after the initial dot. match: /\b((\d[_\d]*(e|E)\d[_\d]*)|(\d[_\d]*|(\d\.[\d_]*\d))|(\.[\d_]+))/, relevance: 10, }, { // range scope: "symbol", match: /\.{2}/, relevance: 10, }, // Unfortunately this just overrides any keywords. It's also not // complete — it only handles functions at the beginning of a line. // I spent several hours trying to get hljs to handle this, but // because there's no recursion, I'm not sure it's possible. // Possibly we could hook into `on:begin` and implement it // ourselves, but this would be a lot of overhead. // { // function // keywords: TRANSFORMS.join(' '), // beginScope: { 1: 'title.function' }, // begin: [/^\s*[a-zA-Z]+/, /(\s+[a-zA-Z]+)+/], // relevance: 10 // }, ], }; }; hljs.registerLanguage("prql", formatting); // These lines should only exists in the book, not the website. // This file is unfortunately inserted after the default highlight.js // invocation, which tags unknown-language blocks with CSS classes but doesn't // highlight them. Array.from(document.querySelectorAll("code.language-prql")).forEach((a) => hljs.highlightElement(a), ); ================================================ FILE: web/book/src/README.md ================================================ # PRQL Language Book **P**ipelined **R**elational **Q**uery **L**anguage, pronounced "Prequel". PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement. Like SQL, it's readable, explicit and declarative. Unlike SQL, it forms a logical pipeline of transformations, and supports abstractions such as variables and functions. It can be used with any database that uses SQL, since it compiles to SQL. This book serves as a tutorial and reference guide on the language and the broader project. It currently has three sections, navigated by links on the left: - **Tutorial** — A friendly & accessible guide for learning PRQL. It has a gradual increase of difficulty and requires only basic understanding of programming languages. Knowledge of SQL is beneficial, because of many comparisons to SQL, but not required. - **Reference** — In-depth information about the PRQL language. Includes justifications for language design decisions and formal specifications for parts of the language. - **Project** — General information about the project, tooling and development. --- **Examples of PRQL** with a comparison to the generated SQL. PRQL queries can be as simple as: ```prql from tracks filter artist == "Bob Marley" # Each line transforms the previous result aggregate { # `aggregate` reduces each column to a value plays = sum plays, longest = max length, shortest = min length, # Trailing commas are allowed } ``` ...and here's a larger example: ```prql from employees filter start_date > @2021-01-01 # Clear date syntax derive { # `derive` adds columns / variables gross_salary = salary + (tax ?? 0), # Terse coalesce gross_cost = gross_salary + benefits, # Variables can use other variables } filter gross_cost > 0 group {title, country} ( # `group` runs a pipeline over each group aggregate { # `aggregate` reduces each group to a value average gross_salary, sum_gross_cost = sum gross_cost, # `=` sets a column name } ) filter sum_gross_cost > 100_000 # `filter` replaces both of SQL's `WHERE` & `HAVING` derive id = f"{title}_{country}" # F-strings like Python derive country_code = s"LEFT(country, 2)" # S-strings permit SQL as an escape hatch sort {sum_gross_cost, -country} # `-country` means descending order take 1..20 # Range expressions (also valid as `take 20`) ``` ================================================ FILE: web/book/src/SUMMARY.md ================================================ # Introduction [Introduction](./README.md) # Tutorial A friendly & accessible guide for learning PRQL. It has a gradual increase of difficulty and requires only basic understanding of programming languages. Knowledge of SQL is beneficial, because of many comparisons to SQL, but not required. - [Relations](./tutorial/relations.md) - [Filtering](./tutorial/filtering.md) - [Aggregation](./tutorial/aggregation.md) # Reference In-depth information about the PRQL language. Includes justifications for language design decisions and formal specifications for parts of the language. - [Syntax](./reference/syntax/README.md) - [Literals](./reference/syntax/literals.md) - [Strings](./reference/syntax/strings.md) - [F-strings](./reference/syntax/f-strings.md) - [R-strings](./reference/syntax/r-strings.md) - [S-strings](./reference/syntax/s-strings.md) - [Tuples](./reference/syntax/tuples.md) - [Arrays](./reference/syntax/arrays.md) - [Identifiers & keywords](./reference/syntax/keywords.md) - [Function calls](./reference/syntax/function-calls.md) - [Pipes](./reference/syntax/pipes.md) - [Operators](./reference/syntax/operators.md) - [Case](./reference/syntax/case.md) - [Ranges](./reference/syntax/ranges.md) - [Comments](./reference/syntax/comments.md) - [Parameters](./reference/syntax/parameters.md) - [Importing data](./reference/data/README.md) - [From](./reference/data/from.md) - [Reading files](./reference/data/read-files.md) - [Ad-hoc data](./reference/data/relation-literals.md) - [Declarations]() - [Variables — `let` & `into`](./reference/declarations/variables.md) - [Functions](./reference/declarations/functions.md) - [Standard library](./reference/stdlib/README.md) - [Transforms](./reference/stdlib/transforms/README.md) - [Aggregate](./reference/stdlib/transforms/aggregate.md) - [Append](./reference/stdlib/transforms/append.md) - [Derive](./reference/stdlib/transforms/derive.md) - [Filter](./reference/stdlib/transforms/filter.md) - [Group](./reference/stdlib/transforms/group.md) - [Join](./reference/stdlib/transforms/join.md) - [Loop](./reference/stdlib/transforms/loop.md) - [Select](./reference/stdlib/transforms/select.md) - [Sort](./reference/stdlib/transforms/sort.md) - [Take](./reference/stdlib/transforms/take.md) - [Window](./reference/stdlib/transforms/window.md) - [Aggregation functions]() - [Date functions](./reference/stdlib/date.md) - [Mathematical functions](./reference/stdlib/math.md) - [Text functions](./reference/stdlib/text.md) - [Removing duplicates](./reference/stdlib/distinct.md) - [Specification](./reference/spec/README.md) - [Null handling](./reference/spec/null.md) - [Name resolution](./reference/spec/name-resolution.md) - [Modules](./reference/spec/modules.md) - [Type system](./reference/spec/type-system.md) # Project General information about the project, tooling and development. - [Changelog](./project/changelog.md) - [Target & version](./project/target.md) - [Bindings](./project/bindings/README.md) - [.NET](./project/bindings/dotnet.md) - [Elixir](./project/bindings/elixir.md) - [Java](./project/bindings/java.md) - [JavaScript](./project/bindings/javascript.md) - [PHP](./project/bindings/php.md) - [Python](./project/bindings/python.md) - [R](./project/bindings/r.md) - [Rust](./project/bindings/rust.md) - [Integrations](./project/integrations/README.md) - [`prqlc CLI`](./project/integrations/prqlc-cli.md) - [ClickHouse](./project/integrations/clickhouse.md) - [Jupyter](./project/integrations/jupyter.md) - [DuckDB](./project/integrations/duckdb.md) - [QStudio](./project/integrations/qstudio.md) - [Prefect](./project/integrations/prefect.md) - [VS Code](./project/integrations/vscode.md) - [PostgreSQL](./project/integrations/postgresql.md) - [Databend](./project/integrations/databend.md) - [Rill](./project/integrations/rill.md) - [Syntax highlighting](./project/integrations/syntax-highlighting.md) - [Contributing to PRQL](./project/contributing/README.md) - [Development](./project/contributing/development.md) - [Language design](./project/contributing/language-design.md) ================================================ FILE: web/book/src/lib.rs ================================================ #![cfg(not(target_family = "wasm"))] use std::str::FromStr; use anyhow::{bail, Result}; use itertools::Itertools; use mdbook_core::book::{Book, BookItem}; use mdbook_preprocessor::{Preprocessor, PreprocessorContext}; use prqlc::compile; use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; use pulldown_cmark_to_cmark::cmark_with_options; use strum::EnumString; pub struct ComparisonPreprocessor; impl Preprocessor for ComparisonPreprocessor { fn name(&self) -> &str { "comparison-preprocessor" } fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result { eprintln!("Running PRQL comparison preprocessor"); book.for_each_mut(|item: &mut BookItem| { if let BookItem::Chapter(chapter) = item { let new = replace_examples(&chapter.content).unwrap(); chapter.content.clear(); chapter.content.push_str(&new); } }); Ok(book) } fn supports_renderer(&self, renderer: &str) -> Result { Ok(renderer == "html") } } #[derive(Debug, PartialEq, Eq, EnumString, strum::Display)] #[strum(serialize_all = "kebab_case")] pub enum LangTag { Prql, // The query either can't be formatted or, after being formatted, it can't // be compiled. NoFmt, // Ignore it, as though it's not PRQL. NoEval, // The query can't be compiled. Error, // Don't test the query. NoTest, #[strum(default)] Other(String), } /// Returns the language of a code block, divided by spaces /// For example: ```prql no-test pub fn code_block_lang_tags(event: &Event) -> Option> { if let Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) = event { Some(lang.split(' ').map(LangTag::from_str).try_collect().ok()?) } else { None } } fn replace_examples(text: &str) -> Result { let mut parser = Parser::new_ext(text, Options::all()); let mut cmark_acc = vec![]; while let Some(event) = parser.next() { // If it's there no tag, or it's not PRQL, or it has `no-eval`, just // push it and continue. let Some(lang_tags) = code_block_lang_tags(&event) else { cmark_acc.push(event.clone()); continue; }; if !lang_tags.contains(&LangTag::Prql) { cmark_acc.push(event.clone()); continue; } for tag in &lang_tags { if matches!(tag, LangTag::Other(_)) { bail!("Unknown code block language: {}", tag) } } if lang_tags.contains(&LangTag::NoEval) { cmark_acc.push(event.clone()); continue; } let Some(Event::Text(text)) = parser.next() else { bail!("Expected text within code block") }; let prql = text.to_string(); let result = compile(&prql, &prqlc::Options::default().no_signature()); if lang_tags.contains(&LangTag::NoTest) { cmark_acc.push(Event::Html(table_of_prql_only(&prql).into())); } else if lang_tags.contains(&LangTag::Error) { // This logic is implemented again, and better, in // [../tests/snapshot.rs], so would be fine to just skip here. let error_message = match result { Ok(sql) => { anyhow::bail!( "Query was labeled to raise an error, but succeeded.\n{prql}\n\n{sql}\n\n" ) } Err(e) => ansi_to_html::convert(e.to_string().as_str()).unwrap(), }; cmark_acc.push(Event::Html(table_of_error(&prql, &error_message).into())) } else { // Show the comparison cmark_acc.push(Event::Html( table_of_comparison( &prql, result .map_err(|e| { anyhow::anyhow!("Query raised an error:\n\n {prql}\n\n{e}\n\n") })? .as_str(), ) .into(), )) } // Skip ending tag parser.next(); } let mut buf = String::new(); let opts = pulldown_cmark_to_cmark::Options::default(); cmark_with_options(cmark_acc.into_iter(), &mut buf, opts)?; Ok(buf) } fn table_of_comparison(prql: &str, sql: &str) -> String { format!( r#"

PRQL

```prql {prql} ```

SQL

```sql {sql} ```
"#, prql = prql.trim(), sql = sql, ) .to_string() } // Similar to `table_of_comparison`, but without a second column. fn table_of_prql_only(prql: &str) -> String { format!( r#"

PRQL

```prql {prql} ```
"#, prql = prql.trim(), ) .to_string() } // Exactly the same as `table_of_comparison`, but with a different title for the second column. fn table_of_error(prql: &str, message: &str) -> String { format!( r#"

PRQL

```prql {prql} ```

Error

{message}
"#, prql = prql.trim(), message = message, ) .to_string() } #[test] fn test_replace_examples() -> Result<()> { use insta::assert_snapshot; // Here we do want colors anstream::ColorChoice::Always.write_global(); let md = r###" # PRQL Doc ```prql from x ``` ```python import sys ``` ```prql error this is an error ``` "###; assert_snapshot!(replace_examples(md)?, md, @r#" # PRQL Doc

PRQL

```prql from x ```

SQL

```sql SELECT * FROM x ```
````python import sys ````

PRQL

```prql this is an error ```

Error

Error:
       ╭─[ :1:1 ]
       
     1 │ this is an error
      ──┬─
        ╰─── Unknown name `this`
    ───╯
    
"#); Ok(()) } #[test] fn test_admonition() -> Result<()> { use insta::assert_snapshot; let md = r#" > [!NOTE] > This is a note. > [!WARNING] > This is a warning. "#; assert_snapshot!(replace_examples(md)?, @r#" > [!NOTE] > This is a note. > [!WARNING] > This is a warning. "#); Ok(()) } #[test] fn test_table() -> Result<()> { use insta::assert_snapshot; let table = r" # Syntax | a | |---| | c | | a | |-----| | \| | "; assert_snapshot!(replace_examples(table)?, @r" # Syntax |a| |-| |c| |a| |-| |\|| "); Ok(()) } ================================================ FILE: web/book/src/main.rs ================================================ // We don't need to run this with wasm, and the features that `mdbook` uses of // `clap`'s don't support wasm. #[cfg(not(target_family = "wasm"))] fn main() { use mdbook_preprocessor::{parse_input, Preprocessor}; use mdbook_prql::ComparisonPreprocessor; use std::io; use std::process; let preprocessor = ComparisonPreprocessor; // Handle the supports subcommand if let Some(arg) = std::env::args().nth(1) { if arg == "supports" { let renderer = std::env::args().nth(2).unwrap_or_else(|| { eprintln!("mdbook-prql: missing renderer argument for 'supports' subcommand"); process::exit(1); }); let supports = preprocessor .supports_renderer(&renderer) .expect("supports_renderer should not fail"); process::exit(if supports { 0 } else { 1 }); } } // Parse input from stdin let (ctx, book) = parse_input(io::stdin()).unwrap_or_else(|e| { eprintln!("mdbook-prql: failed to parse JSON input from mdbook: {}", e); process::exit(1); }); // Run the preprocessor let processed_book = preprocessor.run(&ctx, book).unwrap_or_else(|e| { eprintln!("mdbook-prql: failed to process PRQL code blocks: {}", e); process::exit(1); }); // Serialize and output result let output = serde_json::to_string(&processed_book).unwrap_or_else(|e| { eprintln!("mdbook-prql: failed to serialize book to JSON: {}", e); process::exit(1); }); println!("{}", output); } #[cfg(target_family = "wasm")] fn main() -> ! { panic!("Not used as a binary in wasm (but it seems cargo insists we have a `main` function).") } ================================================ FILE: web/book/src/project/bindings/README.md ================================================ # Bindings PRQL has bindings for many languages. These include: We have three tiers of bindings: - Supported - Unsupported - Nascent ## Supported Supported bindings require: - A maintainer. - Implementations of the [core compile functions](https://docs.rs/prqlc/latest/prqlc/#functions). - Test coverage for these functions. - A published package to the language's standard package repository. - A script in `Taskfile.yaml` to bootstrap a development environment. - Any dev tools, such as a linter & formatter, in pre-commit or MegaLinter. The currently supported bindings are: - [JavaScript](./javascript.md) - [Python](./python.md) - [R](./r.md) - [Rust](./rust.md) Most of these are in the main PRQL repo, and we gate any changes to the compiler's API on compatible changes to the bindings. ## Unsupported Unsupported bindings work, but don't fulfil all of the above criteria. We don't gate changes to the compiler's API. If they stop working, we'll demote them to nascent. - [Java](./java.md) - [Elixir](./elixir.md) - `prqlc-c`, the C bindings ## Nascent Nascent bindings are in development, and may not yet fully work. - [.NET](./dotnet.md) - [PHP](./php.md) ## Naming Over time, we're trying to move to a consistent naming scheme: - Crates are named `prqlc-$lang`. - Where possible, packages are published to each language's package repository as `prqlc`. ================================================ FILE: web/book/src/project/bindings/dotnet.md ================================================ {{#include ../../../../../prqlc/bindings/dotnet/README.md}} ================================================ FILE: web/book/src/project/bindings/elixir.md ================================================ {{#include ../../../../../prqlc/bindings/elixir/README.md}} ================================================ FILE: web/book/src/project/bindings/java.md ================================================ {{#include ../../../../../prqlc/bindings/java/README.md}} ================================================ FILE: web/book/src/project/bindings/javascript.md ================================================ {{#include ../../../../../prqlc/bindings/js/README.md}} ================================================ FILE: web/book/src/project/bindings/php.md ================================================ {{#include ../../../../../prqlc/bindings/php/README.md}} ================================================ FILE: web/book/src/project/bindings/python.md ================================================ {{#include ../../../../../prqlc/bindings/prqlc-python/README.md}} ================================================ FILE: web/book/src/project/bindings/r.md ================================================ # R (prqlr) R bindings for `prqlc`. `prqlr` also includes `knitr` (R Markdown and Quarto) integration, which allows us to easily create documents with the PRQL conversion results embedded in. Check out for more context. > [!NOTE] > `prqlr` is generously maintained by > [@eitsupi](https://github.com/eitsupi) in the > [PRQL/prqlc-r](https://github.com/PRQL/prqlc-r) repo. ## Installation ```r install.packages("prqlr") ``` ================================================ FILE: web/book/src/project/bindings/rust.md ================================================ Please check the documentation of the [prqlc crate](https://docs.rs/prqlc/latest/prqlc/). ================================================ FILE: web/book/src/project/changelog.md ================================================ {{#include ../../../../CHANGELOG.md}} ================================================ FILE: web/book/src/project/contributing/README.md ================================================ # Contributing If you're interested in joining the community to build a better SQL, here are ways to start: - Star the [repo](https://github.com/PRQL/prql). - Send a link to PRQL to a couple of people whose opinion you respect. - Subscribe to [new releases](https://www.jessesquires.com/blog/2020/07/30/github-tip-watching-releases/) for updates. - Follow us on [Twitter](https://twitter.com/prql_lang). - Join our [Discord](https://discord.gg/eQcfaCmsNc). - Find an issue labeled [Good First Issue](https://github.com/prql/prql/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) and start contributing to the code. - Join our [fortnightly Developer Call](https://github.com/PRQL/prql/issues/1083); ([iCal file](./fortnightly-dev-call.ics)). PRQL is evolving from a project with lots of excitement into a project that folks are using in their work and integrating into their tools. We're actively looking for collaborators to lead that growth with us. ## Areas for larger contributions ### Compiler The compiler is written in Rust, and there's enough to do such that any level of experience with Rust is sufficient. We try to keep a few onboarding issues on hand under the ["good first issue" label](https://github.com/PRQL/prql/labels/good%20first%20issue). These have been screened to have sufficient context to get started (and we very much welcome questions where there's some context missing). To get started, check out the docs on [Development](./development.md) and the [Compiler architecture](https://github.com/PRQL/prql/blob/main/prqlc/prqlc/ARCHITECTURE.md) And if you have questions, there are lots of friendly people on the Discord who will patiently help you. ### Bindings & integrations For PRQL to be successful, it needs to be available for the languages & tools that people already use. - We currently have bindings to the PRQL compiler in a few different languages; many of these can be improved, documented, and packaged in a better way. - If you have experience with packaging in an ecosystem that doesn't currently have bindings, then creating PRQL bindings for that language we don't currently support would be valuable to the project. - If there's a tool that you use yourself to run data queries which you think would benefit from a PRQL integration, suggest one to us or the tool. If it's open-source, build & share a prototype. Relevant issues are labeled [Integrations](https://github.com/PRQL/prql/labels/integrations). ### Language design We decide on new language features in GitHub issues, usually under ["language design" label](https://github.com/PRQL/prql/issues?q=is%3Aopen+label%3Alanguage-design+sort%3Aupdated-desc). You can also contribute by: - Finding instances where the compiler produces incorrect results, and post a bug report — feel free to use the [playground](https://prql-lang.org/playground). - Opening an issue / append to an existing issue with examples of queries that are difficult to express in PRQL — especially if more difficult than SQL. With sufficient examples, suggest a change to the language! (Though suggestions _without_ examples are difficult to engage with, so please do anchor suggestions in examples.) ### Marketing - Improve our website. We have [a few issues open](https://github.com/PRQL/prql/labels/web) on this front and are looking for anyone with at least some design skills. - Contribute towards the docs. Anything from shaping a whole section of the docs, to simply improving a confusing paragraph or fixing a typo. - Tell people about PRQL. - Find a group of users who would be interested in PRQL, help them get up to speed, help the project understand what they need. ## Core team If you have any questions or feedback and don't receive a response on one of the general channels such as GitHub or Discord, feel free to reach out to: - [**@aljazerzen**](https://github.com/aljazerzen) — Aljaž Mur Eržen - [**@max-sixty**](https://github.com/max-sixty) — Maximilian Roos - [**@eitsupi**](https://github.com/eitsupi) — SHIMA Tatsuya - [**@snth**](https://github.com/snth) — Tobias Brandt ### Core team Emeritus Thank you to those who have previously served on the core team: - **@charlie-sanders** — Charlie Sanders ================================================ FILE: web/book/src/project/contributing/development.md ================================================ # Development ## Setting up an initial dev environment We can set up a local development environment sufficient for navigating, editing, and testing PRQL's compiler code in two minutes: - Install [`rustup` & `cargo`](https://doc.rust-lang.org/cargo/getting-started/installation.html). - [Optional but highly recommended] Install `cargo-insta`, our testing framework: ```sh cargo install cargo-insta ``` - That's it! Running the unit tests for the `prqlc` crate after cloning the repo should complete successfully: ```sh cargo test --package prqlc --lib ``` ...or, to run tests and update the test snapshots: ```sh cargo insta test --accept --package prqlc --lib ``` There's more context on our tests in [How we test](#how-we-test) below. That's sufficient for making an initial contribution to the compiler. --- ## Setting up a full dev environment > [!NOTE] > We really care about this process being easy, both because the project > benefits from more contributors like you, and to reciprocate your future > contribution. If something isn't easy, please let us know in a GitHub Issue. > We'll enthusiastically help you, and use your feedback to improve the scripts > & instructions. For more advanced development; for example compiling for wasm or previewing the website, we have two options: ### Option 1: Use the project's `task` > [!NOTE] > This is tested on macOS, should work on amd64 Linux, but won't work on > others (include Windows), since it relies on `brew`. - [Install Task](https://taskfile.dev/installation/). - Then run the `setup-dev` task. This runs commands from our [Taskfile.yaml](https://github.com/PRQL/prql/blob/main/Taskfile.yaml), installing dependencies with `cargo`, `brew`, `npm` & `uv`, and suggests some VS Code extensions. ```sh task setup-dev ``` ### Option 2: Install tools individually - We'll need `cargo-insta`, to update snapshot tests: ```sh cargo install cargo-insta ``` - We'll need Python, which most systems will have already. The easiest way to check is to try running the full tests: ```sh cargo test ``` ...and if that doesn't complete successfully, ensure we have Python >= 3.7, to compile `prqlc-python`. - For more involved contributions, such as building the website, playground, book, or some release artifacts, we'll need some additional tools. But we won't need those immediately, and the error messages on what's missing should be clear when we attempt those things. When we hit them, the [Taskfile.yaml](https://github.com/PRQL/prql/blob/main/Taskfile.yaml) will be a good source to copy & paste instructions from. ### Option 3: Use a Dev Container This project has a [devcontainer.json file](https://github.com/PRQL/prql/blob/main/.devcontainer/devcontainer.json) and a [pre-built dev container base Docker image](https://github.com/PRQL/prql/pkgs/container/prql-devcontainer-base). Learn more about Dev Containers at [https://containers.dev/](https://containers.dev/) Currently, the tools for Rust are already installed in the pre-built image, and, Node.js, Python and others are configured to be installed when build the container. While there are a variety of tools that support Dev Containers, the focus here is on developing with VS Code in a container by [GitHub Codespaces](https://docs.github.com/en/codespaces/overview) or [VS Code Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers). To use a Dev Container on a local computer with VS Code, install the [VS Code Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) and its system requirements. Then refer to the links above to get started. ### Option 4: Use nix development environment > [!NOTE] > This is used by a member of the core team on Linux, but doesn't > currently work on Mac. We're open to contributions to improve support. A [nix](https://nixos.org/) flake `flake.nix` provides 3 development environments: - **default**, for building the compiler - **web**, for the compiler and the website, - **full**, for the compiler, the website and the compiler bindings. To load the shell: 1. [Install nix (the package manager)](https://nixos.org/download). (only first time) 2. Enable flakes, which are a (pretty stable) experimental feature of nix. (only first time) For non-NixOS users: ```sh mkdir -p ~/.config/nix/ tee 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf ``` For NixOS users, follow instructions [here](https://nixos.wiki/wiki/Flakes). 3. Run: ```sh nix develop ``` To use the "web" or "full" shell, run: ```sh nix develop .#web ``` Optionally, you can install [direnv](https://direnv.net/), to automatically load the shell when you enter this repo. The easiest way is to also install [direnv-nix](https://github.com/nix-community/nix-direnv) and configure your `.envrc` with: ```sh # .envrc use flake .#full ``` --- ## Contribution workflow We're similar to most projects on GitHub — open a Pull Request with a suggested change! ### Commits - If a change is user-facing, please add a line in [**`CHANGELOG.md`**](https://github.com/PRQL/prql/blob/main/CHANGELOG.md), with `{message}, ({@contributor, #X})` where `X` is the PR number. - If there's a missing entry, a follow-up PR containing just the changelog entry is welcome. - We're using [Conventional Commits](https://www.conventionalcommits.org) message format, enforced through [action-semantic-pull-request](https://github.com/amannn/action-semantic-pull-request). ### Merges - **We merge any code that makes PRQL better** - A PR doesn't need to be perfect to be merged; it doesn't need to solve a big problem. It needs to: - be in the right direction, - make incremental progress, - be explicit on its current state, so others can continue the progress. - That said, there are a few instances when we need to ensure we have some consensus before merging code — for example non-trivial changes to the language, or large refactorings to the library. - If you have merge permissions, and are reasonably confident that a PR is suitable to merge (whether or not you're the author), feel free to merge. - If you don't have merge permissions and have authored a few PRs, ask and ye shall receive. - The primary way we ratchet the code quality is through automated tests. - This means PRs almost always need a test to demonstrate incremental progress. - If a change breaks functionality without breaking tests, our tests were probably insufficient. - If a change breaks existing tests (for example, changing an external API), that indicates we should be careful about merging a change, including soliciting others' views. - We use PR reviews to give general context, offer specific assistance, and collaborate on larger decisions. - Reviews around 'nits' like code formatting / idioms / etc are very welcome. But the norm is for them to be received as helpful advice, rather than as mandatory tasks to complete. Adding automated tests & lints to automate these suggestions is welcome. - If you have merge permissions and would like a PR to be reviewed before it merges, that's great — ask or assign a reviewer. - If a PR hasn't received attention after a day, please feel free to ping the pull request. - People may review a PR after it's merged. As part of the understanding that we can merge quickly, contributors are expected to incorporate substantive feedback into a future PR. - We should revert quickly if the impact of a PR turns out not to be consistent with our expectations, or there isn't as much consensus on a decision as we had hoped. It's very easy to revert code and then re-revert when we've resolved the issue; it's a sign of moving quickly. Other options which resolve issues immediately are also fine, such as commenting out an incorrect test or adding a quick fix for the underlying issue. ## Docs We're very keen on contributions to improve our documentation. This includes our docs in the book, on the website, in our code, or in a Readme. We also appreciate issues pointing out that our documentation was confusing, incorrect, or stale — if it's confusing for you, it's probably confusing for others. Some principles for ensuring our docs remain maintainable: - Docs should be as close as possible to the code. Doctests are ideal on this dimension — they're literally very close to the code and they can't drift apart since they're tested on every commit. Or, for example, it's better to add text to a `--help` message, rather than write a paragraph in the Readme explaining the CLI. - We should have some visualization of how to maintain docs when we add them. Docs have a habit of falling out of date — the folks reading them are often different from those writing them, they're sparse from the code, generally not possible to test, and are rarely the by-product of other contributions. Docs that are concise & specific are easier to maintain. - Docs should be specifically relevant to PRQL; anything else we can instead link to. If something doesn't fit into one of these categories, there are still lots of ways of getting the word out there — a blog post / gist / etc. Let us know and we're happy to link to it / tweet it. ## How we test We use a pyramid of tests — we have fast, focused tests at the bottom of the pyramid, which give us low latency feedback when developing, and then slower, broader tests which ensure that we don't miss anything as PRQL develops{{footnote: Our approach is very consistent with **[@matklad](https://github.com/matklad)**'s advice, in his excellent blog post [How to Test](https://matklad.github.io//2021/05/31/how-to-test.html).}}. > [!NOTE] > If you're making your first contribution, you don't need to engage > with all this — it's fine to just make a change and push the results; the > tests that run in GitHub will point you towards any errors, which can be then > be run locally if needed. We're always around to help out. Our tests, from the bottom of the pyramid to the top: - **[Static checks](https://github.com/PRQL/prql/blob/main/.pre-commit-config.yaml)** — we run a few static checks to ensure the code stays healthy and consistent. They're defined in [**`.pre-commit-config.yaml`**](https://github.com/PRQL/prql/blob/main/.pre-commit-config.yaml), using [pre-commit](https://pre-commit.com). They can be run locally with ```sh task lint # or pre-commit run -a ``` The tests fix most of the issues they find themselves. Most of them also run on GitHub on every commit; any changes they make are added onto the branch automatically in an additional commit. - Checking by [MegaLinter](https://megalinter.io/latest/), which includes more Linters, is also done automatically on GitHub. (experimental) - **Unit tests & inline insta snapshots** — we rely on unit tests to rapidly check that our code basically works. We extensively use [Insta](https://insta.rs/), a snapshot testing tool which writes out the values generated by our code, making it fast & simple to write and modify tests{{footnote: [Here's an example of an insta test](https://github.com/PRQL/prql/blob/0.2.2/prql-compiler/src/parser.rs#L580-L605) — note that only the initial line of each test is written by us; the remainder is filled in by insta.}} These are the fastest tests which run our code; they're designed to run on every save while you're developing. We include a `task` which does this: ```sh task prqlc:test # or cargo insta test --accept --package prqlc --lib ``` - **[Documentation](https://github.com/PRQL/prql/tree/main/web/book/tests/documentation)** — we compile all examples from our documentation in the Website, README, and PRQL Book, to test that they produce the SQL we expect, and that changes to our code don't cause any unexpected regressions. These are included in: ```sh cargo insta test --accept ``` - **[Database integration tests](https://github.com/PRQL/prql/tree/main/prqlc/prqlc/tests/integration/dbs)** — we run tests with example queries against databases with actual data to ensure we're producing correct SQL across our supported dialects. The in-process tests can be run locally with: ```sh task prqlc:test-all # or cargo insta test --accept --features=default,test-dbs ``` More details on running with external databases are in the [Readme](https://github.com/PRQL/prql/tree/main/prqlc/prqlc/tests/integration/dbs). > [!NOTE] > Integration tests use DuckDB, and so require a clang compiler to > compile [`duckdb-rs`](https://github.com/wangfenjin/duckdb-rs). Most > development systems will have one, but if the test command fails, install a > clang compiler with: > > - On macOS, install xcode with `xcode-select --install` > - On Debian Linux, `apt-get update && apt-get install clang` > - On Windows, `duckdb-rs` isn't supported, so these tests are excluded - **[GitHub Actions on every commit](https://github.com/PRQL/prql/blob/main/.github/workflows/tests.yaml)** — we run tests relevant to a PR's changes in CI — for example changes to docs will attempt to build docs, changes to a binding will run that binding's tests. The vast majority of changes trigger tests which run in less than five minutes, and we should be reassessing their scope if they take longer than that. Once these pass, a pull request can be merged. - **[GitHub Actions on merge](https://github.com/PRQL/prql/blob/c042eef48709e2c1af577161554fd09f14e67e0f/.github/workflows/pull-request.yaml#L124)** — we run a wider set tests on every merge to main. This includes testing across OSs, all our language bindings, a measure of test code coverage, and some performance benchmarks. If these tests fail after merging, we should revert the commit before fixing the test and then re-reverting. Most of these will run locally with: ```sh task test-all ``` - **[GitHub Actions nightly](https://github.com/PRQL/prql/blob/main/.github/workflows/nightly.yaml)** — every night, we run tests that take longer, are less likely to fail, or are unrelated to code changes — such as security checks, bindings' tests on multiple OSs, or expensive timing benchmarks. We can run these tests before a merge by adding a label `pr-nightly` to the PR. The goal of our tests is to allow us to make changes quickly. If they're making it more difficult to make changes, or there are missing tests that would offer the confidence to make changes faster, please raise an issue. --- ## Website The website is published together with the book and the playground, and is automatically built and released on any push to the `web` branch. The `web` branch points to the latest release plus any website-specific fixes. That way, the compiler behavior in the playground matches the latest release while allowing us to fix mistakes in the docs with a tighter loop than every release. Fixes to the playground, book, or website should have a `pr-backport-web` label added to their PR — a bot will then open & merge another PR onto the `web` branch once the initial branch merges. The website components will run locally with: ```sh # Run the main website task web:run-website # Run the PRQL online book task web:run-book # Run the PRQL playground task web:run-playground ``` ## Bindings We have a number of language bindings, as documented at . Some of these are within our monorepo, some are in separate repos. Here's a provisional framework for when we use the main prql repo vs separate repos for bindings: | Factor | Rationale | Example | | ------------------------------------------------ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------- | | Does someone want to sign up to maintain a repo? | A different repo is harder for the core team to maintain | `tree-sitter-prql` is well maintained | | Can it change independently from the compiler? | If it's in a different repo, it can't be changed in lockstep with the compiler | `prql-vscode` is fine to change "behind" the language | | Would a separate repo invite new contributors? | A monorepo with all the rust code can be less inviting for those familiar with other langs | `prql-vscode` had some JS-only contributors | | Is there an convention for a stand-alone repo? | A small number of ecosystems require a separate repo | `homebrew-prql` needs to be named that way for a Homebrew tap | --- ## Releasing Currently we release in a semi-automated way: 1. PR & merge an updated [Changelog](https://github.com/PRQL/prql/blob/main/CHANGELOG.md). GitHub will produce a draft version at , including "New Contributors". Use this script to generate a line introducing the enumerated changes: ```sh echo "It has $(git rev-list --count $(git rev-list --tags --max-count=1)..) commits from $(git shortlog --summary $(git rev-list --tags --max-count=1).. | wc -l | tr -d '[:space:]') contributors. Selected changes:" ``` When a fix closes an issue reported by someone other than the PR author, thank them in the changelog entry, e.g. `(@pr-author, #5639; reported by @issue-reporter)`. 2. If the current version is correct, then skip ahead. But if the version needs to be changed — for example, we had planned on a patch release, but instead require a minor release — then run `cargo release version $version -x && cargo release replace -x && task prqlc:test-all` to bump the version, and PR the resulting commit. 3. Ensure all changes intended for the release are merged to `main`. Then create the release (which creates the tag on the latest commit on `main`): **Web UI:** Go to [Draft a new release](https://github.com/PRQL/prql/releases/new){{footnote: Only maintainers have access to this page.}}, copy the changelog entry into the release description{{footnote: Unfortunately GitHub's markdown parser interprets linebreaks as newlines. I haven't found a better way of editing the markdown to look reasonable than manually editing the text or asking LLM to help.}}, enter the tag to be created, and hit "Publish". **CLI:** ```sh gh release create $version --title "$version" --notes "$(cat <<'EOF' EOF )" ``` 4. From there, both the tag and release is created and all packages are published automatically based on our [release workflow](https://github.com/PRQL/prql/blob/main/.github/workflows/release.yaml). 5. Run `cargo release patch --no-publish --no-push --execute --no-verify --no-confirm --no-tag && task prqlc:test-all` to bump the versions and add a new Changelog section; then PR the resulting commit. Note this currently contains `task prqlc:test-all` to update snapshot tests which contain the version. 6. Check whether there are [milestones](https://github.com/PRQL/prql/milestones) that need to be pushed out. 7. Review the **Current Status** on the README.md to ensure it reflects the project state. We may make this more automated in future; e.g. automatic changelog creation. ================================================ FILE: web/book/src/project/contributing/fortnightly-dev-call.ics ================================================ BEGIN:VCALENDAR PRODID:-//Apple Inc.//macOS 12.6.1//EN VERSION:2.0 BEGIN:VEVENT DTSTAMP:20240107T160200 UID:dev-call.prql-lang.org DTSTART;TZID=America/New_York:20240107T133000 DTEND;TZID=America/New_York:20240107T140000 RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=1SU,3SU;UNTIL=20250331 DESCRIPTION:Discord #dev-voice-channel\nhttps://discord.gg/2RUBzz3R3J\n1730Z on 1st & 3rd Sunday LOCATION:https://discord.gg/2RUBzz3R3J\n1730Z SEQUENCE:6 STATUS:CONFIRMED SUMMARY:PRQL Fortnightly Dev Call TRANSP:TRANSPARENT END:VEVENT END:VCALENDAR ================================================ FILE: web/book/src/project/contributing/language-design.md ================================================ # Language design In a way PRQL is just a transpiler to SQL. This can cause its language design to gravitate toward thinking about PRQL features in terms of how they translate to SQL. ``` PRQL feature -> SQL feature -> relational result ``` This is flawed because: - it does not model interactions between features well, - SQL behavior can sometimes be misleading (the order of a subquery will not persist in the parent query) or even differs between dialects (set operations). Instead, we should think of PRQL features in terms of how they affect PRQL expressions, which in most cases means how they affect relations. ``` PRQL feature -> relation | v PRQL feature -> relation | v PRQL feature -> relation | v relational result ``` Thinking about SQL comes in only at the last step when relation (or rather relational expression) is translated to an SQL expression. ================================================ FILE: web/book/src/project/integrations/README.md ================================================ # Integrations PRQL is building integrations with lots of external tools, including: - [`prqlc` CLI](./prqlc-cli.md) - Rust compiler for the command line - [ClickHouse](./clickhouse.md) - [Jupyter](./jupyter.md) - [DuckDB](./duckdb.md) - [QStudio](./qstudio.md) - [Prefect](./prefect.md) - [VS Code](./vscode.md) - [PostgreSQL](./postgresql.md) - [Databend](./databend.md) - [Rill](./rill.md) - [Syntax highlighting](./syntax-highlighting.md) for many popular tools. ================================================ FILE: web/book/src/project/integrations/clickhouse.md ================================================ # ClickHouse PRQL works natively in ClickHouse. Check out the [ClickHouse docs](https://clickhouse.com/docs/en/guides/developer/alternative-query-languages) for more details. ================================================ FILE: web/book/src/project/integrations/databend.md ================================================ # Databend Databend natively supports PRQL. For more details see the [databend docs](https://www.databend.com/blog/2024-04-03-databend-integrates-prql/). ================================================ FILE: web/book/src/project/integrations/duckdb.md ================================================ # DuckDB There's a [DuckDB](https://duckdb.org/) community extension by **[@ywelsch](https://github.com/ywelsch)** at the DuckDB Community Extension Repository. ```sql INSTALL prql FROM community; LOAD prql; -- Once the extension is loaded, you can write PRQL queries from (read_csv 'https://raw.githubusercontent.com/PRQL/prql/0.8.0/prql-compiler/tests/integration/data/chinook/invoices.csv') filter invoice_date >= @2009-02-01 take 5; ``` Check out the [extension's documentation](https://community-extensions.duckdb.org/extensions/prql.html) for more details. ================================================ FILE: web/book/src/project/integrations/jupyter.md ================================================ # Jupyter [pyprql](https://pypi.org/project/pyprql/) contains `pyprql.magic`, a thin wrapper of [`JupySQL`](https://pypi.org/project/jupysql/)'s SQL IPython magics. This allows us to run PRQL interactively on Jupyter/IPython. Check out for more context. ## Installation ```sh pip install pyprql ``` ## Usage When installing pyprql, the [duckdb-engine](https://pypi.org/project/duckdb-engine/) package is also installed with it, so we can start using PRQL immediately to query CSV and Parquet files. For example, running [the example from the JupySQL documentation](https://jupysql.ploomber.io/en/latest/quick-start.html) on IPython: ```python In [1]: %load_ext pyprql.magic In [2]: !curl -sL https://raw.githubusercontent.com/mwaskom/seaborn-data/master/penguins.csv -o penguins.csv In [3]: %prql duckdb:// In [4]: %prql from `penguins.csv` | take 3 Out[4]: species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex 0 Adelie Torgersen 39.1 18.7 181 3750 MALE 1 Adelie Torgersen 39.5 17.4 186 3800 FEMALE 2 Adelie Torgersen 40.3 18.0 195 3250 FEMALE In [5]: %%prql ...: from `penguins.csv` ...: filter bill_length_mm > 40 ...: take 3 ...: ...: Out[5]: species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex 0 Adelie Torgersen 40.3 18.0 195 3250 FEMALE 1 Adelie Torgersen 42.0 20.2 190 4250 None 2 Adelie Torgersen 41.1 17.6 182 3200 FEMALE ``` ================================================ FILE: web/book/src/project/integrations/postgresql.md ================================================ # PostgreSQL PL/PRQL is a PostgreSQL extension that lets you write functions with PRQL. PL/PRQL functions serve as intermediaries, compiling the user's PRQL code into SQL statements that PostgreSQL executes. The extension is based on the [pgrx](https://github.com/pgcentralfoundation/pgrx) framework for developing PostgreSQL extensions in Rust. This framework manages the interaction with PostgreSQL's internal APIs, type conversions, and other function hooks necessary to integrate PRQL with PostgreSQL. ## Examples PL/PRQL functions are defined using the `plprql` language specifier: ```sql create function match_stats(int) returns table(player text, kd_ratio float) as $$ from matches filter match_id == $1 group player ( aggregate { total_kills = sum kills, total_deaths = sum deaths } ) filter total_deaths > 0 derive kd_ratio = total_kills / total_deaths select { player, kd_ratio } $$ language plprql; select * from match_stats(1001) player | kd_ratio ---------+---------- Player1 | 0.625 Player2 | 1.6 (2 rows) ``` You can also run PRQL code directly with the `prql` function which is useful for custom SQL in ORMs: ```sql select prql('from matches | filter player == ''Player1''') as (id int, match_id int, round int, player text, kills int, deaths int) limit 2; id | match_id | round | player | kills | deaths ----+----------+-------+---------+-------+-------- 1 | 1001 | 1 | Player1 | 4 | 1 3 | 1001 | 2 | Player1 | 1 | 7 (2 rows) -- Same as above without the need for the static types, but returns cursor select prql('from matches | filter player == ''Player1''', 'player1_cursor'); fetch 2 from player1_cursor; ``` ## Getting Started For installation instructions and more information on the extension, see the [PL/PRQL repository](https://github.com/kaspermarstal/plprql). ================================================ FILE: web/book/src/project/integrations/prefect.md ================================================ # Prefect Because [Prefect](https://www.prefect.io/) is in native Python, it's extremely easy to integrate with PRQL. With a Postgres Task, replace: ```python PostgresExecute.run(..., query=sql) ``` ...with... ```python PostgresExecute.run(..., query=prql_python.compile(prql)) ``` We're big fans of Prefect, and if there is anything that would make the integration easier, please open an issue. ================================================ FILE: web/book/src/project/integrations/prqlc-cli.md ================================================ {{#include ../../../../../prqlc/prqlc/README.md}} ================================================ FILE: web/book/src/project/integrations/qstudio.md ================================================ # QStudio IDE [QStudio](https://www.timestored.com/qstudio/prql-ide) is a SQL GUI that lets you browse tables, run SQL scripts, and chart and export the results. QStudio runs on Windows, macOS and Linux, and works with every popular database including mysql, postgresql, mssql, kdb.... > [!NOTE] > QStudio relies on the PRQL compiler. You must ensure that `prqlc` is > in your path. See the > [installation instructions](https://prql-lang.org/book/project/integrations/prqlc-cli.html#installation) > for `prqlc` in the PRQL reference guide for details. QStudio calls `prqlc` (the PRQL compiler) to generate SQL code from PRQL queries (.prql files) then runs the SQL against the selected database to display the results. For more details, check out: - [QStudio site](https://www.timestored.com/qstudio/prql-ide) - [QStudio-PRQL Quick Start](https://github.com/richb-hanover/qStudio-PRQL_Quick_Start) - There is a [double-clickable macOS app](https://randomneuronsfiring.com/wp-content/uploads/QStudio.zip) that bundles QStudio and the `prqlc` compiler. ================================================ FILE: web/book/src/project/integrations/rill.md ================================================ # Rill PRQL has had some work to integrate with Rill. See the [Rill issues](https://github.com/PRQL/prql/issues?q=is%3Aissue+rill) for more details. ================================================ FILE: web/book/src/project/integrations/syntax-highlighting.md ================================================ {{#include ../../../../../grammars/README.md}} --- Since the [Elm](https://elm-lang.org/) language coincidentally provides syntax highlighting suitable for PRQL, it may look better to mark PRQL code as Elm when the above definition files are not available. For example, the following Markdown code block will be nicely highlighted on GitHub, Pandoc, and other Markdown renderers: ````markdown ```elm from employees filter start_date > @2021-01-01 ``` ```` We hope that in the future these renderers will recognize PRQL code blocks and have syntax highlighting applied, and we are tracking these with several issues. - GitHub (Linguist): - Pandoc (Kate): ================================================ FILE: web/book/src/project/integrations/vscode.md ================================================ # Visual Studio Code extension PRQL has a Visual Studio Code extension that compiles a PRQL query in a VS Code editor and displays the resulting SQL code in a second pane on the side. This is very handy for editing, saving, and reusing PRQL queries in VS Code. To install the VS Code extension, open VS Code and type Ctrl-Shift-P (Cmd-Shift-P on a Mac) and type `PRQL`. Install the extension as usual. [Repo for the PRQL VS Code extension](https://github.com/PRQL/prql-vscode) [Extension on VS Marketplace](https://marketplace.visualstudio.com/items?itemName=PRQL-lang.prql-vscode) ================================================ FILE: web/book/src/project/target.md ================================================ # Target & Version ## Target dialect PRQL allows specifying a target dialect at the top of the query, which allows PRQL to compile to a database-specific SQL flavor. ### Examples ```prql prql target:sql.postgres from employees sort age take 10 ``` ```prql prql target:sql.mssql from employees sort age take 10 ``` ## Dialects ### Supported Supported dialects support all PRQL language features where possible, are tested on every commit, and we'll endeavor to fix bugs. - `sql.clickhouse` - `sql.duckdb` - `sql.generic` {{footnote: while there's no "generic" DB to test `sql.generic` against, we still count it as supported.}} - `sql.glaredb` - `sql.mysql` - `sql.postgres` - `sql.sqlite` ### Unsupported Unsupported dialects have implementations in the compiler, but are tested minimally or not at all, and may have gaps for some features. We're open to contributions to improve our coverage of these, and to adding additional dialects. - `sql.mssql` - `sql.ansi` - `sql.bigquery` - `sql.snowflake` ## Priority of targets The compile target of a query is defined in the query's header or as an argument to the compiler. option. The argument to the compiler takes precedence. For example, the following shell example specifies `sql.generic` in the query and `sql.duckdb` in the `--target` option of the `prqlc compile` command. In this case, `sql.duckdb` takes precedence and the SQL output is based on the DuckDB dialect. ```sh echo 'prql target:sql.generic from foo' | prqlc compile --target sql.duckdb ``` To use the target described in the query, a special target `sql.any` can be specified in the compiler option. ```sh echo 'prql target:sql.generic from foo' | prqlc compile --target sql.any ``` ## Version PRQL allows specifying a version of the language in the PRQL header, like: ```prql prql version:"0.13.12" from employees ``` This has two roles, one of which is implemented: - The compiler will raise an error if the compiler is older than the query version. This prevents confusing errors when queries use newer features of the language but the compiler hasn't yet been upgraded. - The compiler will compile for the major version of the query. This allows the language to evolve without breaking existing queries, or forcing multiple installations of the compiler. This isn't yet implemented, but is a gating feature for PRQL 1.0. The version of the compiler currently in use can be called using the special function `std.prql.version` in PRQL. ```prql [{version = prql.version}] ``` > [!NOTE] > This function was renamed from `std.prql_version` to `prql.version` in > PRQL 0.11.1. `std.prql_version` will be removed in PRQL 0.12.0. ================================================ FILE: web/book/src/reference/data/README.md ================================================ # Importing data ================================================ FILE: web/book/src/reference/data/from.md ================================================ # From Specifies a data source. ```prql from artists ``` To introduce an alias, use an assign expression: ```prql from e = employees select e.first_name ``` Table names containing spaces or special characters [need to be contained within backticks](../syntax/keywords.md#quoting): ```prql from `artist tracks` ``` `default_db.tablename` can be used if the table name matches a function from the standard library. > [!NOTE] > We realize this is an awkward workaround. Track & 👍 > [#3271](https://github.com/PRQL/prql/issues/3271) for resolving this. ```prql default_db.group # in place of `from group` take 1 ``` ================================================ FILE: web/book/src/reference/data/read-files.md ================================================ # Reading files There are a few functions mainly designed for DuckDB to read from files: ```prql prql target:sql.duckdb from a = (read_parquet "artists.parquet") join b = (read_csv "albums.csv") (a.artist_id == b.artist_id) join c = (read_json "metadata.json") (a.artist_id == c.artist_id) ``` > [!NOTE] > These don't currently have all the DuckDB options. If those would be > helpful, please log an issue and it's a fairly easy addition. > [!NOTE] > We may be able to reduce the boilerplate > `WITH table_x AS SELECT * FROM...` in future versions. When specifying file names directly in the `FROM` clause without using functions, which is allowed in DuckDB, enclose the file names in backticks ` `` ` as follows: ```prql from `artists.parquet` ``` ## See also - [Target and Version](../../project/target.md) - [Ad-hoc data](./relation-literals.md) ================================================ FILE: web/book/src/reference/data/relation-literals.md ================================================ # How do I: create ad-hoc relations? It's often useful to make a small inline relation, for example when exploring how a database will evaluate an expression, or for a small lookup table. This can be quite verbose in SQL. PRQL offers two approaches — array literals, and a `from_text` transform. ## Array literals Because relations (aka a table) in PRQL are just arrays of tuples, they can be expressed with array and tuple syntax: ```prql from [ {a=5, b=false}, {a=6, b=true}, ] filter b == true select a ``` ```prql let my_artists = [ {artist="Miles Davis"}, {artist="Marvin Gaye"}, {artist="James Brown"}, ] from artists join my_artists (==artist) join albums (==artist_id) select {artists.artist_id, albums.title} ``` ## `from_text` `from_text` takes a string in a common format, and converts it to table. It accepts a few formats: - `format:csv` parses CSV (default), - `format:json` parses either: - an array of objects each of which represents a row, or - an object with fields `columns` & `data`, where `columns` take an array of column names and `data` takes an array of arrays. ```prql from_text """ a,b,c 1,2,3 4,5,6 """ derive { d = b + c, answer = 20 * 2 + 2, } ``` ```prql from_text format:json """ [ {"a": 1, "m": "5"}, {"a": 4, "n": "6"} ] """ ``` ```prql from_text format:json """ { "columns": ["a", "b", "c"], "data": [ [1, "x", false], [4, "y", null] ] } """ ``` ## See also - [How do I: read files?](./read-files.md) ================================================ FILE: web/book/src/reference/declarations/README.md ================================================ # Declarations ================================================ FILE: web/book/src/reference/declarations/functions.md ================================================ # Functions Functions have two types of parameters: 1. Positional parameters, which require an argument. 2. Named parameters, which optionally take an argument, otherwise using their default value. So this function is named `fahrenheit_to_celsius` and has one parameter `temp`: ```prql let fahrenheit_to_celsius = temp -> (temp - 32) / 1.8 from cities derive temp_c = (fahrenheit_to_celsius temp_f) ``` The function below is named `interp`, and has two positional parameters named `high` and `x`, and one named parameter named `low` which takes a default argument of `0`. It calculates the proportion of the distance that `x` is between `low` and `high`. ```prql let interp = low:0 high x -> (x - low) / (high - low) from students derive { sat_proportion_1 = (interp 1600 sat_score), sat_proportion_2 = (interp low:0 1600 sat_score), } ``` ## Other examples ```prql let is_adult = col -> col >= 18 let writes_code = col -> (col | in ["PRQL", "Rust"]) let square = col -> (col | math.pow 2) let starts_with_a = col -> (col | text.lower | text.starts_with("a")) from employees select { first_name, last_name, hobby, adult = is_adult age, age_squared = square age, } filter ((starts_with_a last_name) && (writes_code hobby)) ``` ## Piping values into functions Consistent with the principles of PRQL, it's possible to pipe values into functions, which makes composing many functions more readable. When piping a value into a function, the value is passed as an argument to the final positional parameter of the function. Here's the same result as the examples above with an alternative construction: ```prql let interp = low:0 high x -> (x - low) / (high - low) from students derive { sat_proportion_1 = (sat_score | interp 1600), sat_proportion_2 = (sat_score | interp low:0 1600), } ``` and ```prql let fahrenheit_to_celsius = temp -> (temp - 32) / 1.8 from cities derive temp_c = (temp_f | fahrenheit_to_celsius) ``` We can combine a chain of functions, which makes logic more readable: ```prql let fahrenheit_to_celsius = temp -> (temp - 32) / 1.8 let interp = low:0 high x -> (x - low) / (high - low) from kettles derive boiling_proportion = (temp_c | fahrenheit_to_celsius | interp 100) ``` ### Late binding Functions can bind to any variable that is in scope when the function is executed. For example, here `cost_total` refers to the column that's introduced in the `from`. ```prql let cost_share = cost -> cost / cost_total from costs select {materials, labor, overhead, cost_total} derive { materials_share = (cost_share materials), labor_share = (cost_share labor), overhead_share = (cost_share overhead), } ``` ## Partial application Functions can be partially applied, which is useful for creating reusable transform wrappers. When a function returns a partially-applied transform, the missing parameters are automatically propagated. For example, we can create a `top_n` function that wraps `take`: ```prql let top_n = n -> take n from invoices top_n 10 ``` This works because `take` requires two arguments (the count and the relation), but `top_n` only provides one. The relation parameter is automatically filled in from the pipeline. We can also compose multiple partial applications: ```prql let top_n = n -> take n let add_constant = x -> derive {constant = x} let my_pipeline = (top_n 5 | add_constant 42) from invoices my_pipeline ``` Or store a fully-configured transform for reuse: ```prql let top_n = n -> take n let top_5 = top_n 5 from invoices top_5 ``` ================================================ FILE: web/book/src/reference/declarations/variables.md ================================================ # Variables — `let` & `into` Variables assign a name — say `x` — to an expression, like in most programming languages. The name can then be used in any expression, acting as a substitute for the expression `x`. Syntactically, variables can take 3 forms. - `let` declares the name before the expression. ```prql no-eval let my_name = x ``` - `into` declares the name after the expression. This form is useful for quick pipeline splitting and conforms with the "flow from top to bottom" rule of pipelines. ```prql no-eval x into my_name ``` - The final expression of a pipeline defaults to taking the name `main`. ```prql no-eval from x ``` ... is equivalent to: ```prql no-eval let main = x ``` When compiling to SQL, relational variables are compiled to Common Table Expressions (or sub-queries in some cases). ```prql let top_50 = ( from employees sort salary take 50 aggregate {total_salary = sum salary} ) from top_50 # Starts a new pipeline ``` ```prql from employees take 50 into first_50 from first_50 ``` Variables can be assigned an s-string containing the whole SQL query [s-string](../syntax/s-strings.md), enabling us to use features which PRQL doesn't yet support. ```prql let grouping = s""" SELECT SUM(a) FROM tbl GROUP BY GROUPING SETS ((b, c, d), (d), (b, d)) """ from grouping ``` ================================================ FILE: web/book/src/reference/spec/README.md ================================================ # Specification This chapter explains PRQL's semantics: how expressions are interpreted and their meaning. It's intended for advanced users and compiler contributors. ================================================ FILE: web/book/src/reference/spec/modules.md ================================================ # Modules > [!WARNING] > The `module` facility is in discussion. This page documents our > understanding of the way the final PRQL compiler will likely work. The PRQL > compiler currently uses these techniques to compile the `std`, `date`, `text`, > and `math` modules into the language. > > However, at this time (Spring 2024), the `module` facility does not work > within a PRQL query itself. That is, a `module` statement in a query cannot > import files from the local file system. Design goals for **modules**: 1. Allow importing declarations from other files. 2. Have namespaces for things like `std`. 3. Have a hierarchical structure so we can represent files in directories. 4. Have an unambiguous module structure within a project. ## Definition A module is a namespace that contains declarations. A module is itself a declaration, which means that it can contain nested child modules. This means that modules form a [tree graph](), which we call "the module structure". For the sake of this document, we will express the module structure with `module` keyword and a code block encased in curly braces: ``` module my_playlists { let bangers = ... # a declaration module soundtracks { let movie_albums = ... # another declaration } } ``` > The syntax `module name { ...decls... }` is not part of PRQL language, with > the objection that it is unnecessary as it only adds more ways of defining > modules. If a significant upside of this syntax is found, it may be added in > the future. ## Name resolution Any declarations within a module can be referenced from the outside of the module: ```prql no-eval # using module structure declared above module my_playlists let great_tracks = my_playlists.bangers let movie_scores = my_playlists.soundtracks.movie_albums ``` Identifiers are resolved relative to current module. ```prql no-eval module my_playlists { module soundtracks { let movie_albums = (from albums | filter id == 3) } from soundtracks.movie_albums } from my_playlists.soundtracks.movie_albums ``` If an identifier cannot be resolved relative to the current module, it tries to resolve relative to the parent module. This is repeated, stepping up the module hierarchy until a match is found or root of the module structure is reached. ```prql no-eval module my_playlists { let decl_1 = ... module soundtracks { let decl_2 = ... } module upbeat_rock { let decl_3 = ... from decl_1 | join soundtracks.decl2 | join decl_3 } } ``` ## Main var declaration The final variable declaration in a module can omit the leading `let main =` and acquire an implicit name main. ``` module my_playlists { let bangers = (from tracks | take 10) from playlists | take 10 } let album_titles = my_playlists.main ``` When a module is referenced as a value, the `main` variable is used instead. This is especially useful when referring to a module which is to be compiled to RQ (and later SQL). ``` # last line from previous example could thus be shortened to: let album_titles = my_playlists ``` ## File importing > [!WARNING] > The examples below do **not** work. At this time (Spring 2024), the > `module` facility does not work within a PRQL query itself. That is, a > `module` statement in a query cannot import files from the local file system. To include PRQL source code from other files, we can use the following syntax: ``` module my_playlists ``` This loads either `./my_playlists.prql` (a leaf module) or `./my_playlists/_my_playlists.prql` (a directory module) and uses its contents as module `my_playlists`. If none or both of the files are present, a compilation error is raised. Only directory modules can contain module declarations. If a leaf module contains a module declaration, a compilation error is raised, suggesting the leaf module to be converted into a directory module. This is a step toward any module structure having a single "normalized" representation in the file system. Such normalization is desired because it restrains the possible file system layouts to a comprehensible and predictable layout, while not sacrificing any functionality. Described importing rules don't achieve this "single normalized representation" in full, since any leaf modules could be replaced by a directory module with zero submodules, without any semantic changes. Restricting directory modules to have at least one sub-module would not improve approachability enough to justify adding this restriction. For example, the following module structure is annotated with files names in which the modules would reside: ```prql no-eval module my_project { # _my_project.prql module sales { # sales.prql } module projections { # projections/_projections.prql module year_2023 { # projections/year_2023.prql } module year_2024 { # projections/year_2024.prql } } } ``` If module `my_project.sales` wants to add a submodule `util`, it has to be converted to a directory modules. This means that it has to be moved to `sales/_sales.prql`. The new module would reside in `sales/util.prql`. The annotated layout is not the only possible layout for this module structure, since any of the modules `sales`, `year_2023` or `year_2024` could be converted into a directory module with zero sub-modules. Point 4 of design goals means that each declaration within a project has a single fully-qualified name within this project. This is ensured by strict rules regarding importing files and the fact that the module structure is a tree. ## Declaration order The order of declarations in a module holds no semantic value, except the "last `main` variable". References between modules can be cyclic. ``` module mod_a { let decl_a_1 = ... let decl_a_2 = (from mod_b.decl_b | take 10) } module mod_b { let decl_b = (from mod_a.decl_a | take 10) } ``` References between variable declarations cannot be cyclic. ``` let decl_a = (from decl_b) let decl_b = (from decl_a) # error: cyclic reference ``` ``` module mod_a { let decl_a = (from mod_b.decl_b) } module mod_b { let decl_b = (from mod_a.decl_a) # error: cyclic reference } ``` ## Compiler interface `prqlc` provides two interfaces for compiling files. **Multi-file interface** requires three arguments: - path to the file containing the module which is the root of the module structure, - identifier of the pipeline that should be compiled to RQ (this can also be an identifier of a module that has a `main` pipeline) and, - a "file loader", which can load files on-demand. The path to the root module can be automatically detected by searching for `.prql` files starting with `_` in the current working directory. Example prqlc usage: ``` $ prqlc compile _project.prql sales.projections.orders_2024 $ prqlc compile sales.projections.orders_2024 ``` **Single-file interface** requires a single argument; the PRQL source. Any attempts to load modules in this mode result in compilation errors. This interface is needed, for example, when integrating the compiler with a database connector (i.e. JDBC) where no other files can be loaded. ## Built-in module structure > As noted above, this facility is in discussion. ``` # root module of every project module project { module std { let sum = a -> ... let mean = a -> ... } module default_db { # all inferred tables and defined CTEs } let main = ( from t = tracks select [track_id, title] ) } ``` ## Example > [!WARNING] > The examples below do **not** work. At this time (Spring 2024), the > `module` facility does not work within a PRQL query itself. That is, a > `module` statement in a query cannot import files from the local file system. This is an example project, where each of code block is a separate file. ``` # _project.prql module employees module sales module util ``` ``` # employees.prql let employees = (...) let salaries = (...) let departments = (...) ``` ``` # sales/_sales.prql module orders module projections let revenue_by_source = (...) ``` ``` # sales/orders.prql let current_year = (...) let archived = (...) let by_employee = (from orders | join employees.employees ...) ``` ``` # sales/projections.prql let orders_2023 = (from orders.current_year | append orders.archived) let orders_2024 = (...) ``` ``` # util.prql let pretty_print_num = col -> (...) ``` --- Sources: - [Notes On Module System](https://matklad.github.io/2021/11/27/notes-on-module-system.html), by @matklad. ================================================ FILE: web/book/src/reference/spec/name-resolution.md ================================================ # Name resolution Because PRQL primarily handles relational data, it has specialized scoping rules for referencing columns. ## Scopes In PRQL's compiler, a scope is the collection of all names one can reference from a specific point in the program. In PRQL, names in the scope are composed from namespace and variable name which are separated by a dot, similar to SQL. Namespaces can contain many dots, but variable names cannot. > [!NOTE] > For example, name `my_table.some_column` is a variable `some_column` > from namespace `my_table`. > > Name `foo.bar.baz` is a variable `baz` from namespace `foo.bar`. When processing a query, a scope is maintained and updated for each point in the query. It start with only namespace `std`, which is the standard library. It contains common functions like `sum` or `count`, along with all transform functions such as `derive` and `group`. In pipelines (or rather in transform functions), scope is also injected with namespaces of tables which may have been referenced with `from` or `join` transforms. These namespaces contain simply all the columns of the table and possibly a wildcard variable, which matches any variable (see the algorithm below). Within transforms, there is also a special namespace that does not have a name. It is called a _"frame"_ and it contains columns of the current table the transform is operating on. ## Resolving For each ident we want to resolve, we search the scope's items in order. One of three things can happen: - Scope contains an exact match, e.g. a name that matches in namespace and the variable name. - Scope does not contain an exact match, but the ident did not specify a namespace, so we can match a namespace that contains a `*` wildcard. If there's a single namespace, the matched namespace is also updated to contain this new variable name. - Otherwise, the nothing is matched and an error is raised. ## Translating to SQL When translating into an SQL statement which references only one table, there is no need to reference column names with table prefix. ```prql from employees select first_name ``` But when there are multiple tables and we don't have complete knowledge of all table columns, a column without a prefix (i.e. `first_name`) may actually reside in multiple tables. Because of this, we have to use table prefixes for all column names. ```prql from employees derive {first_name, dept_id} join d=departments (==dept_id) select {first_name, d.title} ``` As you can see, `employees.first_name` now needs table prefix, to prevent conflicts with potential column with the same name in `departments` table. Similarly, `d.title` needs the table prefix. ================================================ FILE: web/book/src/reference/spec/null.md ================================================ # Null handling SQL has an unconventional way of handling `NULL` values, since it treats them as unknown values. As a result, in SQL: - `NULL` is not a value indicating a missing entry, but a placeholder for anything possible, - `NULL = NULL` evaluates to `NULL`, since one cannot know if one unknown is equal to another unknown, - `NULL <> NULL` evaluates to `NULL`, using same logic, - to check if a value is `NULL`, SQL introduces `IS NULL` and `IS NOT NULL` operators, - `DISTINCT column` may return multiple `NULL` values. For more information, check out the [Postgres documentation](https://www.postgresql.org/docs/current/functions-comparison.html). PRQL, on the other hand, treats `null` as a value, which means that: - `null == null` evaluates to `true`, - `null != null` evaluates to `false`, - distinct column cannot contain multiple `null` values. ```prql from employees filter first_name == null filter null != last_name ``` Note that PRQL doesn't change how `NULL` is compared between columns, for example in joins. (PRQL compiles to SQL and so can't change the behavior of the database). For more context or to provide feedback check out the discussion on [issue #99](https://github.com/PRQL/prql/issues/99). ================================================ FILE: web/book/src/reference/spec/type-system.md ================================================ # Type system > Status: under development > The type system determines the allowed values of a term. ## Purpose Each of the SQL DBMSs has their own type system. Thanks to the SQL standard, they are very similar, but have key differences regardless. For example, SQLite does not have a type for date or time or timestamps, but it has functions for handling date and time that take ISO 8601 strings or integers that represent Unix timestamps. So it does support most of what is possible to do with dates in other dialects, even though it stores data with a different physical layout and uses different functions to achieve that. PRQL's task is to define it's own description of _data formats_, just as how it already defines common _data transformations_. This is done in two steps: 1. Define PRQL's Type System (PTS), following principles we think a relational language should have (and not fixate on what existing SQL DBMSs have). 2. Define a mapping between SQL Type System (STS) and PTS, for each of the DBMSs. Ideally we'd want that to be a bijection, so each type in PTS would be represented by a single type in STS and vice-versa. Unfortunately this is not entirely possible, as shown below. In practical terms, we want for a user to be able to: - ... express types of their database with PRQL (map their STS into PTS). In some cases, we can allow to say "your database is not representable with PRQL, change it or use only a subset of it". An example of what we don't want to support are arrays with arbitrary indexes in Postgres (i.e. 2-based index for arrays). This task of mapping to PTS could be automated by LSP server, by introspecting user's SQL database and generating PRQL source. - ... express their SQL queries in PRQL. Again, using mapping from STS to PTS, one should be able to express any SQL operation in PRQL. For example, translate MSSQL `DATEDIFF` to subtraction operator `-` in PRQL. For now, this mapping is manual, but should be documented and may be automated. - ... use any PRQL feature in their database. Here we are mapping from PTS into an arbitrary STS. For example, translate PRQL's datetime operations to use TEXT in SQLite. As of now, prqlc already does a good job of automatically doing this mapping. Example of the mapping between PTS and two STSs: | PTS | STS Postgres | STS SQLite | | --------- | ------------ | ---------- | | int32 | integer | INTEGER | | int64 | bigint | INTEGER | | timestamp | timestamp | TEXT | ## Principles **Algebraic types** - have a way of expressing sum and product types. In Rust, sum would be an enum and product would be tuple or a struct. In SQL, product would be a row, since it can contain different types, all at once. Sum would be harder to express, see [this post](https://www.parsonsmatt.org/2019/03/19/sum_types_in_sql.html). The value proposition here is that algebraic types give a lot modeling flexibility, all while being conceptually simple. **Composable** - as with transformation, we'd want types to compose together. Using Python, JavaScript, C++ or Rust, one could define many different data structures that would correspond to our idea of "relation". Most of them would be an object/struct that has column names and types and then a generic array of arrays for rows. PRQL's type system should also be able to express relations as composed from primitive types, but have only one idiomatic way of doing so. In practice, this means that builtin types include only primitives (int, text, bool, float), tuple (for product), enum (for sum) and array (for repeating). An SQL row translates to a tuple, and a relation translates to an array of tuples. Composability also leads to a minimal type system, which does not differentiate between tuples, objects and structs. A single product type is enough. **No subtyping** - avoid super types and inheritance. Subtyping is a natural extension to a type system, where a type can be a super type of some other type. This is base mechanism for Object Oriented Programming, but is also present in most dynamically types languages. For example, a type `number` might be super type of `int` and `float`. PTS does not have subtyping, because it requires dynamic dispatch and because it adds unnecessary complexity to generic type arguments. Dynamic dispatch, is a mechanism that would be able, for example, to call appropriate `to_string` function for each element of an array of `number`. This array contains both elements of type `int` and type `float`, with different `to_string` implementations. ## Definition > For any undefined terms used in this section, refer to set theory and > mathematical definitions in general. A "type of a variable" is a "set of all possible values of that variable". ### Primitives At the moment of writing, PRQL defines following primitive types: `int`, `float`, `bool`, `text`, `date`, `time` and `timestamp`. New primitive types will be added in the future and some of existing types might be split into smaller subsets (see section "Splitting primitives"). ### Tuples Tuple type is a product type. It contains n ordered fields, where n is known at compile-time. Each field has a type itself and an optional name. Fields are not necessarily of the same type. In other languages, similar constructs are named record, struct, tuple, named tuple or (data)class. ``` type my_row = {id = int, bool, name = str} ``` ### Arrays Array is a container type that contains n ordered fields, where n is not known at compile-time. All fields are of the same type and cannot be named. ``` type array_of_int = [int] ``` ### Functions ``` type floor_signature = func float -> int ``` ### Union ``` type status = ( paid = () || unpaid = float || {reason = text, cancelled_at = timestamp} || ) ``` This is "a sum type". ## Type annotations Variable annotations and function parameters may specify type annotations: ``` let a = x ``` The value of `x` (and thus `a`) must be an element of `t`. ``` let my_func = func x -> y ``` The value of argument supplied to `x` must be an element of `t`. ``` let my_func = func x -> y ``` The value of function body `y` must be an element of `t`. ## Physical layout _Logical type_ is user-facing the notion of a type that is the building block of the type system. _Physical layout_ is the underlying memory layout of the data represented by a variable. In many programming languages, physical layout of a logical type is dependent on the target platform. Similarly, physical layout of a PRQL logical type is dependent on representation of that type in the target STS. ``` PTS logical type ---> STS logical type ---> STS physical layout ``` Note that not all STS types do not have a single physical layout. Postgres has a logical (pseudo)type `anyelement`, which is a super type of any data type. It can be used as a function parameter type, but does not have a single physical layout so it cannot be used in a column declaration. For now, PRQL does not define physical layouts of any type. It is not needed since PRQL is not used for DDL (see section "Splitting primitives") or does not support raw access to underlying memory. As a consequence, results of a PRQL query cannot be robustly compared across DBMSs, since the physical layout of the result will vary. In the future, PRQL may define a common physical layout of types, probably using Apache Arrow. ## Examples ``` type my_relation = [{ id = int, title = text, age = int }] type invoices = [{ invoice_id = int64, issued_at = timestamp, labels = [text] #[repr(json)] items = [{ article_id = int64, count = int16 where x -> x >= 1, }], paid_by_user_id = (int64 || null), }] ``` ## Appendix ### Splitting primitives This document mentions `int32` and `int64` as distinct types, but there is no need for that in the initial implementation. The built-in `int` can associate with all operations on integers and translate PRQL to valid SQL regardless of the size of the integer. Later, `int` cam be replaced by `int8`, `int16`, `int32`, `int64`. The general rule for "when to make a distinction between types" would be "as soon as the types carry different information and we find an operation that would be expressed differently". In this example, that would require some operation on `int32` to have different syntax than same operation over `int64`. We can have such relaxed rule because PRQL is not aiming to be a Data Definition Language and does not have to bother with exact physical layout of types. ### Type representations There are cases where a PTS type has multiple possible and valid representations in some STSs. For such cases, we'd want to support the use of alternative representations for storing data, but also application of any function that is defined for the original type. Using SQLite as an example again, users may have some temporal data stored as INTEGER unix timestamp and some as TEXT that contains ISO 8601 without timezone. From the user's perspective, both of these types are `timestamp`s and should be declared as such. But when compiling operations over these types to SQL, the compiler should consider their different representations in STS. For example a difference between two timestamps `timestamp - timestamp` can be translated to a normal int subtraction for INTEGER repr, but must apply SQLite's function `unixepoch` when dealing with TEXT repr. Table declarations should therefore support annotations that give hints about which representation is used: ``` table foo { #[repr(text)] created_at: timestamp, } ``` A similar example is an "array of strings type" in PTS that could be represented by a `text[]` (if DBMS supports arrays) or `json` or it's variant `jsonb` in Postgres. Again, the representation would affect operators: in Postgres, arrays would be accessed with `my_array[1]` and json arrays would use `my_json_array -> 1`. This example may not be applicable, if we decide that we want a separate JSON type in PST. ### RQ functions, targets and reprs > This part is talks about technical implementations, not the language itself #### Idea RQ contains a single node kind for expressing operations and functions: BuiltInFunction (may be renamed in the future). It is a bottleneck that we can leverage when trying to affect how an operator or a function interacts with different type representations on different targets. Idea is to implement the BuiltInFunction multiple times and annotate it with it intended target and parameter representation. Then we can teach the compiler to pick the appropriate function implementation that suit current repr and compilation target. #### Specifics RQ specification is an interface that contains functions, identified by name (i.e. `std.int8.add`). These functions have typed parameters and a return value. If an RQ function call does not match the function declaration in number or in types of the parameters, this is considered an invalid RQ AST. We provide multiple implementations for each RQ function. They are annotated with a target (i.e. `#[target(sql.sqlite)]`) and have their params annotated with type reprs (i.e. `#[repr(int)]`). ``` # using a made-up syntax #[target(sql.sqlite)] func std.int8.add #[repr(int8)] x #[repr(int8)] y -> s"{x} + {y}" ``` Each RQ type has one canonical repr that serves as the reference implementation for other reprs and indicates the amount of contained data (i.e. 1 bit, 8 bits, 64 bits). #### Example Let's say for example, that we'd want to support 8bit integer arithmetic, and that we'd want the result of `127 + 1` to be `-128` (ideally we'd handle this better, but bear with me for the sake of the example). Because some RDBMSs don't support 8bit numbers and do all their integer computation with 64bit numbers (SQLite), we need to implement an alternative type representation for that target. The logical type `int8` could have the following two reprs: - canonical `repr_int8` that contains 8 bits in two's complement, covering integer values in range -128 to 127 (inclusive), - `repr_int64` that contains 64 bits of data, but is using only the values that are also covered by `repr_int8`. Now we'd implement function `std.int8.add` for each of the reprs. Let's assume that the `int8` implementation is straightforward and that databases don't just change the data type when a number overflows. The impl for `int64` requires a CASE statement that checks if the value would overflow and subtact 256 in that case. The goal here is that the results of the two impls are equivalent. To validate that, we also need a way to convert between the reprs, or another `to_string` function, implemented for both reprs. ================================================ FILE: web/book/src/reference/stdlib/README.md ================================================ # Standard library The standard library currently contains commonly used functions that are used in SQL. It's not yet as broad as we'd like, and we're very open to expanding it. Currently s-strings are an escape-hatch for any function that isn't in our standard library. If we find ourselves using them for something frequently, raise an issue and we'll add it to the stdlib. Here's the source of the current [PRQL `std`](https://github.com/PRQL/prql/blob/main/prqlc/prqlc/src/semantic/std.prql): > [!NOTE] > PRQL 0.9.0 has started supporting different DB implementations for > standard library functions. The source is the > [`std.sql`](https://github.com/PRQL/prql/blob/main/prqlc/prqlc/src/sql/std.sql.prql). ```prql no-eval {{#include ../../../../../prqlc/prqlc/src/semantic/std.prql}} ``` And a couple of examples: ```prql from employees derive { gross_salary = (salary + payroll_tax | as int), gross_salary_rounded = (gross_salary | math.round 0), time = s"NOW()", # an s-string, given no `now` function exists in PRQL } ``` Example of different implementations of division and integer division: ```prql prql target:sql.sqlite from [{x = 13, y = 5}] select { quotient = x / y, int_quotient = x // y, } ``` ```prql prql target:sql.mysql from [{x = 13, y = 5}] select { quotient = x / y, int_quotient = x // y, } ``` ================================================ FILE: web/book/src/reference/stdlib/date.md ================================================ # Date functions These are all the functions defined in the `date` module: ### `to_text` Converts a date into a text.\ Since there are many possible date representations, `to_text` takes a `format` parameter that describes thanks to [specifiers](#date--time-format-specifiers) how the date or timestamp should be structured. > [!NOTE] > Since all RDBMS have different ways to format dates and times, PRQL > **requires an explicit dialect** to be specified > [!NOTE] > For now the supported DBs are: BigQuery, Clickhouse, DuckDB, MySQL, MSSQL > and Postgres. ```prql prql target:sql.duckdb from invoices select (invoice_date | date.to_text "%d/%m/%Y") ``` ```prql prql target:sql.postgres from invoices select (invoice_date | date.to_text "%d/%m/%Y") ``` ```prql prql target:sql.mysql from invoices select (invoice_date | date.to_text "%d/%m/%Y") ``` ### Date & time format specifiers PRQL specifiers for date and time formatting is a subset of specifiers used by [`chrono`](https://docs.rs/chrono/latest/chrono/format/strftime/index.html). Here is the list of the specifiers currently supported: | Spec. | Example | Description | | ----- | ----------------------------- | ---------------------------------------------------------------- | | | | | | | | **DATE SPECIFIERS:** | | `%Y` | `2001` | Year number, zero-padded to 4 digits | | `%y` | `01` | Year number, zero-padded to 2 digits | | `%m` | `07` | Month number (01–12), zero-padded to 2 digits | | `%-m` | `7` | Month number (1-12) | | `%b` | `Jul` | Abbreviated month name. Always 3 letters. | | `%B` | `July` | Full month name | | `%d` | `08` | Day number (01-31), zero-padded to 2 digits | | `%-d` | ` 8` | Day number (1-31) | | `%a` | `Sun` | Abbreviated weekday name. Always 3 letters | | `%A` | `Sunday` | Full weekday name | | `%D` | `07/08/01` | Month-day-year format. Same as `%m/%d/%y` | | `%x` | `07/08/01` | Locale's date representation | | `%F` | `2001-07-08` | Year-month-day format (ISO 8601). Same as `%Y-%m-%d` | | | | | | | | **TIME SPECIFIERS:** | | `%H` | `00` | Hour number (00-23) | | `%k` | ` 0` | Same as `%H` but space-padded. Same as `%_H`. | | `%I` | `12` | Hour number in 12-hour clocks (01--12), zero-padded to 2 digits. | | `%p` | `AM` | `AM` or `PM` in 12-hour clocks. | | `%M` | `34` | Minute number (00-59), zero-padded to 2 digits. | | `%S` | `60` | Second number (00-59), zero-padded to 2 digits. | | `%f` | `264900` | Number of microseconds[^1] since last whole second | | `%R` | `00:34` | Hour-minute format. Same as `%H:%M`. | | `%T` | `00:34:60` | Hour-minute-second format. Same as `%H:%M:%S`. | | `%X` | `00:34:60` | Locale's time representation (e.g., 23:13:48). | | `%r` | `12:34:60 AM` | Locale's 12 hour clock time. (e.g., 11:11:04 PM) | | | | | | | | **DATE & TIME SPECIFIERS:** | | `%+` | `2001-07-08T00:34:60.026490Z` | ISO 8601 / RFC 3339 date & time format. | | | | | | | | **SPECIAL SPECIFIERS:** | | `%t` | | Literal tab (`\t`). | | `%n` | | Literal newline (`\n`). | | `%%` | | Literal percent sign. | [^1]: This is different from chrono, for which `%f` represents nanoseconds ================================================ FILE: web/book/src/reference/stdlib/distinct.md ================================================ # How do I: remove duplicates? PRQL doesn't have a specific `distinct` keyword. Instead duplicate tuples in a relation can be removed by using `group` and `take 1`: ```prql from employees select department group employees.* ( take 1 ) ``` This also works with a wildcard: ```prql from employees group employees.* (take 1) ``` ## Remove duplicates from each group? To [select a single row from each group](https://stackoverflow.com/questions/3800551/select-first-row-in-each-group-by-group) `group` can be combined with `sort` and `take`: ```prql # youngest employee from each department from employees group department ( sort age take 1 ) ``` Note that we can't always compile to `DISTINCT`; when the columns in the `group` aren't all the available columns, we need to use a window function: ```prql from employees group {first_name, last_name} (take 1) ``` ================================================ FILE: web/book/src/reference/stdlib/math.md ================================================ # Mathematical functions These are all the functions defined in the `math` module: | function | parameters | description | | -------- | ---------- | ---------------------------------- | | abs | `col` | Absolute value of `col` | | acos | `col` | Arccosine of `col` | | asin | `col` | Arcsine of `col` | | atan | `col` | Arctangent of `col` | | ceil | `col` | Rounds the number up of `col` | | cos | `col` | Cosine of `col` | | degrees | `col` | Converts radians to degrees | | exp | `col` | Exponential of `col` | | floor | `col` | Rounds the number down | | ln | `col` | Natural logarithm of `col` | | log | `b` `col` | `b`-log of `col` | | log10 | `col` | 10-log of `col` | | pi | | The constant π | | pow | `b` `col` | Computes `col` to the power `b` | | radians | `col` | Converts degrees to radians | | round | `n` `col` | Rounds `col` to `n` decimal places | | sin | `col` | Sin of `col` | | sqrt | `col` | Square root of `col` | | tan | `col` | Tangent of `col` | ## Example ```prql from employees select age_squared = (age | math.pow 2) ``` ================================================ FILE: web/book/src/reference/stdlib/text.md ================================================ # Text functions These are all the functions defined in the `text` module: | function | parameters | description | | ----------- | ---------------------- | ----------------------------------------------------------------------------- | | contains | `sub` `col` | Returns true if `col` contains `sub` | | ends_with | `sub` `col` | Returns true if `col` ends with `sub` | | extract | `idx` `len` `col` | Extracts a substring at the index `idx` (starting at 1) with the length `len` | | length | `col` | Returns the number of characters in `col` | | lower | `col` | Converts `col` to lower case | | ltrim | `col` | Removes all the whitespaces from the left side of `col` | | replace | `before` `after` `col` | Replaces any occurrences of `before` with `after` in `col` | | rtrim | `col` | Removes all the whitespaces from the right side of `col` | | starts_with | `sub` `col` | Returns true if `col` starts with `sub` | | trim | `col` | Removes all the whitespaces from both sides of `col` | | upper | `col` | Converts `col` to upper case | ## Example ```prql from employees select { (last_name | text.lower | text.starts_with("a")), (title | text.replace "manager" "chief"), } ``` ================================================ FILE: web/book/src/reference/stdlib/transforms/README.md ================================================ # Transforms Transforms are functions that take a relation and produce a relation. Usually they are chained together into a pipeline, which resembles an SQL query. Transforms were designed with a focus on modularity, so each of them is fulfilling a specific purpose and has defined invariants (properties of the relation that are left unaffected). That's often referred to as "orthogonality" and its goal is to keep transform functions composable by minimizing interference of their effects. Additionally, it also keeps the number of transforms low. For example, `select` and `derive` will not change the number of rows, while `filter` and `take` will not change the number of columns. In SQL, we can see this lack of invariant when an aggregation function is used in the `SELECT` clause. Before, the number of rows was kept constant, but introduction of an aggregation function caused the whole statement to produce only one row (per group). These are the currently available transforms: | Transform | Purpose | SQL Equivalent | | ----------- | ------------------------------------------------------------------------------- | --------------------------- | | `derive` | [Compute new columns](./derive.md) | `SELECT *, ... AS ...` | | `select` | [Pick & compute columns](./select.md) | `SELECT ... AS ...` | | `filter` | [Pick rows based on their values](./filter.md) | `WHERE`, `HAVING`,`QUALIFY` | | `sort` | [Order rows based on the values of columns](./sort.md) | `ORDER BY` | | `join` | [Add columns from another table, matching rows based on a condition](./join.md) | `JOIN` | | `take` | [Pick rows based on their position](./take.md) | `TOP`, `LIMIT`, `OFFSET` | | `group` | [Partition rows into groups and applies a pipeline to each of them](./group.md) | `GROUP BY`, `PARTITION BY` | | `aggregate` | [Summarize many rows into one row](./aggregate.md) | `SELECT foo(...)` | | `window` | [Apply a pipeline to overlapping segments of rows](./window.md) | `OVER`, `ROWS`, `RANGE` | | `loop` | [Iteratively apply a function to a relation until it's empty](./loop.md) | `WITH RECURSIVE ...` | ## See also - [`from`](../../data/from.md) — `from` is the main way of getting data into a pipeline (it's not listed above since it's not technically a transform, since it doesn't receive an input). ================================================ FILE: web/book/src/reference/stdlib/transforms/aggregate.md ================================================ # Aggregate Summarizes many rows into one row. When applied: - without `group`, it produces one row from the whole table, - within a `group` pipeline, it produces one row from each group. ```prql no-eval aggregate {expression or assign operations} ``` > [!NOTE] > Currently, all declared aggregation functions are `min`, `max`, > `count`, `average`, `stddev`, `avg`, `sum` and `count_distinct`. We are in the > process of filling out [std lib](../). ## Examples ```prql from employees aggregate { average salary, ct = count salary } ``` ```prql from employees group {title, country} ( aggregate { average salary, ct = count salary, } ) ``` ## Aggregate is required Unlike in SQL, using an aggregation function in `derive` or `select` (or any other transform except `aggregate`) will not trigger aggregation. By default, PRQL will interpret such attempts functions as window functions: ```prql from employees derive {avg_sal = average salary} ``` This ensures that `derive` does not manipulate the number of rows, but only ever adds a column. For more information, see [window transform](./window.md). ================================================ FILE: web/book/src/reference/stdlib/transforms/append.md ================================================ # Append Concatenates two tables together. Equivalent to `UNION ALL` in SQL. The number of rows is always the sum of the number of rows from the two input tables. To replicate `UNION DISTINCT`, see [set operations](#set-operations). ```prql from employees_1 append employees_2 ``` ## Remove > _experimental_ Removes rows that appear in another relation, like `EXCEPT ALL`. Duplicate rows are removed one-for-one. ```prql from employees_1 remove employees_2 ``` ## Intersection > _experimental_ ```prql from employees_1 intersect employees_2 ``` ## Set operations > _experimental_ To imitate set operations i.e. (`UNION`, `EXCEPT` and `INTERSECT`), you can use the following functions: ```prql no-eval let distinct = rel -> (from t = _param.rel | group {t.*} (take 1)) let union = `default_db.bottom` top -> (top | append bottom | distinct) let except = `default_db.bottom` top -> (top | distinct | remove bottom) let intersect_distinct = `default_db.bottom` top -> (top | intersect bottom | distinct) ``` Don't mind the `default_db.` and `noop`, these are compiler implementation detail for now. ================================================ FILE: web/book/src/reference/stdlib/transforms/derive.md ================================================ # Derive Computes one or more new columns. ```prql no-eval derive { name = expression, # or column, } ``` ## Examples ```prql from employees derive gross_salary = salary + payroll_tax ``` ```prql from employees derive { gross_salary = salary + payroll_tax, gross_cost = gross_salary + benefits_cost } ``` ================================================ FILE: web/book/src/reference/stdlib/transforms/filter.md ================================================ # Filter Picks rows based on their values. ```prql no-eval filter boolean_expression ``` ## Examples ```prql from employees filter age > 25 ``` ```prql from employees filter (age > 25 || department != "IT") ``` ```prql from employees filter (department | in ["IT", "HR"]) ``` ```prql from employees filter (age | in 25..40) ``` ================================================ FILE: web/book/src/reference/stdlib/transforms/group.md ================================================ # Group Partitions the rows into groups and applies a pipeline to each of the groups. ```prql no-eval group {key_columns} (pipeline) ``` The partitioning of groups are determined by the `key_column`s (first argument). The most conventional use of `group` is with `aggregate`: ```prql from employees group {title, country} ( aggregate { average salary, ct = count salary } ) ``` In concept, a transform in context of a `group` does the same transformation to the group as it would to the table — for example finding the employee who joined first across the whole table: ```prql from employees sort join_date take 1 ``` To find the employee who joined first in each department, it's exactly the same pipeline, but within a `group` expression: ```prql from employees group role ( sort join_date # taken from above take 1 ) ``` ================================================ FILE: web/book/src/reference/stdlib/transforms/join.md ================================================ # Join Adds columns from another relation, matching rows based on a condition. ```prql no-eval join side:{inner|left|right|full} rel (condition) ``` ## Parameters - `side` specifies which rows to include, defaulting to `inner`. - `rel` - the relation to join with, possibly including an alias, e.g. `a=artists`. - `condition` - the criteria on which to match the rows from the two relations. Theoretically, `join` will produce a cartesian product of the two input relations and then filter the result by the condition. It supports two additional features: - _Names [`this` & `that`](../../syntax/keywords.md#this--that)_: Along name `this`, which refers to the first input relation, `condition` can use name `that`, which refers to the second input relation. - _Self equality operator_: If the condition is an equality comparison between two columns with the same name (i.e. `(this.col == that.col)`), it can be expressed with only `(==col)`. ## Examples ```prql from employees join side:left positions (employees.id==positions.employee_id) ``` --- ```prql from employees join side:left p=positions (employees.id==p.employee_id) ``` --- ```prql from tracks join side:left artists ( # This adds a `country` condition, as an alternative to filtering artists.id==tracks.artist_id && artists.country=='UK' ) ``` --- In SQL, CROSS JOIN is a join that returns each row from first relation matched with all rows from the second relation. To accomplish this, we can use condition `true`, which will return all rows of the cartesian product of the input relations: ``` from shirts join hats true ``` --- [`this` & `that`](../../syntax/keywords.md#this--that) can be used to refer to the current & other table respectively: ```prql from tracks join side:inner artists ( this.id==that.artist_id ) ``` --- If the join conditions are of form `left.x == right.x`, we can use "self equality operator": ```prql from employees join positions (==emp_no) ``` ================================================ FILE: web/book/src/reference/stdlib/transforms/loop.md ================================================ # Loop > _Experimental_ ```prql no-eval loop {step_function} {initial_relation} ``` Iteratively applies `step` function to `initial` relation until the `step` returns an empty table. Returns a relation that contains rows of initial relation and all intermediate relations. This behavior could be expressed with following pseudo-code: ```python def loop(step, initial): result = [] current = initial while current is not empty: result = append(result, current) current = step(current) return result ``` ## Examples ```prql from [{n = 1}] loop ( filter n<4 select n = n+1 ) # returns [1, 2, 3, 4] ``` > [!NOTE] > The behavior of `WITH RECURSIVE` may depend on the database > configuration in MySQL. The compiler assumes the behavior described by the > [Postgres documentation](https://www.postgresql.org/docs/15/queries-with.html#QUERIES-WITH-RECURSIVE) > and will not produce correct results for > [alternative configurations of MySQL](https://dev.mysql.com/doc/refman/8.0/en/with.html#common-table-expressions-recursive). > [!NOTE] > Currently, `loop` may produce references to the recursive CTE in > sub-queries, which is not supported by some database engines, e.g. SQLite. For > now, we suggest step functions are kept simple enough to fit into a single > SELECT statement. ================================================ FILE: web/book/src/reference/stdlib/transforms/select.md ================================================ # Select Picks and computes columns. ```prql no-eval select { name = expression, # or column, } # or select !{column} ``` ## Examples ```prql from employees select name = f"{first_name} {last_name}" ``` ```prql from employees select { name = f"{first_name} {last_name}", age_eoy = dob - @2022-12-31, } ``` ```prql from employees select first_name ``` ```prql from e=employees select {e.first_name, e.last_name} ``` ### Excluding columns We can use `!` to exclude a list of columns. This can operate in two ways: - We use `SELECT * EXCLUDE` / `SELECT * EXCEPT` for the columns supplied to `select !{}` in dialects which support it. - Otherwise, the columns must have been defined prior in the query (unless all of a table's columns are excluded); for example in another `select` or a `group` transform. In this case, we evaluate and specify the columns that should be included in the output SQL. Some examples: ```prql prql target:sql.bigquery from tracks select !{milliseconds, bytes} ``` ```prql from tracks select {track_id, title, composer, bytes} select !{title, composer} ``` ```prql from artists derive nick = name select !{artists.*} ``` Note that `!` is also the `NOT` operator, so without the tuple it has a different meaning: ```prql prql target:sql.bigquery from tracks select !is_compilation ``` ================================================ FILE: web/book/src/reference/stdlib/transforms/sort.md ================================================ # Sort Order rows based on the values of one or more expressions (generally columns). ```prql no-eval sort {(+|-) column} ``` ## Parameters - One expression or a tuple of expressions to sort by - Each expression can be prefixed with: - `+`, for ascending order, the default - `-`, for descending order - When using prefixes, even a single expression needs to be in a tuple or parentheses. (Otherwise, `sort -foo` is parsed as a subtraction between `sort` and `foo`.) ## Examples ```prql from employees sort age ``` ```prql from employees sort {-age} ``` ```prql from employees sort {age, -tenure, +salary} ``` We can also use expressions: ```prql from employees sort {s"substr({first_name}, 2, 5)"} ``` ## Ordering guarantees Ordering is persistent through a pipeline in PRQL. For example: ```prql from employees sort tenure join locations (==employee_id) ``` Here, PRQL pushes the `sort` down the pipeline, compiling the `ORDER BY` to the _end_ of the query. Consequently, most relation transforms retain the row order. The explicit semantics are: - `sort` introduces a new order, - `group` resets the order, - `join` retains the order of the left relation, - database tables don't have a known order. Comparatively, in SQL, relations possess no order, being orderable solely within the context of the query result, `LIMIT` statement, or window function. The lack of inherent order can result in an unexpected reshuffling of a previously ordered relation from a `JOIN` or windowing operation. > [!NOTE] > To be precise — in PRQL, a relation is an _array of tuples_ and not a > set or a bag. The persistent nature of this order remains intact through > sub-queries and intermediate table definitions. For instance, an SQL query such as: ```sql WITH albums_sorted AS ( SELECT * FROM albums ORDER BY title ) SELECT * FROM albums_sorted JOIN artists USING (artist_id) ``` ...doesn't guarantee any row order (indeed — even without the `JOIN`, the SQL standard doesn't guarantee an order, although most implementations will respect it). ================================================ FILE: web/book/src/reference/stdlib/transforms/take.md ================================================ # Take Picks rows based on their position. ```prql no-eval take (n|range) ``` See [Ranges](../../syntax/ranges.md) for more details on how ranges work. ## Examples ```prql from employees take 10 ``` ```prql from orders sort {-value, created_at} take 101..110 ``` ================================================ FILE: web/book/src/reference/stdlib/transforms/window.md ================================================ # Window Applies a pipeline to segments of rows, producing one output value for every input value. ```prql no-eval window rows:(range) range:(range) expanding:false rolling:0 (pipeline) ``` For each row, the segment over which the pipeline is applied is determined by one of: - `rows`, which takes a range of rows relative to the current row position. - `0` references the current row. - `range`, which takes a range of values relative to current row value. The bounds of the range are inclusive. If a bound is omitted, the segment will extend until the edge of the table or group. For ease of use, there are two flags that override `rows` or `range`: - `expanding:true` is an alias for `rows:..0`. A sum using this window is also known as "cumulative sum". - `rolling:n` is an alias for `rows:(-n+1)..0`, where `n` is an integer. This will include `n` last values, including current row. An average using this window is also knows as a Simple Moving Average. Some examples: | Expression | Meaning | | ---------------- | ------------------------------------------------------------------ | | `rows:0..2` | current row plus two following | | `rows:-2..0` | two preceding rows plus current row | | `rolling:3` | (same as previous) | | `rows:-2..4` | two preceding rows plus current row plus four following rows | | `rows:..0` | all rows from the start of the table up to & including current row | | `expanding:true` | (same as previous) | | `rows:0..` | current row and all following rows until the end of the table | | `rows:..` | all rows, which same as not having window at all | ## Example ```prql from employees group employee_id ( sort month window rolling:12 ( derive {trail_12_m_comp = sum paycheck} ) ) ``` ```prql from orders sort day window rows:-3..3 ( derive {centered_weekly_average = average value} ) group {order_month} ( sort day window expanding:true ( derive {monthly_running_total = sum value} ) ) ``` Rows vs Range: ```prql from [ {time_id=1, value=15}, {time_id=2, value=11}, {time_id=3, value=16}, {time_id=4, value=9}, {time_id=7, value=20}, {time_id=8, value=22}, ] window rows:-2..0 ( sort time_id derive {sma3rows = average value} ) window range:-2..0 ( sort time_id derive {sma3range = average value} ) ``` | time_id | value | sma3rows | sma3range | | ------- | ----- | -------- | --------- | | 1 | 15 | 15 | 15 | | 2 | 11 | 13 | 13 | | 3 | 16 | 14 | 14 | | 4 | 9 | 12 | 12 | | 7 | 20 | 15 | 20 | | 8 | 22 | 17 | 21 | We can see that rows having `time_id` of 5 and 6 are missing in example data; we can say there are gaps in our time series data. When computing SMA 3 for the fifth row (`time_id==7`) then: - "rows" will compute average on 3 rows (`time_id` in `3, 4, 7`) - "range" will compute average on single row only (`time_id==7`) When computing SMA 3 for the sixth row (`time_id==8`) then: - "rows" will compute average on 3 rows (`time_id` in `4, 7, 8`) - "range" will compute average on 2 rows (`time_id` in `7, 8`) We can observe that "rows" ignores the content of the `time_id`, only uses its order; we can say its window operates on physical rows. On the other hand "range" looks at the content of the `time_id` and based on the content decides how many rows fits into window; we can say window operates on logical rows. ## Windowing by default If you use window functions without `window` transform, they will be applied to the whole table. Unlike in SQL, they will remain window functions and will not trigger aggregation. ```prql from employees sort age derive {rnk = rank age} ``` You can also only apply `group`: ```prql from employees group department ( sort age derive {rnk = rank age} ) ``` ## Window functions as first class citizens There are no limitations on where windowed expressions can be used: ```prql from employees filter salary < (average salary) ``` ================================================ FILE: web/book/src/reference/syntax/README.md ================================================ # Syntax A summary of PRQL syntax: | Syntax | Usage | Example | | ---------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------- | | \| | [Pipelines](./function-calls.md) | from employees \| select first_name | | `=` | [Assigns](../declarations/variables.md) | `from e = employees`
`derive total = (sum salary)` | | `:` | [Named args & parameters](../declarations/functions.md) | `interp low:0 1600 sat_score` | | `{}` | [Tuples](./tuples.md) | `{id, false, total = 3}` | | `[]` | [Arrays](./arrays.md) | `[1, 4, 3, 4]` | | `+`,`!`,`&&`,`==`, etc | [Operators](./operators.md) | filter a == b + c \|\| d >= e | | `()` | [Parentheses](./operators.md#parentheses) | `derive celsius = (fht - 32) / 1.8` | | `\` | [Line wrap](./operators.md#wrapping-lines) | 1 + 2 + 3 +
\ 4 + 5 | | `1`,`100_000`,`5e10` | [Numbers](./literals.md#numbers) | `derive { huge = 5e10 * 10_000 }` | | `''`,`""` | [Strings](./literals.md#strings) | `derive name = 'Mary'` | | `true`,`false` | [Booleans](./literals.md#booleans) | `derive { Col1 = true }` | | `null` | [Null](./literals.md#null) | `filter ( name != null )` | | `@` | [Dates & times](./literals.md#date-and-time) | `@2021-01-01` | | `` ` ` `` | [Quoted identifiers](./keywords.md#quoting) | ``select `first name` `` | | `#` | [Comments](./comments.md) | `# A comment` | | `==` | [Self-equality in `join`](../stdlib/transforms/join.md#self-equality-operator) | `join s=salaries (==id)` | | `->` | [Function definitions](../declarations/functions.md) | `let add = a b -> a + b` | | `=>` | [Case statement](./case.md) | `case [a==1 => c, a==2 => d]` | | `+`,`-` | [Sort order](../stdlib/transforms/sort.md) | `sort {-amount, +date}` | | `??` | [Coalesce](./operators.md#coalesce) | `amount ?? 0` | ================================================ FILE: web/book/src/reference/syntax/arrays.md ================================================ # Arrays Array is a container type, composed of multiple items. All items must be of the same type. Number of fields can be vary. > [!WARNING] > This page is a stub. ================================================ FILE: web/book/src/reference/syntax/case.md ================================================ # Case Search for the first condition that evaluates to `true` and return its associated value. If none of the conditions match, `null` is returned. ```prql from employees derive distance = case [ city == "Calgary" => 0, city == "Edmonton" => 300, ] ``` To set a default, a `true` condition can be used: ```prql from employees derive distance = case [ city == "Calgary" => 0, city == "Edmonton" => 300, true => "Unknown", ] ``` ================================================ FILE: web/book/src/reference/syntax/comments.md ================================================ # Comments Character `#` denotes a comment until the end of the line. ```prql from employees # Comment 1 # Comment 2 aggregate {average salary} ``` There's no distinct multiline comment syntax. ================================================ FILE: web/book/src/reference/syntax/f-strings.md ================================================ # F-strings F-strings are a readable approach to building new strings from existing strings & variables. ```prql from employees select full_name = f"{first_name} {last_name}" ``` This can be much easier to read for longer strings, relative to the SQL approach: ```prql from web select url = f"http{tls}://www.{domain}.{tld}/{page}" ``` Note that currently interpolations can only contain plain variable names and not whole expressions like Python, so this won't work: ```prql error no-fmt from tracks select length_str = f"{length_seconds / 60} minutes" ``` ## Roadmap In the future, f-strings may incorporate string formatting such as datetimes, numbers, and padding. If there's a feature that would be helpful, please [post an issue](https://github.com/PRQL/prql/issues/new/). ================================================ FILE: web/book/src/reference/syntax/function-calls.md ================================================ # Function calls ## Simple A distinction between PRQL and most other programming languages is the function call syntax. It consists of the function name followed by arguments separated by whitespace. ```prql no-eval function_name arg1 arg2 arg3 ``` If one of the arguments is also a function call, it must be encased in parentheses, so we know where arguments of inner function end and the arguments of outer function start. ```prql no-eval outer_func arg_1 (inner_func arg_a, arg_b) arg_2 ``` The function name must refer to a function variable, which has either [been declared](../declarations/functions.md) in the [standard library](../stdlib/) or some other module. Function calls can also specify named parameters using `:` notation: ```prql no-eval function_name arg1 named_param:arg2 arg3 ``` ## Pipeline There is a alternative way of calling functions: using a pipeline. Regardless of whether the pipeline is delimited by pipe symbol `|` or a new line, the pipeline is equivalent to applying each of functions as the last argument of the next function. ```prql no-eval a | foo 3 | bar 'hello' 'world' | baz ``` ... is equivalent to ... ```prql no-eval baz (bar 'hello' 'world' (foo 3 a)) ``` ================================================ FILE: web/book/src/reference/syntax/keywords.md ================================================ # Identifiers & keywords Identifiers can contain alphanumeric characters and `_` and must not start with a number. They can be chained together with the `.` lookup operator, used to retrieve a tuple from a field or a variable from a module. ```prql no-eval hello _h3llo hello.world ``` ## `this` & `that` `this` refers to the current relation: ```prql from invoices aggregate ( count this ) ``` Within a [`join`](../stdlib/transforms/join.md), `that` refers to the other table: ```prql from invoices join tracks (this.track_id==that.id) ``` `this` can also be used to remove any column ambiguity. For example, currently using a bare `time` as a column name will fail, because it's also a type: ```prql error no-fmt from invoices derive t = time ``` But with `this.time`, we can remove the ambiguity: ```prql from invoices derive t = this.time ``` ## Quoting To use characters that would be otherwise invalid, identifiers can be surrounded by with backticks. When compiling to SQL, these identifiers will use dialect-specific quotes and quoting rules. ```prql prql target:sql.mysql from employees select `first name` ``` ```prql prql target:sql.postgres from employees select `first name` ``` ```prql prql target:sql.bigquery from `project-foo.dataset.table` join `project-bar.dataset.table` (==col_bax) ``` ## Schemas & database names Identifiers of database tables can be prefixed with schema and databases names. ```prql from my_database.chinook.albums ``` Note that all of following identifiers will be treated as separate table definitions: `tracks`, `public.tracks`, `my_database.public.tracks`. ## Keywords PRQL uses following keywords: - **`prql`** - query header [_more..._](../../project/target.md) - **`let`** - variable definition [_more..._](../declarations/variables.md) - **`into`** - variable definition [_more..._](../declarations/variables.md) - **`case`** - flow control [_more..._](../syntax/case.md) - **`type`** - type declaration - **`func`** - explicit function declaration [_more..._](../declarations/functions.md) - **`module`** - used internally - **`internal`** - used internally - **`true`** - boolean [_more..._](./literals.md#booleans) - **`false`** - boolean [_more..._](./literals.md#booleans) - **`null`** - NULL [_more..._](./literals.md#null) Keywords can be used as identifiers (of columns or variables) when encased in backticks: `` `case` ``. Transforms are normal functions within the `std` namespace, not keywords. That is, `std.from` is the same function as `from`. In the example below, the resulting query is the same as without the `std.` namespace: ```prql std.from my_table std.select {from = my_table.a, take = my_table.b} std.take 3 ``` ================================================ FILE: web/book/src/reference/syntax/literals.md ================================================ # Literals A literal is a constant value expression, with special syntax rules for each data type. ## Numbers Number literals can contain number characters as well as a period, underscores and char `e`. If a number literal contains a dot or character `e`, it is treated as floating point number (or just _float_), otherwise it is treated as integer number. Character `e` denotes ["scientific notation"](https://en.wikipedia.org/wiki/Scientific_notation), where the number after `e` is the exponent in 10-base. Underscores are ignored, so they can be placed at arbitrary positions, but it is advised to use them as thousand separators. Integers can, alternatively, be expressed using hexadecimal, octal or binary notation using these prefixes respectively: `0x`, `0o` or `0b`. ```prql from numbers select { small = 1.000_000_1, big = 5_000_000, huge = 5e9, binary = 0b0011, hex = 0x80, octal = 0o777, } ``` ## Strings PRQL supports string literals and several other formats of strings. See the [Strings](./strings.md) page for more information. ## Booleans Boolean values can be expressed with `true` or `false` keyword. ## Null The null value can be expressed with `null` keyword. See also the discussion of how [PRQL handles nulls](../spec/null.md). ## Date and time Date and time literals are expressed with character `@`, followed by a string that encodes the date & time. > [!NOTE] > PRQL's notation is designed to be less verbose than SQL's > `TIMESTAMP '2004-10-19 10:23:54'` and more explicit than SQL's implicit option > that just uses a string `'2004-10-19 10:23:54'`. ### Dates Dates are represented by `@{yyyy-mm-dd}` — a `@` followed by the date format. ```prql from employees derive age_at_year_end = (@2022-12-31 - dob) ``` ### Times Times are represented by `@{HH:mm:ss.SSS±Z}` with any parts not supplied defaulting to zero. This includes the timezone, which is represented by `+HH:mm`, `-HH:mm` or `Z`. This is consistent with the ISO8601 time format. ```prql from orders derive should_have_shipped_today = (order_time < @08:30) ``` ### Timestamps Timestamps are represented by `@{yyyy-mm-ddTHH:mm:ss.SSS±Z}` / `@{date}T{time}`, with any time parts not supplied being rounded to zero, including the timezone, which is represented by `+HH:mm`, `-HH:mm` or `Z` (`:` is optional). This is `@` followed by the ISO8601 datetime format, which uses `T` to separate date & time. ```prql from commits derive first_prql_commit = @2020-01-01T13:19:55-08:00 derive first_prql_commit_utc = @2020-01-02T21:19:55Z ``` ### Durations Durations are represented by `{N}{periods}`, such as `2years` or `10minutes`, without a space. > [!NOTE] > These aren't the same as ISO8601, because we evaluated > `P3Y6M4DT12H30M5S` to be difficult to understand, but we could support a > simplified form if there's demand for it. We don't currently support compound > expressions, for example `2years10months`, but most DBs will allow > `2years + 10months`. Please raise an issue if this is inconvenient. ```prql from projects derive first_check_in = start + 10days ``` ### Examples Here's a larger list of date and time examples: - `@20221231` is invalid — it must contain full punctuation (`-` and `:`), - `@2022-12-31` is a date - `@2022-12` or `@2022` are invalid — SQL can't express a month, only a date - `@16:54:32.123456` is a time - `@16:54:32`, `@16:54`, `@16` are all allowed, expressing `@16:54:32.000000`, `@16:54:00.000000`, `@16:00:00.000000` respectively - `@2022-12-31T16:54:32.123456` is a timestamp without timezone - `@2022-12-31T16:54:32.123456Z` is a timestamp in UTC - `@2022-12-31T16:54+02` is timestamp in UTC+2 - `@2022-12-31T16:54+02:00` and `@2022-12-31T16:54+02` are datetimes in UTC+2 - `@16:54+02` is invalid — time is always local, so it cannot have a timezone - `@2022-12-31+02` is invalid — date is always local, so it cannot have a timezone > [!NOTE] > Currently prqlc does not parse or validate any of the datetime strings > and will pass them to the database engine without adjustment. This might be > refined in the future to aid in compatibility across databases. We'll always > support the canonical [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) format > described above. ### Roadmap Datetimes (as a distinct datatype from the timestamps) are supported by some databases (e.g. MySql, BigQuery). With the addition of type casts, these could be represented by a timestamp cast to a datetime: ```prql no-eval derive pi_day = @2017-03-14T15:09:26.535898 ``` These are some examples we can then add: - `@2022-12-31T16:54` is datetime without timezone - `@2022-12-31` is forbidden — datetime must specify time - `@16:54` is forbidden — datetime must specify date ================================================ FILE: web/book/src/reference/syntax/operators.md ================================================ # Operators Expressions can be composed from _function calls_ and _operations_, such as `2 + 3` or `((1 + x) * -y)`. In the example below, note the use of expressions to calculate the alias `circumference` and in the `filter` transform. ```prql from foo select { circumference = diameter * 3.14159, area = (diameter / 2) ** 2, color, } filter circumference > 10 && color != "red" ``` ## Operator precedence This table shows operator precedence. Use parentheses `()` to prioritize operations and for function calls (see the discussion below.) | Group | Operators | Precedence | Associativity | | -------------: | --------------------------- | :--------: | :-----------: | | parentheses | `()` | 0 | see below | | identifier dot | `.` | 1 | | | unary | `-` `+` `!` `==` | 2 | | | range | `..` | 3 | | | pow | `**` | 4 | right-to-left | | mul | `*` `/` `//` `%` | 5 | left-to-right | | add | `+` `-` | 6 | left-to-right | | compare | `==` `!=` `<=` `>=` `<` `>` | 7 | left-to-right | | coalesce | `??` | 8 | left-to-right | | and | `&&` | 9 | left-to-right | | or | \|\| | 10 | left-to-right | | function call | | 11 | | ## Division and integer division The `/` operator performs division that always returns a float value, while the `//` operator does integer division (truncated division) that always returns an integer value. ```prql prql target:sql.sqlite from [ {a = 5, b = 2}, {a = 5, b = -2}, ] select { div_out = a / b, int_div_out = a // b, } ``` ## Coalesce We can coalesce values with an `??` operator. Coalescing takes either the first value or, if that value is null, the second value. ```prql from orders derive amount ?? 0 ``` ## Regex expressions > [!NOTE] > This is currently experimental To perform a case-sensitive regex search, use the `~=` operator. This generally compiles to `REGEXP`, though differs by dialect. A regex search means that to match an exact value, the start and end need to be anchored with `^foo$`. ```prql from tracks filter (name ~= "Love") ``` ```prql prql target:sql.duckdb from artists filter (name ~= "Love.*You") ``` ```prql prql target:sql.bigquery from tracks filter (name ~= "\\bLove\\b") ``` ```prql prql target:sql.postgres from tracks filter (name ~= "\\(I Can't Help\\) Falling") ``` ```prql prql target:sql.mysql from tracks filter (name ~= "With You") ``` ```prql prql target:sql.sqlite from tracks filter (name ~= "But Why Isn't Your Syntax More Similar\\?") ``` ## Parentheses PRQL uses parentheses `()` for several purposes: - Parentheses group operands to control the order of evaluation, for example: `((1 + x) * y)` - Parentheses delimit a minus sign of a function argument, for example: `add (-1) (-3)` - Parentheses delimit nested function calls that contain a pipe, either the `|` symbol or a new line. “Nested” means within a transform; i.e. not just the main pipeline, for example: `(column-name | in 0..20)` - Parentheses wrap a function call that is part of a larger expression, for example: `math.round 0 (sum distance)` Parentheses are _not_ required for expressions that do not contain function calls, for example: `foo + bar`. Here's a set of examples of these rules: ```prql from employees # Requires parentheses, because it contains a pipe derive is_proximate = (distance | in 0..20) # Requires parentheses, because it's a function call derive total_distance = (sum distance) # `??` doesn't require parentheses, as it's not a function call derive min_capped_distance = (min distance ?? 5) # No parentheses needed, because no function call derive travel_time = distance / 40 # No inner parentheses needed around `1+1` because no function call derive distance_rounded_2_dp = (math.round 1+1 distance) derive { # Requires parentheses, because it contains a pipe is_far = (distance | in 100..), # The left value of the range requires parentheses, # because of the minus sign is_negative = (distance | in (-100..0)), # ...this is equivalent is_negative = (distance | in (-100)..0), # _Technically_, this doesn't require parentheses, because it's # the RHS of an assignment in a tuple # (this is especially confusing) average_distance = average distance, } # Requires parentheses because of the minus sign sort (-distance) # A tuple is fine too sort {-distance} ``` For example, the snippet below produces an error because the `sum` function call is not in a tuple. ```prql error no-fmt from employees derive total_distance = sum distance ``` ...while with parentheses, it works at expected: ```prql from employees derive other_distance = (sum distance) ``` > [!NOTE] > We're continuing to think whether these rules can be more intuitive. > We're also planning to make the error messages much better, so the compiler > can help out. ## Wrapping lines Line breaks in PRQL have semantic meaning, so to wrap a single logical line into multiple physical lines, we can use `\` at the beginning of subsequent physical lines: ```prql from artists select is_europe = \ country == "DE" \ || country == "FR" \ || country == "ES" ``` Wrapping will "jump over" empty lines or lines with comments. For example, the `select` here is only one logical line: ```prql from tracks # This would be a really long line without being able to split it: select listening_time_years = (spotify_plays + apple_music_plays + pandora_plays) # We can toggle between lines when developing: # \ * length_seconds \ * length_s # min hour day year \ / 60 / 60 / 24 / 365 ``` > [!NOTE] > Note that PRQL differs from most languages, which use a `\` at the > _end_ of the preceding line. Because PRQL aims to be friendly for data > exploration, we want to make it possible to comment out any line, including > the final line, without breaking the query. This requires all lines after the > first to be structured similarly, and for the character to be at the start of > each following line. See [Pipes](./pipes.md) for more details on line breaks. ================================================ FILE: web/book/src/reference/syntax/parameters.md ================================================ # Parameters Parameter is a placeholder for a value provided after the compilation of the query. It uses the following syntax: `$id`, where `id` is an arbitrary alpha numeric string. Most database engines only support numeric positional parameter ids (i.e `$3`). ```prql from employees filter id == $1 ``` ================================================ FILE: web/book/src/reference/syntax/pipes.md ================================================ # Pipes Pipes are the connection between [transforms](../stdlib/transforms/) that make up a pipeline. The relation produced by a transform before the pipe is used as the input for the transform following the pipe. A pipe can be represented with either a line break or a pipe character (`|`). For example, here the `filter` transform operates on the result of `from employees` (which is just the `employees` table), and the `select` transform operates on the result of the `filter` transform. ```prql from employees filter department == "Product" select {first_name, last_name} ``` In the place of a line break, it's also possible to use the `|` character to pipe results between transforms, such that this is equivalent: ```prql from employees | filter department == "Product" | select {first_name, last_name} ``` ## "Ceci n'est pas une pipe" In almost all situations, a line break acts as a pipe. But there are a few cases where a line break doesn't act as a pipe. - before or after tuple items - before or after list items - before a new statement, which starts with `let` or `from` (or `func`) - within a [line wrap](./operators.md#wrapping-lines) For example: ```prql [ {a=2} # No pipe from line break before & after this list item ] derive { c = 2 * a, # No pipe from line break before & after this tuple item } ``` ```prql let b = \ 3 # No pipe from line break within this line wrap # No pipe from line break before this `from` statement from y derive a = b ``` ## Inner Transforms Parentheses are also used for transforms (such as `group` and `window`) that pass their result to an "inner transform". The example below applies the `aggregate` pipeline to each group of unique `title` and `country` values: ```prql from employees group {title, country} ( aggregate { average salary, ct = count salary, } ) ``` ================================================ FILE: web/book/src/reference/syntax/r-strings.md ================================================ # R-strings R-strings handle escape characters without special treatment: ```prql from artists derive normal_string = "\\\t" # two characters - \ and tab (\t) derive raw_string = r"\\\t" # four characters - \, \, \, and t ``` ================================================ FILE: web/book/src/reference/syntax/ranges.md ================================================ # Ranges Range `start..end` represents as set of values between `start` and `end`, inclusive (greater of equal to `start` and less than or equal to `end`). To express a range that is open on one side, either `start` or `end` can be omitted. Ranges can be used in filters with the `in` function, with any type of literal, including dates: ```prql from events filter (created_at | in @1776-07-04..@1787-09-17) filter (magnitude | in 50..100) derive is_northern = (latitude | in 0..) ``` Ranges can also be used in `take`: ```prql from orders sort {-value, created_at} take 101..110 ``` > [!NOTE] > Half-open ranges are generally less intuitive to read than a simple > `>=` or `<=` operator. ## See also - [take transform](../stdlib/transforms/take.md) ## Roadmap We'd like to use ranges for other types, such as whether an object is in an array or list literal. ================================================ FILE: web/book/src/reference/syntax/s-strings.md ================================================ # S-strings An s-string inserts SQL directly, as an escape hatch when there's something that PRQL doesn't yet implement. For example, there's a `version()` function in PostgreSQL that returns the PostgreSQL version, so if we want to use that, we use an s-string: ```prql from my_table select db_version = s"version()" ``` Embed a column name in an s-string using braces. For example, PRQL's standard library defines the `average` function as: ```prql no-eval let average = column -> s"AVG({column})" ``` So this compiles using the function: ```prql from employees aggregate {average salary} ``` > [!NOTE] > Because S-string contents are SQL, double-quotes (`"`) will denote a > _column name_. To avoid that, use single-quotes (`'`) around the SQL string, > and adjust the quotes of the S-string. For example, instead of > `s'CONCAT("hello", "world")'` use `s"CONCAT('hello', 'world')"` Here's an example of a more involved use of an s-string: ```prql from de=dept_emp join s=salaries side:left (s.emp_no == de.emp_no && s""" ({s.from_date}, {s.to_date}) OVERLAPS ({de.from_date}, {de.to_date}) """) ``` For those who have used Python, s-strings are similar to Python's f-strings, but the result is SQL code, rather than a string literal. For example, a Python f-string of `f"average({col})"` would produce `"average(salary)"`, with quotes; while in PRQL, `s"average({col})"` produces `average(salary)`, without quotes. Note that interpolations can only contain plain variable names and not whole expression like Python. We can also use s-strings to produce a full table: ```prql from s"SELECT DISTINCT ON first_name, id, age FROM employees ORDER BY age ASC" join s = s"SELECT * FROM salaries" (==id) ``` > [!NOTE] > S-strings in user code are intended as an escape hatch for an > unimplemented feature. If we often need s-strings to express something, that's > a sign we should implement it in PRQL or PRQL's stdlib. If you often require > an s-string, > [submit an issue with your use case](https://github.com/PRQL/prql/issues/new/choose). ## Braces To output braces from an s-string, use double braces: ```prql from employees derive { has_valid_title = s"regexp_contains(title, '([a-z0-9]*-){{2,}}')" } ``` ## Precedence within s-strings Variables in s-strings are inserted into the SQL source as-is, which means we may get surprising behavior when the variable has multiple terms and the s-string isn't parenthesized. In this toy example, the expression `salary + benefits / 365` gets precedence wrong. The generated SQL code is as if we had written `salary + (benefits / 365)`. ```prql from employees derive { gross_salary = salary + benefits, daily_rate = s"{gross_salary} / 365" } ``` Instead, the numerator `{gross_salary}` must be encased in parentheses: ```prql from employees derive { gross_salary = salary + benefits, daily_rate = s"({gross_salary}) / 365" } ``` ================================================ FILE: web/book/src/reference/syntax/strings.md ================================================ # Strings String literals can use any matching odd number of either single or double quotes: ```prql from artists derive { single = 'hello world', double = "hello world", double_triple = """hello world""", } ``` ## Quoting and escape characters To quote a string containing quote characters, use the "other" type of quote, or use the escape character `\`, or use more quotes. ```prql from artists select { other = '"hello world"', escaped = "\"hello world\"", triple = """I said "hello world"!""", } ``` Strings can contain any escape character sequences defined by the [JSON standard](https://www.ecma-international.org/publications-and-standards/standards/ecma-404/). ```prql from artists derive escapes = "\tXYZ\n \\ " # tab (\t), "XYZ", newline (\n), " ", \, " " derive world = "\u{0048}\u{0065}\u{006C}\u{006C}\u{006F}" # "Hello" derive hex = "\x48\x65\x6C\x6C\x6F" # "Hello" derive turtle = "\u{01F422}" # "🐢" ``` ## Other string formats - [**F-strings**](./f-strings.md) - Build up a new string from a set of columns or values. - [**R-strings**](./r-strings.md) - Include the raw characters of the string without any form of escaping. - [**S-strings**](./s-strings.md) - Insert SQL statements directly into the query. Use when PRQL doesn't have an equivalent facility. > [!WARNING] > Currently PRQL allows multiline strings with either a single > character or multiple character quotes. This may change for strings using a > single character quote in future versions. > [!NOTE] > These escape rules specify how PRQL interprets escape characters when > compiling strings to SQL, not necessarily how the database will interpret the > string. Dialects interpret escape characters differently, and PRQL doesn't > currently account for these differences. Please open issues with any > difficulties in the current implementation. ## Escape sequences Unless an `r` prefix is present, escape sequences in string literals are interpreted according to rules similar to those used by Standard C. The recognized escape sequences are: | Escape Sequence | Meaning | | --------------- | ----------------------------- | | `\\` | Backslash (\) | | `\'` | Single quote (') | | `\"` | Double quote (") | | `\b` | Backspace | | `\f` | Formfeed | | `\n` | ASCII Linefeed (LF) | | `\r` | ASCII Carriage Return (CR) | | `\t` | ASCII Horizontal Tab (TAB) | | `\xhh` | Character with hex value hh | | `\u{xxxx}` | Character with hex value xxxx | ================================================ FILE: web/book/src/reference/syntax/tuples.md ================================================ # Tuples Tuple is a container type, composed of multiple fields. Each field can have a different type. Number of fields and their types must be known at compile time. Tuple is represented by `{}`. It can span multiple lines. Fields can be assigned a name. Fields are separated by commas, trailing trailing comma is optional. ```prql no-eval let var1 = {x = 1, y = 2} let var2 = { # Span multiple lines a = x, b = y # Optional trailing comma } let var3 = { c, # Individual item d = b, # Assignment } ``` Tuples are the type of a table row, which means that they are expected by many transforms. Most transforms can also take a single field, which will be converted into a tuple. These are equivalent: ```prql from employees select {first_name} ``` ```prql from employees select first_name ``` > [!NOTE] > Prior to `0.9.0`, tuples were previously named Lists, and represented > with `[]` syntax. There may still be references to the old naming. ================================================ FILE: web/book/src/tutorial/aggregation.md ================================================ # Aggregation A key feature of analytics is reducing many values down to a summary. This act is called "aggregation" and always includes a function — for example, `average` or `sum` — that reduces values in the table to a single row. ### `aggregate` transform The `aggregate` transform takes a tuple to create one or more new columns that "distill down" data from all the rows. ```prql no-eval from invoices aggregate { sum_of_orders = sum total } ``` The query above computes the sum of the `total` column of all rows of the `invoices` table to produce a single value. `aggregate` can produce multiple summaries at once when one or more aggregation expressions are contained in a tuple. `aggregate` discards all columns that are not present in the tuple. ```prql no-eval from invoices aggregate { num_orders = count this, sum_of_orders = sum total, } ``` In the example above, the result is a single row with two columns. The `count` function displays the number of rows in the table that was passed in; the `sum` function adds up the values of the `total` column of all rows. ## Grouping Suppose we want to produce summaries of invoices _for each city_ in the table. We could create a query for each city, and aggregate its rows: ```prql no-eval from invoices filter billing_city == "Oslo" aggregate { sum_of_orders = sum total } ``` But we would need to do it for each city: `London`, `Frankfurt`, etc. Of course this is repetitive (and boring) and error prone (because we would need to type each `billing_city` by hand). Moreover, we would need to create a list of each `billing_city` before we started. ### `group` transform The `group` transform separates the table into groups (say, those having the same city) using information that's already in the table. It then applies a transform to each group, and combines the results back together: ```prql no-eval from invoices group billing_city ( aggregate { num_orders = count this, sum_of_orders = sum total, } ) ``` Those familiar with SQL have probably noticed that we just decoupled aggregation from grouping. Although these operations are connected in SQL, PRQL makes it straightforward to use `group` and `aggregate` separate from each other, while combining with other transform functions, such as: ```prql no-eval from invoices group billing_city ( take 2 ) ``` This code collects the first two rows for each city's `group`. ================================================ FILE: web/book/src/tutorial/annotated_example.md ================================================ # Annotated example The [Playground](https://prql-lang.org/playground) defaults to showing a query that demonstrates most of the transforms and capabilities of PRQL. This page explains the details of each line in that example. ```prql no-eval from invoices # A PRQL query begins with a table # Subsequent lines "transform" (modify) it derive { # "derive" adds columns to the result transaction_fee = 0.8, # "=" sets a column name income = total - transaction_fee # Calculations can use other column names } # This is a comment; commenting out a line leaves a valid query filter income > 5 # "filter" replaces both of SQL's WHERE & HAVING filter invoice_date >= @2010-01-16 # Clear date syntax group customer_id ( # "group" performs the pipeline in (...) on each group aggregate { # "aggregate" reduces each group to a single row sum_income = sum income, # ... using SQL SUM(), COUNT(), etc. functions ct = count customer_id, # } ) join c=customers (==customer_id) # join on "customer_id" from both tables derive name = f"{c.last_name}, {c.first_name}" # F-strings like Python derive db_version = s"version()" # S-string offers escape hatch to SQL select { # "select" passes along only the named columns c.customer_id, name, sum_income, ct, db_version, } # trailing commas always ignored sort {-sum_income} # "sort" sorts the result; "-" is decreasing order take 1..10 # Limit to a range - could also be "take 10" # # The "output.sql" tab at right shows the SQL generated from this PRQL query # The "output.arrow" tab shows the result of the query ``` ================================================ FILE: web/book/src/tutorial/filtering.md ================================================ # Filtering rows In the previous page we learned how `select`, `derive`, and `join` change the columns of a table. Now we will explore how to manipulate the rows of a table using `filter` and `take`. ### `filter` transform The `filter` transform picks rows to pass through based on their values: ```prql no-eval from invoices filter billing_city == "Berlin" ``` The resulting table contains all the rows that came from Berlin. PRQL converts the single `filter` transform to use the appropriate SQL `WHERE` or `HAVING` command, depending on where it appears in the pipeline. ### `take` transform The `take` transform picks rows to pass through based on their position within the table. The set of rows picked can be specified in two ways: - a plain number `x`, which will pick the first `x` rows, or - an inclusive range of rows `start..end`. ```prql no-eval from invoices take 4 ``` ```prql no-eval from invoices take 4..7 ``` Of course, it is possible combine all these transforms into a single pipeline: ```prql no-eval from invoices # retain only rows for orders from Berlin filter billing_city == "Berlin" # skip first 10 rows and take the next 10 take 11..20 # take only first 3 rows of *that* result take 3 ``` We did something a bit odd at the end: first we took rows `11..20` and then took the first 3 rows from that result. > [!NOTE] > Note that a single transform `take 11..13` would have produced the > same SQL. The example shows how PRQL allows fast data exploration by > "stacking" transforms in the pipeline. This reduces the cognitive burden: > unlike SQL, each new transform interacts _only_ with the results of the > previous query. ================================================ FILE: web/book/src/tutorial/relations.md ================================================ # Relations PRQL is designed on top of _relational algebra_, which is the established data model used by modern SQL databases. A _relation_ has a rigid mathematical definition, which can be simplified to "a table of data". For example, the `invoices` table from the Chinook database ([https://github.com/lerocha/chinook-database](https://github.com/lerocha/chinook-database)) looks like this: | invoice_id | customer_id | billing_city | _other columns_ | total | | ---------- | ----------- | ------------ | :-------------: | ----- | | 1 | 2 | Stuttgart | ... | 1.98 | | 2 | 4 | Oslo | ... | 3.96 | | 3 | 8 | Brussels | ... | 5.94 | | 4 | 14 | Edmonton | ... | 8.91 | | 5 | 23 | Boston | ... | 13.86 | | 6 | 37 | Frankfurt | ... | 0.99 | A relation is composed of rows. Each row in a relation contains a value for each of the relation's columns. Each column in a relation has a unique name and a designated data type. The table above is a relation, and has columns named `invoice_id`and `customer_id` each with a data type of "integer number", a `billing_city` column with a data type of "text", several other columns, and a `total` column that contains floating-point numbers. ## Queries The main purpose of PRQL is to build queries that combine and transform data from relations such as the `invoices` table above. Here is the most basic query: ```prql no-eval from invoices ``` > [!NOTE] > Try each of these examples here in the > [Playground.](https://prql-lang.org/playground/) Enter the query on the > left-hand side, and click the **Query Results** tab on the right-hand side to > see the result. The result of the query above is not terribly interesting, it's just the same relation as before. ### `select` transform The `select` function picks the columns to pass through based on a list and discards all others. Formally, that list is a _tuple_ of comma-separated expressions wrapped in `{ ... }`. Suppose we only need the `order_id` and `total` columns. Use `select` to choose the columns to pass through. _(Try it in the [Playground.](https://prql-lang.org/playground/))_ ```prql no-eval from invoices select { order_id, total } ``` We can write the items in the tuple on one or several lines: trailing commas are ignored. In addition, we can assign any of the expressions to a _variable_ that becomes the name of the resulting column in the SQL output. ```prql no-eval from invoices select { OrderID = invoice_id, Total = total, } ``` This is the same query as above, rewritten on multiple lines, and assigning `OrderID` and `Total` names to the columns. Once we `select` certain columns, subsequent transforms will have access only to those columns named in the tuple. ### `derive` transform To add columns to a relation, we can use the `derive` function. Let's define a new column for Value Added Tax, set at 19% of the invoice total. ```prql no-eval from invoices derive { VAT = total * 0.19 } ``` The value of the new column can be a constant (such as a number or a string), or can be computed from the value of an existing column. Note that the value of the new column is assigned the name `VAT`. ### `join` transform The `join` transform also adds columns to the relation by combining the rows from two relations "side by side". To determine which rows from each relation should be joined, `join` has match criteria, written in `( ... )`. ```prql no-eval from invoices join customers ( ==customer_id ) ``` This example "connects" the customer information from the `customers` relation with the information from the `invoices` relation, using identical values of the `customer_id` column from each relation to match the rows. It is frequently useful to assign an alias to both relations being joined together so that each relation's columns can be referred to uniquely. ```prql no-eval from inv=invoices join cust=customers ( ==customer_id ) ``` In the example above, the alias `inv` represents the `invoices` relation and `cust` represents the `customers` relation. It then becomes possible to refer to `inv.billing_city` and `cust.last_name` unambiguously. ### Summary PRQL manipulates relations (tables) of data. The `derive`, `select`, and `join` transforms change the number of columns in a table. The first two never affect the number of rows in a table. `join` may change the number of rows, depending on the chosen type of join. This final example combines the above into a single query. It illustrates _a pipeline_ - the fundamental basis of PRQL. We simply add new lines (transforms) at the end of the query. Each transform modifies the relation produced by the statement above to produce the desired result. ```prql no-eval from inv=invoices join cust=customers (==customer_id) derive { VAT = inv.total * 0.19 } select { OrderID = inv.invoice_id, CustomerName = cust.last_name, Total = inv.total, VAT, } ``` ================================================ FILE: web/book/tests/documentation/README.md ================================================ # PRQL documentation tests This directory contains tests for PRQL documentation (website, book and README). ================================================ FILE: web/book/tests/documentation/book.rs ================================================ #![cfg(not(target_family = "wasm"))] use std::fs; use std::path::Path; use anyhow::{anyhow, bail, Result}; use globset::Glob; use insta::assert_snapshot; use itertools::Itertools; use mdbook_prql::{code_block_lang_tags, LangTag}; use prqlc::{pl_to_prql, pl_to_rq, prql_to_pl}; use pulldown_cmark::Tag; use walkdir::WalkDir; use super::compile; /// This test: /// - Extracts PRQL code blocks from the book /// - Compiles them to SQL, comparing to a snapshot. /// - We raise an error if they shouldn't pass or shouldn't fail. /// - Insta raises an error if there's a snapshot diff. /// /// This mirrors the process in [`replace_examples`], which inserts a comparison /// table of SQL into the book, and so serves as a snapshot test of those /// examples. // // We re-use the code (somewhat copy-paste) for the other compile tests below. #[test] fn test_prql_examples_compile() -> Result<()> { // Override the version so the examples work with the version defined in the // manifest. std::env::set_var("PRQL_VERSION_OVERRIDE", env!("CARGO_PKG_VERSION")); let examples = collect_book_examples()?; let mut errs = Vec::new(); for Example { name, tags, prql } in examples { let result = compile(&prql); let should_succeed = !tags.contains(&LangTag::Error); match (should_succeed, result) { (true, Err(e)) => errs.push(format!( " ---- {name} ---- ERROR Use `prql error` as the language label to assert an error compiling the PRQL. -- Original PRQL -- ``` {prql} ``` -- Error -- ``` {e} ``` " )), (false, Ok(output)) => errs.push(format!( " ---- {name} ---- UNEXPECTED SUCCESS Succeeded compiling, but example was marked as `error`. Remove `error` as a language label to assert successfully compiling. -- Original PRQL -- ``` {prql} ``` -- Result -- ``` {output} ``` " )), (_, result) => { assert_snapshot!(name, result.unwrap_or_else(|e| e.to_string()), &prql); } } } if errs.is_empty() { Ok(()) } else { Err(anyhow!(errs.join("\n"))) } } #[test] fn test_prql_examples_rq_serialize() -> Result<()> { for Example { tags, prql, .. } in collect_book_examples()? { // Don't assert that this fails, whether or not they compile to RQ is // undefined. if tags.contains(&LangTag::Error) { continue; } let rq = prql_to_pl(&prql).map(pl_to_rq)?; // Serialize serde_json::to_string(&rq).unwrap(); } Ok(()) } /// Test that the formatted result (the `Display` result) of each example can be /// compiled. // // We previously snapshot all the queries. But that was a lot of output, for // something we weren't yet looking at. // // The ideal would be to auto-format the examples themselves, likely during the // compilation. For that to provide a good output, we need to implement a proper // autoformatter. #[test] fn test_prql_examples_display_then_compile() -> Result<()> { let examples = collect_book_examples()?; let mut errs = Vec::new(); for Example { name, tags, prql } in examples { let result = prql_to_pl(&prql) .and_then(|x| pl_to_prql(&x)) .and_then(|x| compile(&x)); let should_succeed = !tags.contains(&LangTag::NoFmt); match (should_succeed, result) { (true, Err(e)) => errs.push(format!( " ---- {name} ---- ERROR formatting & compiling Use `prql no-fmt` as the language label to assert an error from formatting & compiling. -- Original PRQL -- ``` {prql} ``` -- Error -- ``` {e} ``` " )), (false, Ok(output)) => errs.push(format!( " ---- {name} ---- UNEXPECTED SUCCESS after formatting Succeeded at formatting and then compiling the prql, but example was marked as `no-fmt`. Remove `no-fmt` as a language label to assert successfully compiling the formatted result. -- Original PRQL -- ``` {prql} ``` -- Result -- ``` {output} ``` " )), _ => {} } } if errs.is_empty() { Ok(()) } else { Err(anyhow!(errs.join(""))) } } struct Example { /// Name contains the file, the heading, and the index of the example. name: String, tags: Vec, /// The PRQL text prql: String, } /// Collect all the PRQL examples in the book, as [Example]s. /// Excludes any with a `no-eval` tag. fn collect_book_examples() -> Result> { use pulldown_cmark::{Event, Parser}; let glob = Glob::new("**/*.md")?.compile_matcher(); Ok(WalkDir::new(Path::new("./src/")) .into_iter() .flatten() .filter(|x| glob.is_match(x.path())) .flat_map(|dir_entry| { let text = fs::read_to_string(dir_entry.path())?; // TODO: Still slightly duplicative logic here and in // [lib.rs/replace_examples], but not sure how to avoid it. // let mut parser = Parser::new(&text); let mut prql_blocks: Vec = vec![]; // Keep track of the latest heading, so snapshots can have the // section they're in. This makes them easier to find and means // adding one example at the top of the book doesn't cause a huge // diff in the snapshots of that file's examples.. let mut latest_heading = "".to_string(); let file_name = &dir_entry .path() .strip_prefix("./src/")? .to_str() .unwrap() .trim_end_matches(".md"); // Iterate through the markdown file, getting examples. while let Some(event) = parser.next() { if let Event::Start(Tag::Heading { .. }) = event.clone() { if let Some(Event::Text(pulldown_cmark::CowStr::Borrowed(heading))) = parser.next() { // We clear and then push because just setting // `latest_heading` leads to lifetime issues. latest_heading = heading .chars() .filter(|&c| c.is_ascii_alphanumeric() || c == '-' || c == ' ') .collect(); } } let Some(tags) = code_block_lang_tags(&event) else { continue; }; if tags.contains(&LangTag::Prql) && !tags.contains(&LangTag::NoEval) { let mut prql = String::new(); while let Some(Event::Text(line)) = parser.next() { prql.push_str(line.to_string().as_str()); } if prql.is_empty() { bail!("Expected text in PRQL code block"); } let heading = latest_heading.replace(' ', "-").to_ascii_lowercase(); // Only add the heading if it's different from the file name. let name = if !file_name.ends_with(&heading) { format!("{file_name}/{heading}") } else { file_name.to_string() }; prql_blocks.push(Example { name, tags, prql }); } } Ok(prql_blocks) }) .flatten() // Add an index suffix to each path's examples (so we group by the path). .chunk_by(|e| e.name.clone()) .into_iter() .flat_map(|(path, blocks)| { blocks.into_iter().enumerate().map(move |(i, e)| Example { name: format!("{path}/{i}"), ..e }) }) .collect()) } ================================================ FILE: web/book/tests/documentation/main.rs ================================================ #![cfg(not(target_family = "wasm"))] /// As well as the examples in the book, we also test the examples in the /// website & README in this integration test binary. mod book; mod readme; mod website; use ::prqlc::Options; fn compile(prql: &str) -> Result { prqlc::compile( prql, &Options::default() .no_signature() .with_display(prqlc::DisplayOptions::Plain), ) } ================================================ FILE: web/book/tests/documentation/readme.rs ================================================ use regex::Regex; use super::compile; #[test] fn test_readme_examples() { let contents = include_str!("../../../../README.md"); // Similar to code at https://github.com/PRQL/prql/blob/65706a115a84997c608eaeda38b1aef1240fcec3/web/book/tests/snapshot.rs#L152, but specialized for the Readme. let re = Regex::new(r"(?s)```(elm|prql)\r?\n(?P.+?)\r?\n```").unwrap(); assert_ne!(re.find_iter(contents).count(), 0); re.captures_iter(contents).for_each(|capture| { compile(&capture["prql"]).unwrap(); }); } ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__README__prql-language-book__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from tracks\nfilter artist == \"Bob Marley\" # Each line transforms the previous result\naggregate { # `aggregate` reduces each column to a value\n plays = sum plays,\n longest = max length,\n shortest = min length, # Trailing commas are allowed\n}\n" --- SELECT COALESCE(SUM(plays), 0) AS plays, MAX(length) AS longest, MIN(length) AS shortest FROM tracks WHERE artist = 'Bob Marley' ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__README__prql-language-book__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nfilter start_date > @2021-01-01 # Clear date syntax\nderive { # `derive` adds columns / variables\n gross_salary = salary + (tax ?? 0), # Terse coalesce\n gross_cost = gross_salary + benefits, # Variables can use other variables\n}\nfilter gross_cost > 0\ngroup {title, country} ( # `group` runs a pipeline over each group\n aggregate { # `aggregate` reduces each group to a value\n average gross_salary,\n sum_gross_cost = sum gross_cost, # `=` sets a column name\n }\n)\nfilter sum_gross_cost > 100_000 # `filter` replaces both of SQL's `WHERE` & `HAVING`\nderive id = f\"{title}_{country}\" # F-strings like Python\nderive country_code = s\"LEFT(country, 2)\" # S-strings permit SQL as an escape hatch\nsort {sum_gross_cost, -country} # `-country` means descending order\ntake 1..20 # Range expressions (also valid as `take 20`)\n" --- WITH table_0 AS ( SELECT title, country, AVG(salary + COALESCE(tax, 0)) AS _expr_0, COALESCE(SUM(salary + COALESCE(tax, 0) + benefits), 0) AS sum_gross_cost FROM employees WHERE start_date > DATE '2021-01-01' AND salary + COALESCE(tax, 0) + benefits > 0 GROUP BY title, country ) SELECT title, country, _expr_0, sum_gross_cost, CONCAT(title, '_', country) AS id, LEFT(country, 2) AS country_code FROM table_0 WHERE sum_gross_cost > 100000 ORDER BY sum_gross_cost, country DESC LIMIT 20 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__project__target__examples__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.postgres\n\nfrom employees\nsort age\ntake 10\n" --- SELECT * FROM employees ORDER BY age LIMIT 10 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__project__target__examples__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.mssql\n\nfrom employees\nsort age\ntake 10\n" --- SELECT * FROM employees ORDER BY age OFFSET 0 ROWS FETCH FIRST 10 ROWS ONLY ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__project__target__version__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql version:\"0.13.4\"\n\nfrom employees\n" --- SELECT * FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__project__target__version__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "[{version = prql.version}]\n" --- WITH table_0 AS ( SELECT '0.13.12' AS version ) SELECT version FROM table_0 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__data__from__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from artists\n" --- SELECT * FROM artists ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__data__from__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from e = employees\nselect e.first_name\n" --- SELECT first_name FROM employees AS e ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__data__from__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from `artist tracks`\n" --- SELECT * FROM "artist tracks" ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__data__from__3.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "default_db.group # in place of `from group`\ntake 1\n" --- SELECT * FROM "group" LIMIT 1 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__data__read-files__reading-files__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.duckdb\n\nfrom a = (read_parquet \"artists.parquet\")\njoin b = (read_csv \"albums.csv\") (a.artist_id == b.artist_id)\njoin c = (read_json \"metadata.json\") (a.artist_id == c.artist_id)\n" --- WITH table_0 AS ( SELECT * FROM read_parquet( 'artists.parquet', binary_as_string = false, file_row_number = false, hive_partitioning = NULL, union_by_name = false ) ), table_1 AS ( SELECT * FROM read_csv_auto('albums.csv') ), table_2 AS ( SELECT * FROM read_json_auto('metadata.json') ) SELECT table_0.*, table_1.*, table_2.* FROM table_0 INNER JOIN table_1 ON table_0.artist_id = table_1.artist_id INNER JOIN table_2 ON table_0.artist_id = table_2.artist_id ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__data__read-files__reading-files__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from `artists.parquet`\n" --- SELECT * FROM "artists.parquet" ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__data__relation-literals__array-literals__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from [\n {a=5, b=false},\n {a=6, b=true},\n]\nfilter b == true\nselect a\n" --- WITH table_0 AS ( SELECT 5 AS a, false AS b UNION ALL SELECT 6 AS a, true AS b ) SELECT a FROM table_0 WHERE b = true ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__data__relation-literals__array-literals__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "let my_artists = [\n {artist=\"Miles Davis\"},\n {artist=\"Marvin Gaye\"},\n {artist=\"James Brown\"},\n]\n\nfrom artists\njoin my_artists (==artist)\njoin albums (==artist_id)\nselect {artists.artist_id, albums.title}\n" --- WITH table_0 AS ( SELECT 'Miles Davis' AS artist UNION ALL SELECT 'Marvin Gaye' AS artist UNION ALL SELECT 'James Brown' AS artist ), my_artists AS ( SELECT artist FROM table_0 ) SELECT artists.artist_id, albums.title FROM artists INNER JOIN my_artists ON artists.artist = my_artists.artist INNER JOIN albums ON artists.artist_id = albums.artist_id ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__data__relation-literals__array-literals__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from_text \"\"\"\na,b,c\n1,2,3\n4,5,6\n\"\"\"\nderive {\n d = b + c,\n answer = 20 * 2 + 2,\n}\n" --- WITH table_0 AS ( SELECT '1' AS a, '2' AS b, '3' AS c UNION ALL SELECT '4' AS a, '5' AS b, '6' AS c ) SELECT a, b, c, b + c AS d, 20 * 2 + 2 AS answer FROM table_0 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__data__relation-literals__array-literals__3.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from_text format:json \"\"\"\n[\n {\"a\": 1, \"m\": \"5\"},\n {\"a\": 4, \"n\": \"6\"}\n]\n\"\"\"\n" --- WITH table_0 AS ( SELECT 1 AS a, '5' AS m UNION ALL SELECT 4 AS a, NULL AS m ) SELECT a, m FROM table_0 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__data__relation-literals__array-literals__4.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from_text format:json \"\"\"\n{\n \"columns\": [\"a\", \"b\", \"c\"],\n \"data\": [\n [1, \"x\", false],\n [4, \"y\", null]\n ]\n}\n\"\"\"\n" --- WITH table_0 AS ( SELECT 1 AS a, 'x' AS b, false AS c UNION ALL SELECT 4 AS a, 'y' AS b, NULL AS c ) SELECT a, b, c FROM table_0 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__declarations__functions__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "let fahrenheit_to_celsius = temp -> (temp - 32) / 1.8\n\nfrom cities\nderive temp_c = (fahrenheit_to_celsius temp_f)\n" --- SELECT *, (temp_f - 32) / 1.8 AS temp_c FROM cities ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__declarations__functions__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "let interp = low:0 high x -> (x - low) / (high - low)\n\nfrom students\nderive {\n sat_proportion_1 = (interp 1600 sat_score),\n sat_proportion_2 = (interp low:0 1600 sat_score),\n}\n" --- SELECT *, (sat_score - 0) / (1600 - 0) AS sat_proportion_1, (sat_score - 0) / (1600 - 0) AS sat_proportion_2 FROM students ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__declarations__functions__late-binding__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "let cost_share = cost -> cost / cost_total\n\nfrom costs\nselect {materials, labor, overhead, cost_total}\nderive {\n materials_share = (cost_share materials),\n labor_share = (cost_share labor),\n overhead_share = (cost_share overhead),\n}\n" --- SELECT materials, labor, overhead, cost_total, materials / cost_total AS materials_share, labor / cost_total AS labor_share, overhead / cost_total AS overhead_share FROM costs ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__declarations__functions__other-examples__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "let is_adult = col -> col >= 18\nlet writes_code = col -> (col | in [\"PRQL\", \"Rust\"])\nlet square = col -> (col | math.pow 2)\nlet starts_with_a = col -> (col | text.lower | text.starts_with(\"a\"))\n\nfrom employees\nselect {\n first_name,\n last_name,\n hobby,\n adult = is_adult age,\n age_squared = square age,\n}\nfilter ((starts_with_a last_name) && (writes_code hobby))\n" --- WITH table_0 AS ( SELECT first_name, last_name, hobby, age >= 18 AS adult, POW(age, 2) AS age_squared FROM employees ) SELECT first_name, last_name, hobby, adult, age_squared FROM table_0 WHERE LOWER(last_name) LIKE CONCAT('a', '%') AND hobby IN ('PRQL', 'Rust') ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__declarations__functions__partial-application__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "let top_n = n -> take n\n\nfrom invoices\ntop_n 10\n" --- SELECT * FROM invoices LIMIT 10 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__declarations__functions__partial-application__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "let top_n = n -> take n\nlet add_constant = x -> derive {constant = x}\n\nlet my_pipeline = (top_n 5 | add_constant 42)\n\nfrom invoices\nmy_pipeline\n" --- SELECT *, 42 AS constant FROM invoices LIMIT 5 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__declarations__functions__partial-application__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "let top_n = n -> take n\nlet top_5 = top_n 5\n\nfrom invoices\ntop_5\n" --- SELECT * FROM invoices LIMIT 5 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__declarations__functions__piping-values-into-functions__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "let interp = low:0 high x -> (x - low) / (high - low)\n\nfrom students\nderive {\n sat_proportion_1 = (sat_score | interp 1600),\n sat_proportion_2 = (sat_score | interp low:0 1600),\n}\n" --- SELECT *, (sat_score - 0) / (1600 - 0) AS sat_proportion_1, (sat_score - 0) / (1600 - 0) AS sat_proportion_2 FROM students ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__declarations__functions__piping-values-into-functions__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "let fahrenheit_to_celsius = temp -> (temp - 32) / 1.8\n\nfrom cities\nderive temp_c = (temp_f | fahrenheit_to_celsius)\n" --- SELECT *, (temp_f - 32) / 1.8 AS temp_c FROM cities ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__declarations__functions__piping-values-into-functions__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "let fahrenheit_to_celsius = temp -> (temp - 32) / 1.8\nlet interp = low:0 high x -> (x - low) / (high - low)\n\nfrom kettles\nderive boiling_proportion = (temp_c | fahrenheit_to_celsius | interp 100)\n" --- SELECT *, ((temp_c - 32) / 1.8 - 0) / (100 - 0) AS boiling_proportion FROM kettles ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__declarations__variables__variables--__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "let top_50 = (\n from employees\n sort salary\n take 50\n aggregate {total_salary = sum salary}\n)\n\nfrom top_50 # Starts a new pipeline\n" --- WITH table_0 AS ( SELECT salary FROM employees ORDER BY salary LIMIT 50 ), top_50 AS ( SELECT COALESCE(SUM(salary), 0) AS total_salary FROM table_0 ) SELECT total_salary FROM top_50 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__declarations__variables__variables--__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\ntake 50\ninto first_50\n\nfrom first_50\n" --- WITH first_50 AS ( SELECT * FROM employees LIMIT 50 ) SELECT * FROM first_50 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__declarations__variables__variables--__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "let grouping = s\"\"\"\n SELECT SUM(a)\n FROM tbl\n GROUP BY\n GROUPING SETS\n ((b, c, d), (d), (b, d))\n\"\"\"\n\nfrom grouping\n" --- WITH table_0 AS ( SELECT SUM(a) FROM tbl GROUP BY GROUPING SETS ((b, c, d), (d), (b, d)) ) SELECT * FROM table_0 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__spec__name-resolution__translating-to-sql__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nselect first_name\n" --- SELECT first_name FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__spec__name-resolution__translating-to-sql__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nderive {first_name, dept_id}\njoin d=departments (==dept_id)\nselect {first_name, d.title}\n" --- SELECT employees.first_name, d.title FROM employees INNER JOIN departments AS d ON employees.dept_id = d.dept_id ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__spec__null__null-handling__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nfilter first_name == null\nfilter null != last_name\n" --- SELECT * FROM employees WHERE first_name IS NULL AND last_name IS NOT NULL ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__README__standard-library__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nderive {\n gross_salary = (salary + payroll_tax | as int),\n gross_salary_rounded = (gross_salary | math.round 0),\n time = s\"NOW()\", # an s-string, given no `now` function exists in PRQL\n}\n" --- SELECT *, CAST(salary + payroll_tax AS int) AS gross_salary, ROUND(CAST(salary + payroll_tax AS int), 0) AS gross_salary_rounded, NOW() AS time FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__README__standard-library__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.sqlite\n\nfrom [{x = 13, y = 5}]\nselect {\n quotient = x / y,\n int_quotient = x // y,\n}\n" --- WITH table_0 AS ( SELECT 13 AS x, 5 AS y ) SELECT (x * 1.0 / y) AS quotient, ROUND(ABS(x / y) - 0.5) * SIGN(x) * SIGN(y) AS int_quotient FROM table_0 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__README__standard-library__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.mysql\n\nfrom [{x = 13, y = 5}]\nselect {\n quotient = x / y,\n int_quotient = x // y,\n}\n" --- WITH table_0 AS ( SELECT 13 AS x, 5 AS y ) SELECT (x / y) AS quotient, (x DIV y) AS int_quotient FROM table_0 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__date__date-functions__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.duckdb\n\nfrom invoices\nselect (invoice_date | date.to_text \"%d/%m/%Y\")\n\n" --- SELECT strftime(invoice_date, '%d/%m/%Y') FROM invoices ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__date__date-functions__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.postgres\n\nfrom invoices\nselect (invoice_date | date.to_text \"%d/%m/%Y\")\n\n" --- SELECT TO_CHAR(invoice_date, 'DD/MM/YYYY') FROM invoices ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__date__date-functions__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.mysql\n\nfrom invoices\nselect (invoice_date | date.to_text \"%d/%m/%Y\")\n\n" --- SELECT DATE_FORMAT(invoice_date, '%d/%m/%Y') FROM invoices ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__distinct__how-do-i-remove-duplicates__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nselect department\ngroup employees.* (\n take 1\n)\n" --- SELECT DISTINCT department FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__distinct__how-do-i-remove-duplicates__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\ngroup employees.* (take 1)\n" --- SELECT DISTINCT * FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__distinct__remove-duplicates-from-each-group__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "# youngest employee from each department\nfrom employees\ngroup department (\n sort age\n take 1\n)\n" --- WITH table_0 AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY department ORDER BY age ) AS _expr_0 FROM employees ) SELECT * FROM table_0 WHERE _expr_0 <= 1 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__distinct__remove-duplicates-from-each-group__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\ngroup {first_name, last_name} (take 1)\n" --- WITH table_0 AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY first_name, last_name) AS _expr_0 FROM employees ) SELECT * FROM table_0 WHERE _expr_0 <= 1 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__math__example__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nselect age_squared = (age | math.pow 2)\n" --- SELECT POW(age, 2) AS age_squared FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__text__example__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nselect {\n (last_name | text.lower | text.starts_with(\"a\")),\n (title | text.replace \"manager\" \"chief\"),\n}\n" --- SELECT LOWER(last_name) LIKE CONCAT('a', '%'), REPLACE(title, 'manager', 'chief') FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__aggregate__aggregate-is-required__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nderive {avg_sal = average salary}\n" --- SELECT *, AVG(salary) OVER () AS avg_sal FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__aggregate__examples__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\naggregate {\n average salary,\n ct = count salary\n}\n" --- SELECT AVG(salary), COUNT(*) AS ct FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__aggregate__examples__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\ngroup {title, country} (\n aggregate {\n average salary,\n ct = count salary,\n }\n)\n" --- SELECT title, country, AVG(salary), COUNT(*) AS ct FROM employees GROUP BY title, country ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__append__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees_1\nappend employees_2\n" --- SELECT * FROM employees_1 UNION ALL SELECT * FROM employees_2 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__append__intersection__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees_1\nintersect employees_2\n" --- SELECT * FROM employees_1 AS t INTERSECT ALL SELECT * FROM employees_2 AS b ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__append__remove__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees_1\nremove employees_2\n" --- SELECT * FROM employees_1 AS t EXCEPT ALL SELECT * FROM employees_2 AS b ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__derive__examples__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nderive gross_salary = salary + payroll_tax\n" --- SELECT *, salary + payroll_tax AS gross_salary FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__derive__examples__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nderive {\n gross_salary = salary + payroll_tax,\n gross_cost = gross_salary + benefits_cost\n}\n" --- SELECT *, salary + payroll_tax AS gross_salary, salary + payroll_tax + benefits_cost AS gross_cost FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__filter__examples__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nfilter age > 25\n" --- SELECT * FROM employees WHERE age > 25 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__filter__examples__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nfilter (age > 25 || department != \"IT\")\n" --- SELECT * FROM employees WHERE age > 25 OR department <> 'IT' ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__filter__examples__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nfilter (department | in [\"IT\", \"HR\"])\n" --- SELECT * FROM employees WHERE department IN ('IT', 'HR') ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__filter__examples__3.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nfilter (age | in 25..40)\n" --- SELECT * FROM employees WHERE age BETWEEN 25 AND 40 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__group__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\ngroup {title, country} (\n aggregate {\n average salary,\n ct = count salary\n }\n)\n" --- SELECT title, country, AVG(salary), COUNT(*) AS ct FROM employees GROUP BY title, country ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__group__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nsort join_date\ntake 1\n" --- SELECT * FROM employees ORDER BY join_date LIMIT 1 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__group__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\ngroup role (\n sort join_date # taken from above\n take 1\n)\n" --- WITH table_0 AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY role ORDER BY join_date ) AS _expr_0 FROM employees ) SELECT * FROM table_0 WHERE _expr_0 <= 1 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__join__examples__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\njoin side:left positions (employees.id==positions.employee_id)\n" --- SELECT employees.*, positions.* FROM employees LEFT OUTER JOIN positions ON employees.id = positions.employee_id ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__join__examples__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\njoin side:left p=positions (employees.id==p.employee_id)\n" --- SELECT employees.*, p.* FROM employees LEFT OUTER JOIN positions AS p ON employees.id = p.employee_id ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__join__examples__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from tracks\njoin side:left artists (\n # This adds a `country` condition, as an alternative to filtering\n artists.id==tracks.artist_id && artists.country=='UK'\n)\n" --- SELECT tracks.*, artists.* FROM tracks LEFT OUTER JOIN artists ON artists.id = tracks.artist_id AND artists.country = 'UK' ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__join__examples__3.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from tracks\njoin side:inner artists (\n this.id==that.artist_id\n)\n" --- SELECT tracks.*, artists.* FROM tracks INNER JOIN artists ON tracks.id = artists.artist_id ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__join__examples__4.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\njoin positions (==emp_no)\n" --- SELECT employees.*, positions.* FROM employees INNER JOIN positions ON employees.emp_no = positions.emp_no ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__loop__examples__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from [{n = 1}]\nloop (\n filter n<4\n select n = n+1\n)\n\n# returns [1, 2, 3, 4]\n" --- WITH RECURSIVE table_0 AS ( SELECT 1 AS n ), table_1 AS ( SELECT n FROM table_0 UNION ALL SELECT n + 1 FROM table_1 WHERE n < 4 ) SELECT n FROM table_1 AS table_2 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__select__examples__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nselect name = f\"{first_name} {last_name}\"\n" --- SELECT CONCAT(first_name, ' ', last_name) AS name FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__select__examples__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nselect {\n name = f\"{first_name} {last_name}\",\n age_eoy = dob - @2022-12-31,\n}\n" --- SELECT CONCAT(first_name, ' ', last_name) AS name, dob - DATE '2022-12-31' AS age_eoy FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__select__examples__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nselect first_name\n" --- SELECT first_name FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__select__examples__3.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from e=employees\nselect {e.first_name, e.last_name}\n" --- SELECT first_name, last_name FROM employees AS e ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__select__excluding-columns__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.bigquery\nfrom tracks\nselect !{milliseconds, bytes}\n" --- SELECT * EXCEPT (milliseconds, bytes) FROM tracks ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__select__excluding-columns__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from tracks\nselect {track_id, title, composer, bytes}\nselect !{title, composer}\n" --- SELECT track_id, bytes FROM tracks ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__select__excluding-columns__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from artists\nderive nick = name\nselect !{artists.*}\n" --- SELECT name AS nick FROM artists ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__select__excluding-columns__3.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.bigquery\nfrom tracks\nselect !is_compilation\n" --- SELECT NOT is_compilation FROM tracks ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__sort__examples__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nsort age\n" --- SELECT * FROM employees ORDER BY age ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__sort__examples__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nsort {-age}\n" --- SELECT * FROM employees ORDER BY age DESC ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__sort__examples__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nsort {age, -tenure, +salary}\n" --- SELECT * FROM employees ORDER BY age, tenure DESC, salary ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__sort__examples__3.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nsort {s\"substr({first_name}, 2, 5)\"}\n" --- WITH table_0 AS ( SELECT *, substr(first_name, 2, 5) AS _expr_0 FROM employees ) SELECT * FROM table_0 ORDER BY _expr_0 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__sort__ordering-guarantees__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nsort tenure\njoin locations (==employee_id)\n" --- SELECT employees.*, locations.* FROM employees INNER JOIN locations ON employees.employee_id = locations.employee_id ORDER BY employees.tenure ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__take__examples__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\ntake 10\n" --- SELECT * FROM employees LIMIT 10 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__take__examples__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from orders\nsort {-value, created_at}\ntake 101..110\n" --- SELECT * FROM orders ORDER BY value DESC, created_at LIMIT 10 OFFSET 100 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__window__example__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\ngroup employee_id (\n sort month\n window rolling:12 (\n derive {trail_12_m_comp = sum paycheck}\n )\n)\n" --- SELECT *, SUM(paycheck) OVER ( PARTITION BY employee_id ORDER BY month ROWS BETWEEN 11 PRECEDING AND CURRENT ROW ) AS trail_12_m_comp FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__window__example__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from orders\nsort day\nwindow rows:-3..3 (\n derive {centered_weekly_average = average value}\n)\ngroup {order_month} (\n sort day\n window expanding:true (\n derive {monthly_running_total = sum value}\n )\n)\n" --- SELECT *, AVG(value) OVER ( ORDER BY day ROWS BETWEEN 3 PRECEDING AND 3 FOLLOWING ) AS centered_weekly_average, SUM(value) OVER ( PARTITION BY order_month ORDER BY day ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) AS monthly_running_total FROM orders ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__window__example__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from [\n {time_id=1, value=15},\n {time_id=2, value=11},\n {time_id=3, value=16},\n {time_id=4, value=9},\n {time_id=7, value=20},\n {time_id=8, value=22},\n]\nwindow rows:-2..0 (\n sort time_id\n derive {sma3rows = average value}\n)\nwindow range:-2..0 (\n sort time_id\n derive {sma3range = average value}\n)\n" --- WITH table_0 AS ( SELECT 1 AS time_id, 15 AS value UNION ALL SELECT 2 AS time_id, 11 AS value UNION ALL SELECT 3 AS time_id, 16 AS value UNION ALL SELECT 4 AS time_id, 9 AS value UNION ALL SELECT 7 AS time_id, 20 AS value UNION ALL SELECT 8 AS time_id, 22 AS value ) SELECT time_id, value, AVG(value) OVER ( ORDER BY time_id ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ) AS sma3rows, AVG(value) OVER ( ORDER BY time_id RANGE BETWEEN 2 PRECEDING AND CURRENT ROW ) AS sma3range FROM table_0 ORDER BY time_id ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__window__window-functions-as-first-class-citizens__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nfilter salary < (average salary)\n" --- WITH table_0 AS ( SELECT *, AVG(salary) OVER () AS _expr_0 FROM employees ) SELECT * FROM table_0 WHERE salary < _expr_0 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__window__windowing-by-default__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nsort age\nderive {rnk = rank age}\n" --- SELECT *, RANK() OVER ( ORDER BY age ) AS rnk FROM employees ORDER BY age ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__stdlib__transforms__window__windowing-by-default__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\ngroup department (\n sort age\n derive {rnk = rank age}\n)\n" --- SELECT *, RANK() OVER ( PARTITION BY department ORDER BY age ) AS rnk FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__case__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nderive distance = case [\n city == \"Calgary\" => 0,\n city == \"Edmonton\" => 300,\n]\n" --- SELECT *, CASE WHEN city = 'Calgary' THEN 0 WHEN city = 'Edmonton' THEN 300 ELSE NULL END AS distance FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__case__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nderive distance = case [\n city == \"Calgary\" => 0,\n city == \"Edmonton\" => 300,\n true => \"Unknown\",\n]\n" --- SELECT *, CASE WHEN city = 'Calgary' THEN 0 WHEN city = 'Edmonton' THEN 300 ELSE 'Unknown' END AS distance FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__comments__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees # Comment 1\n# Comment 2\naggregate {average salary}\n" --- SELECT AVG(salary) FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__f-strings__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nselect full_name = f\"{first_name} {last_name}\"\n" --- SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__f-strings__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from web\nselect url = f\"http{tls}://www.{domain}.{tld}/{page}\"\n" --- SELECT CONCAT( 'http', tls, '://www.', domain, '.', tld, '/', page ) AS url FROM web ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__f-strings__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from tracks\nselect length_str = f\"{length_seconds / 60} minutes\"\n" --- Error: ╭─[ :2:38 ] │ 2 │ select length_str = f"{length_seconds / 60} minutes" │ ┬ │ ╰── expected something else, '.', ':', or '}', but found " " ───╯ ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__keywords__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "std.from my_table\nstd.select {from = my_table.a, take = my_table.b}\nstd.take 3\n" --- SELECT a AS "from", b AS take FROM my_table LIMIT 3 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__keywords__identifiers--keywords__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from invoices\naggregate (\n count this\n)\n" --- SELECT COUNT(*) FROM invoices ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__keywords__identifiers--keywords__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from invoices\njoin tracks (this.track_id==that.id)\n" --- SELECT invoices.*, tracks.* FROM invoices INNER JOIN tracks ON invoices.track_id = tracks.id ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__keywords__identifiers--keywords__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from invoices\nderive t = time\n" --- Error: ╭─[ :2:12 ] │ 2 │ derive t = time │ ──┬─ │ ╰─── expected a value, but found a type ───╯ ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__keywords__identifiers--keywords__3.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from invoices\nderive t = this.time\n" --- SELECT *, time AS t FROM invoices ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__keywords__quoting__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.mysql\nfrom employees\nselect `first name`\n" --- SELECT `first name` FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__keywords__quoting__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.postgres\nfrom employees\nselect `first name`\n" --- SELECT "first name" FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__keywords__quoting__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.bigquery\n\nfrom `project-foo.dataset.table`\njoin `project-bar.dataset.table` (==col_bax)\n" --- SELECT `project-foo.dataset.table`.*, `project-bar.dataset.table`.* FROM `project-foo.dataset.table` INNER JOIN `project-bar.dataset.table` ON `project-foo.dataset.table`.col_bax = `project-bar.dataset.table`.col_bax ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__keywords__schemas--database-names__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from my_database.chinook.albums\n" --- SELECT * FROM my_database.chinook.albums ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__literals__dates__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nderive age_at_year_end = (@2022-12-31 - dob)\n" --- SELECT *, DATE '2022-12-31' - dob AS age_at_year_end FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__literals__durations__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from projects\nderive first_check_in = start + 10days\n" --- SELECT *, "start" + INTERVAL 10 DAY AS first_check_in FROM projects ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__literals__numbers__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from numbers\nselect {\n small = 1.000_000_1,\n big = 5_000_000,\n huge = 5e9,\n binary = 0b0011,\n hex = 0x80,\n octal = 0o777,\n}\n" --- SELECT 1.0000001 AS small, 5000000 AS big, 5000000000.0 AS huge, 3 AS "binary", 128 AS hex, 511 AS octal FROM numbers ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__literals__times__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from orders\nderive should_have_shipped_today = (order_time < @08:30)\n" --- SELECT *, order_time < TIME '08:30' AS should_have_shipped_today FROM orders ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__literals__timestamps__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from commits\nderive first_prql_commit = @2020-01-01T13:19:55-08:00\nderive first_prql_commit_utc = @2020-01-02T21:19:55Z\n" --- SELECT *, TIMESTAMP '2020-01-01T13:19:55-0800' AS first_prql_commit, TIMESTAMP '2020-01-02T21:19:55Z' AS first_prql_commit_utc FROM commits ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__operators__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from foo\nselect {\n circumference = diameter * 3.14159,\n area = (diameter / 2) ** 2,\n color,\n}\nfilter circumference > 10 && color != \"red\"\n" --- WITH table_0 AS ( SELECT diameter * 3.14159 AS circumference, POW(diameter / 2, 2) AS area, color FROM foo ) SELECT circumference, area, color FROM table_0 WHERE circumference > 10 AND color <> 'red' ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__operators__coalesce__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from orders\nderive amount ?? 0\n" --- SELECT *, COALESCE(amount, 0) FROM orders ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__operators__division-and-integer-division__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.sqlite\n\nfrom [\n {a = 5, b = 2},\n {a = 5, b = -2},\n]\nselect {\n div_out = a / b,\n int_div_out = a // b,\n}\n" --- WITH table_0 AS ( SELECT 5 AS a, 2 AS b UNION ALL SELECT 5 AS a, -2 AS b ) SELECT (a * 1.0 / b) AS div_out, ROUND(ABS(a / b) - 0.5) * SIGN(a) * SIGN(b) AS int_div_out FROM table_0 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__operators__parentheses__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\n# Requires parentheses, because it contains a pipe\nderive is_proximate = (distance | in 0..20)\n# Requires parentheses, because it's a function call\nderive total_distance = (sum distance)\n# `??` doesn't require parentheses, as it's not a function call\nderive min_capped_distance = (min distance ?? 5)\n# No parentheses needed, because no function call\nderive travel_time = distance / 40\n# No inner parentheses needed around `1+1` because no function call\nderive distance_rounded_2_dp = (math.round 1+1 distance)\nderive {\n # Requires parentheses, because it contains a pipe\n is_far = (distance | in 100..),\n # The left value of the range requires parentheses,\n # because of the minus sign\n is_negative = (distance | in (-100..0)),\n # ...this is equivalent\n is_negative = (distance | in (-100)..0),\n # _Technically_, this doesn't require parentheses, because it's\n # the RHS of an assignment in a tuple\n # (this is especially confusing)\n average_distance = average distance,\n}\n# Requires parentheses because of the minus sign\nsort (-distance)\n# A tuple is fine too\nsort {-distance}\n" --- SELECT *, distance BETWEEN 0 AND 20 AS is_proximate, SUM(distance) OVER () AS total_distance, MIN(COALESCE(distance, 5)) OVER () AS min_capped_distance, distance / 40 AS travel_time, ROUND(distance, 1 + 1) AS distance_rounded_2_dp, distance >= 100 AS is_far, distance BETWEEN -100 AND 0, distance BETWEEN -100 AND 0 AS is_negative, AVG(distance) OVER () AS average_distance FROM employees ORDER BY distance DESC ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__operators__parentheses__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nderive total_distance = sum distance\n" --- Error: ╭─[ :2:29 ] │ 2 │ derive total_distance = sum distance │ ────┬─── │ ╰───── Unknown name `distance` ───╯ ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__operators__parentheses__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nderive other_distance = (sum distance)\n" --- SELECT *, SUM(distance) OVER () AS other_distance FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__operators__regex-expressions__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from tracks\nfilter (name ~= \"Love\")\n" --- SELECT * FROM tracks WHERE REGEXP(name, 'Love') ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__operators__regex-expressions__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.duckdb\n\nfrom artists\nfilter (name ~= \"Love.*You\")\n" --- SELECT * FROM artists WHERE REGEXP_MATCHES(name, 'Love.*You') ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__operators__regex-expressions__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.bigquery\n\nfrom tracks\nfilter (name ~= \"\\\\bLove\\\\b\")\n" --- SELECT * FROM tracks WHERE REGEXP_CONTAINS(name, '\bLove\b') ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__operators__regex-expressions__3.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.postgres\n\nfrom tracks\nfilter (name ~= \"\\\\(I Can't Help\\\\) Falling\")\n" --- SELECT * FROM tracks WHERE name ~ '\(I Can''t Help\) Falling' ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__operators__regex-expressions__4.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.mysql\n\nfrom tracks\nfilter (name ~= \"With You\")\n" --- SELECT * FROM tracks WHERE REGEXP_LIKE(name, 'With You', 'c') ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__operators__regex-expressions__5.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "prql target:sql.sqlite\n\nfrom tracks\nfilter (name ~= \"But Why Isn't Your Syntax More Similar\\\\?\")\n" --- SELECT * FROM tracks WHERE name REGEXP 'But Why Isn''t Your Syntax More Similar\?' ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__operators__wrapping-lines__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from artists\nselect is_europe =\n\\ country == \"DE\"\n\\ || country == \"FR\"\n\\ || country == \"ES\"\n" --- SELECT country = 'DE' OR country = 'FR' OR country = 'ES' AS is_europe FROM artists ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__operators__wrapping-lines__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from tracks\n# This would be a really long line without being able to split it:\nselect listening_time_years = (spotify_plays + apple_music_plays + pandora_plays)\n# We can toggle between lines when developing:\n# \\ * length_seconds\n\\ * length_s\n# min hour day year\n\\ / 60 / 60 / 24 / 365\n" --- SELECT ( spotify_plays + apple_music_plays + pandora_plays ) * length_s / 60 / 60 / 24 / 365 AS listening_time_years FROM tracks ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__parameters__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nfilter id == $1\n" --- SELECT * FROM employees WHERE id = $1 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__pipes__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nfilter department == \"Product\"\nselect {first_name, last_name}\n" --- SELECT first_name, last_name FROM employees WHERE department = 'Product' ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__pipes__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees | filter department == \"Product\" | select {first_name, last_name}\n" --- SELECT first_name, last_name FROM employees WHERE department = 'Product' ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__pipes__ceci-nest-pas-une-pipe__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "[\n {a=2} # No pipe from line break before & after this list item\n]\nderive {\n c = 2 * a, # No pipe from line break before & after this tuple item\n}\n" --- WITH table_0 AS ( SELECT 2 AS a ) SELECT a, 2 * a AS c FROM table_0 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__pipes__ceci-nest-pas-une-pipe__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "let b =\n \\ 3 # No pipe from line break within this line wrap\n\n# No pipe from line break before this `from` statement\n\nfrom y\nderive a = b\n" --- SELECT *, 3 AS a FROM y ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__pipes__inner-transforms__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\ngroup {title, country} (\n aggregate {\n average salary,\n ct = count salary,\n }\n)\n" --- SELECT title, country, AVG(salary), COUNT(*) AS ct FROM employees GROUP BY title, country ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__r-strings__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from artists\nderive normal_string = \"\\\\\\t\" # two characters - \\ and tab (\\t)\nderive raw_string = r\"\\\\\\t\" # four characters - \\, \\, \\, and t\n" --- SELECT *, '\ ' AS normal_string, '\\\t' AS raw_string FROM artists ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__ranges__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from events\nfilter (created_at | in @1776-07-04..@1787-09-17)\nfilter (magnitude | in 50..100)\nderive is_northern = (latitude | in 0..)\n" --- SELECT *, latitude >= 0 AS is_northern FROM events WHERE created_at BETWEEN DATE '1776-07-04' AND DATE '1787-09-17' AND magnitude BETWEEN 50 AND 100 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__ranges__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from orders\nsort {-value, created_at}\ntake 101..110\n" --- SELECT * FROM orders ORDER BY value DESC, created_at LIMIT 10 OFFSET 100 ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__s-strings__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from my_table\nselect db_version = s\"version()\"\n" --- SELECT version() AS db_version FROM my_table ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__s-strings__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\naggregate {average salary}\n" --- SELECT AVG(salary) FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__s-strings__2.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from de=dept_emp\njoin s=salaries side:left (s.emp_no == de.emp_no && s\"\"\"\n ({s.from_date}, {s.to_date})\n OVERLAPS\n ({de.from_date}, {de.to_date})\n\"\"\")\n" --- SELECT de.*, s.* FROM dept_emp AS de LEFT OUTER JOIN salaries AS s ON s.emp_no = de.emp_no AND (s.from_date, s.to_date) OVERLAPS (de.from_date, de.to_date) ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__s-strings__3.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from s\"SELECT DISTINCT ON first_name, id, age FROM employees ORDER BY age ASC\"\njoin s = s\"SELECT * FROM salaries\" (==id)\n" --- WITH table_0 AS ( SELECT DISTINCT ON first_name, id, age FROM employees ORDER BY age ASC ), table_1 AS ( SELECT * FROM salaries ) SELECT table_0.*, table_1.* FROM table_0 INNER JOIN table_1 ON table_0.id = table_1.id ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__s-strings__braces__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nderive {\n has_valid_title = s\"regexp_contains(title, '([a-z0-9]*-){{2,}}')\"\n}\n" --- SELECT *, regexp_contains(title, '([a-z0-9]*-){2,}') AS has_valid_title FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__s-strings__precedence-within-s-strings__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nderive {\n gross_salary = salary + benefits,\n daily_rate = s\"{gross_salary} / 365\"\n}\n" --- SELECT *, salary + benefits AS gross_salary, salary + benefits / 365 AS daily_rate FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__s-strings__precedence-within-s-strings__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nderive {\n gross_salary = salary + benefits,\n daily_rate = s\"({gross_salary}) / 365\"\n}\n" --- SELECT *, salary + benefits AS gross_salary, (salary + benefits) / 365 AS daily_rate FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__strings__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from artists\nderive {\n single = 'hello world',\n double = \"hello world\",\n double_triple = \"\"\"hello world\"\"\",\n}\n" --- SELECT *, 'hello world' AS single, 'hello world' AS double, 'hello world' AS double_triple FROM artists ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__strings__quoting-and-escape-characters__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from artists\nselect {\n other = '\"hello world\"',\n escaped = \"\\\"hello world\\\"\",\n triple = \"\"\"I said \"hello world\"!\"\"\",\n}\n" --- SELECT '"hello world"' AS other, '"hello world"' AS escaped, 'I said "hello world"!' AS triple FROM artists ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__strings__quoting-and-escape-characters__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from artists\nderive escapes = \"\\tXYZ\\n \\\\ \" # tab (\\t), \"XYZ\", newline (\\n), \" \", \\, \" \"\nderive world = \"\\u{0048}\\u{0065}\\u{006C}\\u{006C}\\u{006F}\" # \"Hello\"\nderive hex = \"\\x48\\x65\\x6C\\x6C\\x6F\" # \"Hello\"\nderive turtle = \"\\u{01F422}\" # \"🐢\"\n" --- SELECT *, ' XYZ \ ' AS escapes, 'Hello' AS world, 'Hello' AS hex, '🐢' AS turtle FROM artists ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__tuples__0.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nselect {first_name}\n" --- SELECT first_name FROM employees ================================================ FILE: web/book/tests/documentation/snapshots/documentation__book__reference__syntax__tuples__1.snap ================================================ --- source: web/book/tests/documentation/book.rs expression: "from employees\nselect first_name\n" --- SELECT first_name FROM employees ================================================ FILE: web/book/tests/documentation/website.rs ================================================ use std::fs::read_dir; use regex::Regex; use serde_yaml::Value; use similar_asserts::assert_eq; use super::compile; fn sql_normalize(sql: &str) -> String { let re = Regex::new(r"\n\s+").unwrap(); re.replace_all(sql, " ").trim().to_string() } #[test] fn test_website_examples() { for example in read_dir("../website/data/examples").unwrap().flatten() { let file = std::fs::File::open(example.path()).unwrap(); let example_value: Value = serde_yaml::from_reader(&file).unwrap(); let prql = example_value.get("prql").unwrap().as_str().unwrap(); let compiled_sql = compile(prql).unwrap(); if let Some(sql) = example_value.get("sql") { assert_eq!( sql_normalize(&compiled_sql), sql_normalize(sql.as_str().unwrap()), "Failed for file: {:?}", example.path() ); } } } ================================================ FILE: web/book/theme/head.hbs ================================================ ================================================ FILE: web/book/theme/highlight.js ================================================ var hljs=function(){"use strict";var e={exports:{}};function n(e){return e instanceof Map?e.clear=e.delete=e.set=()=>{throw Error("map is read-only")}:e instanceof Set&&(e.add=e.clear=e.delete=()=>{throw Error("set is read-only")}),Object.freeze(e),Object.getOwnPropertyNames(e).forEach(t=>{var a=e[t];"object"!=typeof a||Object.isFrozen(a)||n(a)}),e}e.exports=n,e.exports.default=n;var t=e.exports;class a{constructor(e){void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1}ignoreMatch(){this.isMatchIgnored=!0}}function i(e){return e.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function r(e,...n){const t=Object.create(null);for(const n in e)t[n]=e[n];return n.forEach(e=>{for(const n in e)t[n]=e[n]}),t}const s=e=>!!e.kind;class o{constructor(e,n){this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){this.buffer+=i(e)}openNode(e){if(!s(e))return;let n=e.kind;n=e.sublanguage?"language-"+n:((e,{prefix:n})=>{if(e.includes(".")){const t=e.split(".");return[`${n}${t.shift()}`,...t.map((e,n)=>`${e}${"_".repeat(n+1)}`)].join(" ")}return`${n}${e}`})(n,{prefix:this.classPrefix}),this.span(n)}closeNode(e){s(e)&&(this.buffer+="
")}value(){return this.buffer}span(e){this.buffer+=``}}class l{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){this.top.children.push(e)}openNode(e){const n={kind:e,children:[]};this.add(n),this.stack.push(n)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n),n.children.forEach(n=>this._walk(e,n)),e.closeNode(n)),e}static _collapse(e){"string"!=typeof e&&e.children&&(e.children.every(e=>"string"==typeof e)?e.children=[e.children.join("")]:e.children.forEach(e=>{l._collapse(e)}))}}class c extends l{constructor(e){super(),this.options=e}addKeyword(e,n){""!==e&&(this.openNode(n),this.addText(e),this.closeNode())}addText(e){""!==e&&this.add(e)}addSublanguage(e,n){const t=e.root;t.kind=n,t.sublanguage=!0,this.add(t)}toHTML(){return new o(this,this.options).value()}finalize(){return!0}}function d(e){return e?"string"==typeof e?e:e.source:null}function g(e){return m("(?=",e,")")}function u(e){return m("(?:",e,")*")}function b(e){return m("(?:",e,")?")}function m(...e){return e.map(e=>d(e)).join("")}function p(...e){const n=(e=>{const n=e[e.length-1];return"object"==typeof n&&n.constructor===Object?(e.splice(e.length-1,1),n):{}})(e);return"("+(n.capture?"":"?:")+e.map(e=>d(e)).join("|")+")"}function _(e){return RegExp(e.toString()+"|").exec("").length-1}const h=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;function f(e,{joinWith:n}){let t=0;return e.map(e=>{t+=1;const n=t;let a=d(e),i="";for(;a.length>0;){const e=h.exec(a);if(!e){i+=a;break}i+=a.substring(0,e.index),a=a.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?i+="\\"+(Number(e[1])+n):(i+=e[0],"("===e[0]&&t++)}return i}).map(e=>`(${e})`).join(n)}const E="[a-zA-Z]\\w*",y="[a-zA-Z_]\\w*",w="\\b\\d+(\\.\\d+)?",N="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",v="\\b(0b[01]+)",k={begin:"\\\\[\\s\\S]",relevance:0},O={scope:"string",begin:"'",end:"'",illegal:"\\n",contains:[k]},x={scope:"string",begin:'"',end:'"',illegal:"\\n",contains:[k]},M=(e,n,t={})=>{const a=r({scope:"comment",begin:e,end:n,contains:[]},t);a.contains.push({scope:"doctag",begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)",end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0});const i=p("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/);return a.contains.push({begin:m(/[ ]+/,"(",i,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),a},S=M("//","$"),A=M("/\\*","\\*/"),C=M("#","$");var T=Object.freeze({__proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:E,UNDERSCORE_IDENT_RE:y,NUMBER_RE:w,C_NUMBER_RE:N,BINARY_NUMBER_RE:v,RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",SHEBANG:(e={})=>{const n=/^#![ ]*\//;return e.binary&&(e.begin=m(n,/.*\b/,e.binary,/\b.*/)),r({scope:"meta",begin:n,end:/$/,relevance:0,"on:begin":(e,n)=>{0!==e.index&&n.ignoreMatch()}},e)},BACKSLASH_ESCAPE:k,APOS_STRING_MODE:O,QUOTE_STRING_MODE:x,PHRASAL_WORDS_MODE:{begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},COMMENT:M,C_LINE_COMMENT_MODE:S,C_BLOCK_COMMENT_MODE:A,HASH_COMMENT_MODE:C,NUMBER_MODE:{scope:"number",begin:w,relevance:0},C_NUMBER_MODE:{scope:"number",begin:N,relevance:0},BINARY_NUMBER_MODE:{scope:"number",begin:v,relevance:0},REGEXP_MODE:{begin:/(?=\/[^/\n]*\/)/,contains:[{scope:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[k,{begin:/\[/,end:/\]/,relevance:0,contains:[k]}]}]},TITLE_MODE:{scope:"title",begin:E,relevance:0},UNDERSCORE_TITLE_MODE:{scope:"title",begin:y,relevance:0},METHOD_GUARD:{begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0},END_SAME_AS_BEGIN:e=>Object.assign(e,{"on:begin":(e,n)=>{n.data._beginMatch=e[1]},"on:end":(e,n)=>{n.data._beginMatch!==e[1]&&n.ignoreMatch()}})});function R(e,n){"."===e.input[e.index-1]&&n.ignoreMatch()}function D(e,n){void 0!==e.className&&(e.scope=e.className,delete e.className)}function I(e,n){n&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",e.__beforeBegin=R,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,void 0===e.relevance&&(e.relevance=0))}function L(e,n){Array.isArray(e.illegal)&&(e.illegal=p(...e.illegal))}function B(e,n){if(e.match){if(e.begin||e.end)throw Error("begin & end are not supported with match");e.begin=e.match,delete e.match}}function $(e,n){void 0===e.relevance&&(e.relevance=1)}const z=(e,n)=>{if(!e.beforeMatch)return;if(e.starts)throw Error("beforeMatch cannot be used with starts");const t=Object.assign({},e);Object.keys(e).forEach(n=>{delete e[n]}),e.keywords=t.keywords,e.begin=m(t.beforeMatch,g(t.begin)),e.starts={relevance:0,contains:[Object.assign(t,{endsParent:!0})]},e.relevance=0,delete t.beforeMatch},F=["of","and","for","in","not","or","if","then","parent","list","value"];function U(e,n,t="keyword"){const a=Object.create(null);return"string"==typeof e?i(t,e.split(" ")):Array.isArray(e)?i(t,e):Object.keys(e).forEach(t=>{Object.assign(a,U(e[t],n,t))}),a;function i(e,t){n&&(t=t.map(e=>e.toLowerCase())),t.forEach(n=>{const t=n.split("|");a[t[0]]=[e,j(t[0],t[1])]})}}function j(e,n){return n?Number(n):(e=>F.includes(e.toLowerCase()))(e)?0:1}const P={},K=e=>{console.error(e)},H=(e,...n)=>{console.log("WARN: "+e,...n)},q=(e,n)=>{P[`${e}/${n}`]||(console.log(`Deprecated as of ${e}. ${n}`),P[`${e}/${n}`]=!0)},Z=Error();function G(e,n,{key:t}){let a=0;const i=e[t],r={},s={};for(let e=1;e<=n.length;e++)s[e+a]=i[e],r[e+a]=!0,a+=_(n[e-1]);e[t]=s,e[t]._emit=r,e[t]._multi=!0}function W(e){(e=>{e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope,delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope}),(e=>{if(Array.isArray(e.begin)){if(e.skip||e.excludeBegin||e.returnBegin)throw K("skip, excludeBegin, returnBegin not compatible with beginScope: {}"),Z;if("object"!=typeof e.beginScope||null===e.beginScope)throw K("beginScope must be object"),Z;G(e,e.begin,{key:"beginScope"}),e.begin=f(e.begin,{joinWith:""})}})(e),(e=>{if(Array.isArray(e.end)){if(e.skip||e.excludeEnd||e.returnEnd)throw K("skip, excludeEnd, returnEnd not compatible with endScope: {}"),Z;if("object"!=typeof e.endScope||null===e.endScope)throw K("endScope must be object"),Z;G(e,e.end,{key:"endScope"}),e.end=f(e.end,{joinWith:""})}})(e)}function Q(e){function n(n,t){return RegExp(d(n),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(t?"g":""))}class t{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(e,n){n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]),this.matchAt+=_(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);const e=this.regexes.map(e=>e[1]);this.matcherRe=n(f(e,{joinWith:"|"}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex;const n=this.matcherRe.exec(e);if(!n)return null;const t=n.findIndex((e,n)=>n>0&&void 0!==e),a=this.matchIndexes[t];return n.splice(0,t),Object.assign(n,a)}}class a{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){if(this.multiRegexes[e])return this.multiRegexes[e];const n=new t;return this.rules.slice(e).forEach(([e,t])=>n.addRule(e,t)),n.compile(),this.multiRegexes[e]=n,n}resumingScanAtSamePosition(){return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,n){this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){const n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex;let t=n.exec(e);if(this.resumingScanAtSamePosition())if(t&&t.index===this.lastIndex);else{const n=this.getMatcher(0);n.lastIndex=this.lastIndex+1,t=n.exec(e)}return t&&(this.regexIndex+=t.position+1,this.regexIndex===this.count&&this.considerAll()),t}}if(e.compilerExtensions||(e.compilerExtensions=[]),e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return e.classNameAliases=r(e.classNameAliases||{}),function t(i,s){const o=i;if(i.isCompiled)return o;[D,B,W,z].forEach(e=>e(i,s)),e.compilerExtensions.forEach(e=>e(i,s)),i.__beforeBegin=null,[I,L,$].forEach(e=>e(i,s)),i.isCompiled=!0;let l=null;return"object"==typeof i.keywords&&i.keywords.$pattern&&(i.keywords=Object.assign({},i.keywords),l=i.keywords.$pattern,delete i.keywords.$pattern),l=l||/\w+/,i.keywords&&(i.keywords=U(i.keywords,e.case_insensitive)),o.keywordPatternRe=n(l,!0),s&&(i.begin||(i.begin=/\B|\b/),o.beginRe=n(o.begin),i.end||i.endsWithParent||(i.end=/\B|\b/),i.end&&(o.endRe=n(o.end)),o.terminatorEnd=d(o.end)||"",i.endsWithParent&&s.terminatorEnd&&(o.terminatorEnd+=(i.end?"|":"")+s.terminatorEnd)),i.illegal&&(o.illegalRe=n(i.illegal)),i.contains||(i.contains=[]),i.contains=[].concat(...i.contains.map(e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map(n=>r(e,{variants:null},n))),e.cachedVariants?e.cachedVariants:X(e)?r(e,{starts:e.starts?r(e.starts):null}):Object.isFrozen(e)?r(e):e))("self"===e?i:e))),i.contains.forEach(e=>{t(e,o)}),i.starts&&t(i.starts,s),o.matcher=(e=>{const n=new a;return e.contains.forEach(e=>n.addRule(e.begin,{rule:e,type:"begin"})),e.terminatorEnd&&n.addRule(e.terminatorEnd,{type:"end"}),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n})(o),o}(e)}function X(e){return!!e&&(e.endsWithParent||X(e.starts))}class V extends Error{constructor(e,n){super(e),this.name="HTMLInjectionError",this.html=n}}const J=i,Y=r,ee=Symbol("nomatch");var ne=(e=>{const n=Object.create(null),i=Object.create(null),r=[];let s=!0;const o="Could not find the language '{}', did you forget to load/include a language module?",l={disableAutodetect:!0,name:"Plain text",contains:[]};let d={ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",cssSelector:"pre code",languages:null,__emitter:c};function _(e){return d.noHighlightRe.test(e)}function h(e,n,t){let a="",i="";"object"==typeof n?(a=e,t=n.ignoreIllegals,i=n.language):(q("10.7.0","highlight(lang, code, ...args) has been deprecated."),q("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),i=e,a=n),void 0===t&&(t=!0);const r={code:a,language:i};x("before:highlight",r);const s=r.result?r.result:f(r.language,r.code,t);return s.code=r.code,x("after:highlight",s),s}function f(e,t,i,r){const l=Object.create(null);function c(){if(!O.keywords)return void M.addText(S);let e=0;O.keywordPatternRe.lastIndex=0;let n=O.keywordPatternRe.exec(S),t="";for(;n;){t+=S.substring(e,n.index);const i=w.case_insensitive?n[0].toLowerCase():n[0],r=(a=i,O.keywords[a]);if(r){const[e,a]=r;if(M.addText(t),t="",l[i]=(l[i]||0)+1,l[i]<=7&&(A+=a),e.startsWith("_"))t+=n[0];else{const t=w.classNameAliases[e]||e;M.addKeyword(n[0],t)}}else t+=n[0];e=O.keywordPatternRe.lastIndex,n=O.keywordPatternRe.exec(S)}var a;t+=S.substr(e),M.addText(t)}function g(){null!=O.subLanguage?(()=>{if(""===S)return;let e=null;if("string"==typeof O.subLanguage){if(!n[O.subLanguage])return void M.addText(S);e=f(O.subLanguage,S,!0,x[O.subLanguage]),x[O.subLanguage]=e._top}else e=E(S,O.subLanguage.length?O.subLanguage:null);O.relevance>0&&(A+=e.relevance),M.addSublanguage(e._emitter,e.language)})():c(),S=""}function u(e,n){let t=1;const a=n.length-1;for(;t<=a;){if(!e._emit[t]){t++;continue}const a=w.classNameAliases[e[t]]||e[t],i=n[t];a?M.addKeyword(i,a):(S=i,c(),S=""),t++}}function b(e,n){return e.scope&&"string"==typeof e.scope&&M.openNode(w.classNameAliases[e.scope]||e.scope),e.beginScope&&(e.beginScope._wrap?(M.addKeyword(S,w.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap),S=""):e.beginScope._multi&&(u(e.beginScope,n),S="")),O=Object.create(e,{parent:{value:O}}),O}function m(e,n,t){let i=((e,n)=>{const t=e&&e.exec(n);return t&&0===t.index})(e.endRe,t);if(i){if(e["on:end"]){const t=new a(e);e["on:end"](n,t),t.isMatchIgnored&&(i=!1)}if(i){for(;e.endsParent&&e.parent;)e=e.parent;return e}}if(e.endsWithParent)return m(e.parent,n,t)}function p(e){return 0===O.matcher.regexIndex?(S+=e[0],1):(R=!0,0)}function _(e){const n=e[0],a=t.substr(e.index),i=m(O,e,a);if(!i)return ee;const r=O;O.endScope&&O.endScope._wrap?(g(),M.addKeyword(n,O.endScope._wrap)):O.endScope&&O.endScope._multi?(g(),u(O.endScope,e)):r.skip?S+=n:(r.returnEnd||r.excludeEnd||(S+=n),g(),r.excludeEnd&&(S=n));do{O.scope&&M.closeNode(),O.skip||O.subLanguage||(A+=O.relevance),O=O.parent}while(O!==i.parent);return i.starts&&b(i.starts,e),r.returnEnd?0:n.length}let h={};function y(n,r){const o=r&&r[0];if(S+=n,null==o)return g(),0;if("begin"===h.type&&"end"===r.type&&h.index===r.index&&""===o){if(S+=t.slice(r.index,r.index+1),!s){const n=Error(`0 width match regex (${e})`);throw n.languageName=e,n.badRule=h.rule,n}return 1}if(h=r,"begin"===r.type)return(e=>{const n=e[0],t=e.rule,i=new a(t),r=[t.__beforeBegin,t["on:begin"]];for(const t of r)if(t&&(t(e,i),i.isMatchIgnored))return p(n);return t.skip?S+=n:(t.excludeBegin&&(S+=n),g(),t.returnBegin||t.excludeBegin||(S=n)),b(t,e),t.returnBegin?0:n.length})(r);if("illegal"===r.type&&!i){const e=Error('Illegal lexeme "'+o+'" for mode "'+(O.scope||"")+'"');throw e.mode=O,e}if("end"===r.type){const e=_(r);if(e!==ee)return e}if("illegal"===r.type&&""===o)return 1;if(T>1e5&&T>3*r.index)throw Error("potential infinite loop, way more iterations than matches");return S+=o,o.length}const w=v(e);if(!w)throw K(o.replace("{}",e)),Error('Unknown language: "'+e+'"');const N=Q(w);let k="",O=r||N;const x={},M=new d.__emitter(d);(()=>{const e=[];for(let n=O;n!==w;n=n.parent)n.scope&&e.unshift(n.scope);e.forEach(e=>M.openNode(e))})();let S="",A=0,C=0,T=0,R=!1;try{for(O.matcher.considerAll();;){T++,R?R=!1:O.matcher.considerAll(),O.matcher.lastIndex=C;const e=O.matcher.exec(t);if(!e)break;const n=y(t.substring(C,e.index),e);C=e.index+n}return y(t.substr(C)),M.closeAllNodes(),M.finalize(),k=M.toHTML(),{language:e,value:k,relevance:A,illegal:!1,_emitter:M,_top:O}}catch(n){if(n.message&&n.message.includes("Illegal"))return{language:e,value:J(t),illegal:!0,relevance:0,_illegalBy:{message:n.message,index:C,context:t.slice(C-100,C+100),mode:n.mode,resultSoFar:k},_emitter:M};if(s)return{language:e,value:J(t),illegal:!1,relevance:0,errorRaised:n,_emitter:M,_top:O};throw n}}function E(e,t){t=t||d.languages||Object.keys(n);const a=(e=>{const n={value:J(e),illegal:!1,relevance:0,_top:l,_emitter:new d.__emitter(d)};return n._emitter.addText(e),n})(e),i=t.filter(v).filter(O).map(n=>f(n,e,!1));i.unshift(a);const r=i.sort((e,n)=>{if(e.relevance!==n.relevance)return n.relevance-e.relevance;if(e.language&&n.language){if(v(e.language).supersetOf===n.language)return 1;if(v(n.language).supersetOf===e.language)return-1}return 0}),[s,o]=r,c=s;return c.secondBest=o,c}function y(e){let n=null;const t=(e=>{let n=e.className+" ";n+=e.parentNode?e.parentNode.className:"";const t=d.languageDetectRe.exec(n);if(t){const n=v(t[1]);return n||(H(o.replace("{}",t[1])),H("Falling back to no-highlight mode for this block.",e)),n?t[1]:"no-highlight"}return n.split(/\s+/).find(e=>_(e)||v(e))})(e);if(_(t))return;if(x("before:highlightElement",{el:e,language:t}),e.children.length>0&&(d.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."),console.warn("https://github.com/highlightjs/highlight.js/wiki/security"),console.warn("The element with unescaped HTML:"),console.warn(e)),d.throwUnescapedHTML))throw new V("One of your code blocks includes unescaped HTML.",e.innerHTML);n=e;const a=n.textContent,r=t?h(a,{language:t,ignoreIllegals:!0}):E(a);e.innerHTML=r.value,((e,n,t)=>{const a=n&&i[n]||t;e.classList.add("hljs"),e.classList.add("language-"+a)})(e,t,r.language),e.result={language:r.language,re:r.relevance,relevance:r.relevance},r.secondBest&&(e.secondBest={language:r.secondBest.language,relevance:r.secondBest.relevance}),x("after:highlightElement",{el:e,result:r,text:a})}let w=!1;function N(){"loading"!==document.readyState?document.querySelectorAll(d.cssSelector).forEach(y):w=!0}function v(e){return e=(e||"").toLowerCase(),n[e]||n[i[e]]}function k(e,{languageName:n}){"string"==typeof e&&(e=[e]),e.forEach(e=>{i[e.toLowerCase()]=n})}function O(e){const n=v(e);return n&&!n.disableAutodetect}function x(e,n){const t=e;r.forEach(e=>{e[t]&&e[t](n)})}"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",()=>{w&&N()},!1),Object.assign(e,{highlight:h,highlightAuto:E,highlightAll:N,highlightElement:y,highlightBlock:e=>(q("10.7.0","highlightBlock will be removed entirely in v12.0"),q("10.7.0","Please use highlightElement now."),y(e)),configure:e=>{d=Y(d,e)},initHighlighting:()=>{N(),q("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")},initHighlightingOnLoad:()=>{N(),q("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.")},registerLanguage:(t,a)=>{let i=null;try{i=a(e)}catch(e){if(K("Language definition for '{}' could not be registered.".replace("{}",t)),!s)throw e;K(e),i=l}i.name||(i.name=t),n[t]=i,i.rawDefinition=a.bind(null,e),i.aliases&&k(i.aliases,{languageName:t})},unregisterLanguage:e=>{delete n[e];for(const n of Object.keys(i))i[n]===e&&delete i[n]},listLanguages:()=>Object.keys(n),getLanguage:v,registerAliases:k,autoDetection:O,inherit:Y,addPlugin:e=>{(e=>{e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=n=>{e["before:highlightBlock"](Object.assign({block:n.el},n))}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=n=>{e["after:highlightBlock"](Object.assign({block:n.el},n))})})(e),r.push(e)}}),e.debugMode=()=>{s=!1},e.safeMode=()=>{s=!0},e.versionString="11.5.0",e.regex={concat:m,lookahead:g,either:p,optional:b,anyNumberOfTimes:u};for(const e in T)"object"==typeof T[e]&&t(T[e]);return Object.assign(e,T),e})({});const te=e=>({IMPORTANT:{scope:"meta",begin:"!important"},BLOCK_COMMENT:e.C_BLOCK_COMMENT_MODE,HEXCOLOR:{scope:"number",begin:/#(([0-9a-fA-F]{3,4})|(([0-9a-fA-F]{2}){3,4}))\b/},FUNCTION_DISPATCH:{className:"built_in",begin:/[\w-]+(?=\()/},ATTRIBUTE_SELECTOR_MODE:{scope:"selector-attr",begin:/\[/,end:/\]/,illegal:"$",contains:[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},CSS_NUMBER_MODE:{scope:"number",begin:e.NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},CSS_VARIABLE:{className:"attr",begin:/--[A-Za-z][A-Za-z0-9_-]*/}}),ae=["a","abbr","address","article","aside","audio","b","blockquote","body","button","canvas","caption","cite","code","dd","del","details","dfn","div","dl","dt","em","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","header","hgroup","html","i","iframe","img","input","ins","kbd","label","legend","li","main","mark","menu","nav","object","ol","p","q","quote","samp","section","span","strong","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","ul","var","video"],ie=["any-hover","any-pointer","aspect-ratio","color","color-gamut","color-index","device-aspect-ratio","device-height","device-width","display-mode","forced-colors","grid","height","hover","inverted-colors","monochrome","orientation","overflow-block","overflow-inline","pointer","prefers-color-scheme","prefers-contrast","prefers-reduced-motion","prefers-reduced-transparency","resolution","scan","scripting","update","width","min-width","max-width","min-height","max-height"],re=["active","any-link","blank","checked","current","default","defined","dir","disabled","drop","empty","enabled","first","first-child","first-of-type","fullscreen","future","focus","focus-visible","focus-within","has","host","host-context","hover","indeterminate","in-range","invalid","is","lang","last-child","last-of-type","left","link","local-link","not","nth-child","nth-col","nth-last-child","nth-last-col","nth-last-of-type","nth-of-type","only-child","only-of-type","optional","out-of-range","past","placeholder-shown","read-only","read-write","required","right","root","scope","target","target-within","user-invalid","valid","visited","where"],se=["after","backdrop","before","cue","cue-region","first-letter","first-line","grammar-error","marker","part","placeholder","selection","slotted","spelling-error"],oe=["align-content","align-items","align-self","all","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","block-size","border","border-block","border-block-color","border-block-end","border-block-end-color","border-block-end-style","border-block-end-width","border-block-start","border-block-start-color","border-block-start-style","border-block-start-width","border-block-style","border-block-width","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-inline","border-inline-color","border-inline-end","border-inline-end-color","border-inline-end-style","border-inline-end-width","border-inline-start","border-inline-start-color","border-inline-start-style","border-inline-start-width","border-inline-style","border-inline-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","clip-path","clip-rule","color","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","contain","content","content-visibility","counter-increment","counter-reset","cue","cue-after","cue-before","cursor","direction","display","empty-cells","filter","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","flow","font","font-display","font-family","font-feature-settings","font-kerning","font-language-override","font-size","font-size-adjust","font-smoothing","font-stretch","font-style","font-synthesis","font-variant","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-variation-settings","font-weight","gap","glyph-orientation-vertical","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","ime-mode","inline-size","isolation","justify-content","left","letter-spacing","line-break","line-height","list-style","list-style-image","list-style-position","list-style-type","margin","margin-block","margin-block-end","margin-block-start","margin-bottom","margin-inline","margin-inline-end","margin-inline-start","margin-left","margin-right","margin-top","marks","mask","mask-border","mask-border-mode","mask-border-outset","mask-border-repeat","mask-border-slice","mask-border-source","mask-border-width","mask-clip","mask-composite","mask-image","mask-mode","mask-origin","mask-position","mask-repeat","mask-size","mask-type","max-block-size","max-height","max-inline-size","max-width","min-block-size","min-height","min-inline-size","min-width","mix-blend-mode","nav-down","nav-index","nav-left","nav-right","nav-up","none","normal","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-wrap","overflow-x","overflow-y","padding","padding-block","padding-block-end","padding-block-start","padding-bottom","padding-inline","padding-inline-end","padding-inline-start","padding-left","padding-right","padding-top","page-break-after","page-break-before","page-break-inside","pause","pause-after","pause-before","perspective","perspective-origin","pointer-events","position","quotes","resize","rest","rest-after","rest-before","right","row-gap","scroll-margin","scroll-margin-block","scroll-margin-block-end","scroll-margin-block-start","scroll-margin-bottom","scroll-margin-inline","scroll-margin-inline-end","scroll-margin-inline-start","scroll-margin-left","scroll-margin-right","scroll-margin-top","scroll-padding","scroll-padding-block","scroll-padding-block-end","scroll-padding-block-start","scroll-padding-bottom","scroll-padding-inline","scroll-padding-inline-end","scroll-padding-inline-start","scroll-padding-left","scroll-padding-right","scroll-padding-top","scroll-snap-align","scroll-snap-stop","scroll-snap-type","scrollbar-color","scrollbar-gutter","scrollbar-width","shape-image-threshold","shape-margin","shape-outside","speak","speak-as","src","tab-size","table-layout","text-align","text-align-all","text-align-last","text-combine-upright","text-decoration","text-decoration-color","text-decoration-line","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-indent","text-justify","text-orientation","text-overflow","text-rendering","text-shadow","text-transform","text-underline-position","top","transform","transform-box","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","writing-mode","z-index"].reverse(),le=re.concat(se);var ce="\\.([0-9](_*[0-9])*)",de="[0-9a-fA-F](_*[0-9a-fA-F])*",ge={className:"number",variants:[{begin:`(\\b([0-9](_*[0-9])*)((${ce})|\\.)?|(${ce}))[eE][+-]?([0-9](_*[0-9])*)[fFdD]?\\b`},{begin:`\\b([0-9](_*[0-9])*)((${ce})[fFdD]?\\b|\\.([fFdD]\\b)?)`},{begin:`(${ce})[fFdD]?\\b`},{begin:"\\b([0-9](_*[0-9])*)[fFdD]\\b"},{begin:`\\b0[xX]((${de})\\.?|(${de})?\\.(${de}))[pP][+-]?([0-9](_*[0-9])*)[fFdD]?\\b`},{begin:"\\b(0|[1-9](_*[0-9])*)[lL]?\\b"},{begin:`\\b0[xX](${de})[lL]?\\b`},{begin:"\\b0(_*[0-7])*[lL]?\\b"},{begin:"\\b0[bB][01](_*[01])*[lL]?\\b"}],relevance:0};function ue(e,n,t){return-1===t?"":e.replace(n,a=>ue(e,n,t-1))}const be="[A-Za-z$_][0-9A-Za-z$_]*",me=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","case","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],pe=["true","false","null","undefined","NaN","Infinity"],_e=["Object","Function","Boolean","Symbol","Math","Date","Number","BigInt","String","RegExp","Array","Float32Array","Float64Array","Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Int32Array","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array","Set","Map","WeakSet","WeakMap","ArrayBuffer","SharedArrayBuffer","Atomics","DataView","JSON","Promise","Generator","GeneratorFunction","AsyncFunction","Reflect","Proxy","Intl","WebAssembly"],he=["Error","EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"],fe=["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],Ee=["arguments","this","super","console","window","document","localStorage","module","global"],ye=[].concat(fe,_e,he);function we(e){const n=e.regex,t=be,a={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(e,n)=>{const t=e[0].length+e.index,a=e.input[t];if("<"===a||","===a)return void n.ignoreMatch();let i;">"===a&&(((e,{after:n})=>{const t="",O={match:[/const|var|let/,/\s+/,t,/\s*/,/=\s*/,/(async\s*)?/,n.lookahead(k)],keywords:"async",className:{1:"keyword",3:"title.function"},contains:[_]};return{name:"Javascript",aliases:["js","jsx","mjs","cjs"],keywords:i,exports:{PARAMS_CONTAINS:p,CLASS_REFERENCE:f},illegal:/#(?![$_A-z])/,contains:[e.SHEBANG({label:"shebang",binary:"node",relevance:5}),{label:"use_strict",className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,c,d,g,u,o,f,{className:"attr",begin:t+n.lookahead(":"),relevance:0},O,{begin:"("+e.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",relevance:0,contains:[u,e.REGEXP_MODE,{className:"function",begin:k,returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:e.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:i,contains:p}]}]},{begin:/,/,relevance:0},{match:/\s+/,relevance:0},{variants:[{begin:"<>",end:""},{match:/<[A-Za-z0-9\\._:-]+\s*\/>/},{begin:a.begin,"on:begin":a.isTrulyOpeningTag,end:a.end}],subLanguage:"xml",contains:[{begin:a.begin,end:a.end,skip:!0,contains:["self"]}]}]},E,{beginKeywords:"while if case catch for"},{begin:"\\b(?!function)"+e.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",returnBegin:!0,label:"func.def",contains:[_,e.inherit(e.TITLE_MODE,{begin:t,className:"title.function"})]},{match:/\.\.\./,relevance:0},N,{match:"\\$"+t,relevance:0},{match:[/\bconstructor(?=\s*\()/],className:{1:"title.function"},contains:[_]},y,{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/,className:"variable.constant"},h,v,{match:/\$[(.]/}]}}const Ne=e=>m(/\b/,e,/\w$/.test(e)?/\b/:/\B/),ve=["Protocol","Type"].map(Ne),ke=["init","self"].map(Ne),Oe=["Any","Self"],xe=["actor","associatedtype","async","await",/as\?/,/as!/,"as","break","case","catch","class","continue","convenience","default","defer","deinit","didSet","do","dynamic","else","enum","extension","fallthrough",/fileprivate\(set\)/,"fileprivate","final","for","func","get","guard","if","import","indirect","infix",/init\?/,/init!/,"inout",/internal\(set\)/,"internal","in","is","isolated","nonisolated","lazy","let","mutating","nonmutating",/open\(set\)/,"open","operator","optional","override","postfix","precedencegroup","prefix",/private\(set\)/,"private","protocol",/public\(set\)/,"public","repeat","required","rethrows","return","set","some","static","struct","subscript","super","case","throws","throw",/try\?/,/try!/,"try","typealias",/unowned\(safe\)/,/unowned\(unsafe\)/,"unowned","var","weak","where","while","willSet"],Me=["false","nil","true"],Se=["assignment","associativity","higherThan","left","lowerThan","none","right"],Ae=["#colorLiteral","#column","#dsohandle","#else","#elseif","#endif","#error","#file","#fileID","#fileLiteral","#filePath","#function","#if","#imageLiteral","#keyPath","#line","#selector","#sourceLocation","#warn_unqualified_access","#warning"],Ce=["abs","all","any","assert","assertionFailure","debugPrint","dump","fatalError","getVaList","isKnownUniquelyReferenced","max","min","numericCast","pointwiseMax","pointwiseMin","precondition","preconditionFailure","print","readLine","repeatElement","sequence","stride","swap","swift_unboxFromSwiftValueWithType","transcode","type","unsafeBitCast","unsafeDowncast","withExtendedLifetime","withUnsafeMutablePointer","withUnsafePointer","withVaList","withoutActuallyEscaping","zip"],Te=p(/[/=\-+!*%<>&|^~?]/,/[\u00A1-\u00A7]/,/[\u00A9\u00AB]/,/[\u00AC\u00AE]/,/[\u00B0\u00B1]/,/[\u00B6\u00BB\u00BF\u00D7\u00F7]/,/[\u2016-\u2017]/,/[\u2020-\u2027]/,/[\u2030-\u203E]/,/[\u2041-\u2053]/,/[\u2055-\u205E]/,/[\u2190-\u23FF]/,/[\u2500-\u2775]/,/[\u2794-\u2BFF]/,/[\u2E00-\u2E7F]/,/[\u3001-\u3003]/,/[\u3008-\u3020]/,/[\u3030]/),Re=p(Te,/[\u0300-\u036F]/,/[\u1DC0-\u1DFF]/,/[\u20D0-\u20FF]/,/[\uFE00-\uFE0F]/,/[\uFE20-\uFE2F]/),De=m(Te,Re,"*"),Ie=p(/[a-zA-Z_]/,/[\u00A8\u00AA\u00AD\u00AF\u00B2-\u00B5\u00B7-\u00BA]/,/[\u00BC-\u00BE\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]/,/[\u0100-\u02FF\u0370-\u167F\u1681-\u180D\u180F-\u1DBF]/,/[\u1E00-\u1FFF]/,/[\u200B-\u200D\u202A-\u202E\u203F-\u2040\u2054\u2060-\u206F]/,/[\u2070-\u20CF\u2100-\u218F\u2460-\u24FF\u2776-\u2793]/,/[\u2C00-\u2DFF\u2E80-\u2FFF]/,/[\u3004-\u3007\u3021-\u302F\u3031-\u303F\u3040-\uD7FF]/,/[\uF900-\uFD3D\uFD40-\uFDCF\uFDF0-\uFE1F\uFE30-\uFE44]/,/[\uFE47-\uFEFE\uFF00-\uFFFD]/),Le=p(Ie,/\d/,/[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/),Be=m(Ie,Le,"*"),$e=m(/[A-Z]/,Le,"*"),ze=["autoclosure",m(/convention\(/,p("swift","block","c"),/\)/),"discardableResult","dynamicCallable","dynamicMemberLookup","escaping","frozen","GKInspectable","IBAction","IBDesignable","IBInspectable","IBOutlet","IBSegueAction","inlinable","main","nonobjc","NSApplicationMain","NSCopying","NSManaged",m(/objc\(/,Be,/\)/),"objc","objcMembers","propertyWrapper","requires_stored_property_inits","resultBuilder","testable","UIApplicationMain","unknown","usableFromInline"],Fe=["iOS","iOSApplicationExtension","macOS","macOSApplicationExtension","macCatalyst","macCatalystApplicationExtension","watchOS","watchOSApplicationExtension","tvOS","tvOSApplicationExtension","swift"];var Ue=Object.freeze({__proto__:null,grmr_bash:e=>{const n=e.regex,t={},a={begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[t]}]};Object.assign(t,{className:"variable",variants:[{begin:n.concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},a]});const i={className:"subst",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]},r={begin:/<<-?\s*(?=\w+)/,starts:{contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,className:"string"})]}},s={className:"string",begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,t,i]};i.contains.push(s);const o={begin:/\$\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},e.NUMBER_MODE,t]},l=e.SHEBANG({binary:"(fish|bash|zsh|sh|csh|ksh|tcsh|dash|scsh)",relevance:10}),c={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[e.inherit(e.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{name:"Bash",aliases:["sh"],keywords:{$pattern:/\b[a-z][a-z0-9._-]+\b/,keyword:["if","then","else","elif","fi","for","while","in","do","done","case","esac","function"],literal:["true","false"],built_in:["break","cd","continue","eval","exec","exit","export","getopts","hash","pwd","readonly","return","shift","test","times","trap","umask","unset","alias","bind","builtin","caller","command","declare","echo","enable","help","let","local","logout","mapfile","printf","read","readarray","source","type","typeset","ulimit","unalias","set","shopt","autoload","bg","bindkey","bye","cap","chdir","clone","comparguments","compcall","compctl","compdescribe","compfiles","compgroups","compquote","comptags","comptry","compvalues","dirs","disable","disown","echotc","echoti","emulate","fc","fg","float","functions","getcap","getln","history","integer","jobs","kill","limit","log","noglob","popd","print","pushd","pushln","rehash","sched","setcap","setopt","stat","suspend","ttyctl","unfunction","unhash","unlimit","unsetopt","vared","wait","whence","where","which","zcompile","zformat","zftp","zle","zmodload","zparseopts","zprof","zpty","zregexparse","zsocket","zstyle","ztcp","chcon","chgrp","chown","chmod","cp","dd","df","dir","dircolors","ln","ls","mkdir","mkfifo","mknod","mktemp","mv","realpath","rm","rmdir","shred","sync","touch","truncate","vdir","b2sum","base32","base64","cat","cksum","comm","csplit","cut","expand","fmt","fold","head","join","md5sum","nl","numfmt","od","paste","ptx","pr","sha1sum","sha224sum","sha256sum","sha384sum","sha512sum","shuf","sort","split","sum","tac","tail","tr","tsort","unexpand","uniq","wc","arch","basename","chroot","date","dirname","du","echo","env","expr","factor","groups","hostid","id","link","logname","nice","nohup","nproc","pathchk","pinky","printenv","printf","pwd","readlink","runcon","seq","sleep","stat","stdbuf","stty","tee","test","timeout","tty","uname","unlink","uptime","users","who","whoami","yes"]},contains:[l,e.SHEBANG(),c,o,e.HASH_COMMENT_MODE,r,{match:/(\/[a-z._-]+)+/},s,{className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},t]}},grmr_c:e=>{const n=e.regex,t=e.COMMENT("//","$",{contains:[{begin:/\\\n/}]}),a="[a-zA-Z_]\\w*::",i="(decltype\\(auto\\)|"+n.optional(a)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",r={className:"type",variants:[{begin:"\\b[a-z\\d_]*_t\\b"},{match:/\batomic_[a-z]{3,6}\b/}]},s={className:"string",variants:[{begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",end:"'",illegal:"."},e.END_SAME_AS_BEGIN({begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},o={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},l={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"},contains:[{begin:/\\\n/,relevance:0},e.inherit(s,{className:"string"}),{className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},c={className:"title",begin:n.optional(a)+e.IDENT_RE,relevance:0},d=n.optional(a)+e.IDENT_RE+"\\s*\\(",g={keyword:["asm","auto","break","case","continue","default","do","else","enum","extern","for","fortran","goto","if","inline","register","restrict","return","sizeof","struct","case","typedef","union","volatile","while","_Alignas","_Alignof","_Atomic","_Generic","_Noreturn","_Static_assert","_Thread_local","alignas","alignof","noreturn","static_assert","thread_local","_Pragma"],type:["float","double","signed","unsigned","int","short","long","char","void","_Bool","_Complex","_Imaginary","_Decimal32","_Decimal64","_Decimal128","const","static","complex","bool","imaginary"],literal:"true false NULL",built_in:"std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream auto_ptr deque list queue stack vector map set pair bitset multiset multimap unordered_set unordered_map unordered_multiset unordered_multimap priority_queue make_pair array shared_ptr abort terminate abs acos asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan vfprintf vprintf vsprintf endl initializer_list unique_ptr"},u=[l,r,t,e.C_BLOCK_COMMENT_MODE,o,s],b={variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}],keywords:g,contains:u.concat([{begin:/\(/,end:/\)/,keywords:g,contains:u.concat(["self"]),relevance:0}]),relevance:0},m={begin:"("+i+"[\\*&\\s]+)+"+d,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:g,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:"decltype\\(auto\\)",keywords:g,relevance:0},{begin:d,returnBegin:!0,contains:[e.inherit(c,{className:"title.function"})],relevance:0},{relevance:0,match:/,/},{className:"params",begin:/\(/,end:/\)/,keywords:g,relevance:0,contains:[t,e.C_BLOCK_COMMENT_MODE,s,o,r,{begin:/\(/,end:/\)/,keywords:g,relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,s,o,r]}]},r,t,e.C_BLOCK_COMMENT_MODE,l]};return{name:"C",aliases:["h"],keywords:g,disableAutodetect:!0,illegal:"=]/,contains:[{beginKeywords:"final class struct"},e.TITLE_MODE]}]),exports:{preprocessor:l,strings:s,keywords:g}}},grmr_cpp:e=>{const n=e.regex,t=e.COMMENT("//","$",{contains:[{begin:/\\\n/}]}),a="[a-zA-Z_]\\w*::",i="(?!struct)(decltype\\(auto\\)|"+n.optional(a)+"[a-zA-Z_]\\w*"+n.optional("<[^<>]+>")+")",r={className:"type",begin:"\\b[a-z\\d_]*_t\\b"},s={className:"string",variants:[{begin:'(u8?|U|L)?"',end:'"',illegal:"\\n",contains:[e.BACKSLASH_ESCAPE]},{begin:"(u8?|U|L)?'(\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)|.)",end:"'",illegal:"."},e.END_SAME_AS_BEGIN({begin:/(?:u8?|U|L)?R"([^()\\ ]{0,16})\(/,end:/\)([^()\\ ]{0,16})"/})]},o={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)((ll|LL|l|L)(u|U)?|(u|U)(ll|LL|l|L)?|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},l={className:"meta",begin:/#\s*[a-z]+\b/,end:/$/,keywords:{keyword:"if else elif endif define undef warning error line pragma _Pragma ifdef ifndef include"},contains:[{begin:/\\\n/,relevance:0},e.inherit(s,{className:"string"}),{className:"string",begin:/<.*?>/},t,e.C_BLOCK_COMMENT_MODE]},c={className:"title",begin:n.optional(a)+e.IDENT_RE,relevance:0},d=n.optional(a)+e.IDENT_RE+"\\s*\\(",g={type:["bool","char","char16_t","char32_t","char8_t","double","float","int","long","short","void","wchar_t","unsigned","signed","const","static"],keyword:["alignas","alignof","and","and_eq","asm","atomic_cancel","atomic_commit","atomic_noexcept","auto","bitand","bitor","break","case","catch","class","co_await","co_return","co_yield","compl","concept","const_cast|10","consteval","constexpr","constinit","continue","decltype","default","delete","do","dynamic_cast|10","else","enum","explicit","export","extern","false","final","for","friend","goto","if","import","inline","module","mutable","namespace","new","noexcept","not","not_eq","nullptr","operator","or","or_eq","override","private","protected","public","reflexpr","register","reinterpret_cast|10","requires","return","sizeof","static_assert","static_cast|10","struct","case","synchronized","template","this","thread_local","throw","transaction_safe","transaction_safe_dynamic","true","try","typedef","typeid","typename","union","using","virtual","volatile","while","xor","xor_eq"],literal:["NULL","false","nullopt","nullptr","true"],built_in:["_Pragma"],_type_hints:["any","auto_ptr","barrier","binary_semaphore","bitset","complex","condition_variable","condition_variable_any","counting_semaphore","deque","false_type","future","imaginary","initializer_list","istringstream","jthread","latch","lock_guard","multimap","multiset","mutex","optional","ostringstream","packaged_task","pair","promise","priority_queue","queue","recursive_mutex","recursive_timed_mutex","scoped_lock","set","shared_future","shared_lock","shared_mutex","shared_timed_mutex","shared_ptr","stack","string_view","stringstream","timed_mutex","thread","true_type","tuple","unique_lock","unique_ptr","unordered_map","unordered_multimap","unordered_multiset","unordered_set","variant","vector","weak_ptr","wstring","wstring_view"]},u={className:"function.dispatch",relevance:0,keywords:{_hint:["abort","abs","acos","apply","as_const","asin","atan","atan2","calloc","ceil","cerr","cin","clog","cos","cosh","cout","declval","endl","exchange","exit","exp","fabs","floor","fmod","forward","fprintf","fputs","free","frexp","fscanf","future","invoke","isalnum","isalpha","iscntrl","isdigit","isgraph","islower","isprint","ispunct","isspace","isupper","isxdigit","labs","launder","ldexp","log","log10","make_pair","make_shared","make_shared_for_overwrite","make_tuple","make_unique","malloc","memchr","memcmp","memcpy","memset","modf","move","pow","printf","putchar","puts","realloc","scanf","sin","sinh","snprintf","sprintf","sqrt","sscanf","std","stderr","stdin","stdout","strcat","strchr","strcmp","strcpy","strcspn","strlen","strncat","strncmp","strncpy","strpbrk","strrchr","strspn","strstr","swap","tan","tanh","terminate","to_underlying","tolower","toupper","vfprintf","visit","vprintf","vsprintf"]},begin:n.concat(/\b/,/(?!decltype)/,/(?!if)/,/(?!for)/,/(?!case)/,/(?!while)/,e.IDENT_RE,n.lookahead(/(<[^<>]+>|)\s*\(/))},b=[u,l,r,t,e.C_BLOCK_COMMENT_MODE,o,s],m={variants:[{begin:/=/,end:/;/},{begin:/\(/,end:/\)/},{beginKeywords:"new throw return else",end:/;/}],keywords:g,contains:b.concat([{begin:/\(/,end:/\)/,keywords:g,contains:b.concat(["self"]),relevance:0}]),relevance:0},p={className:"function",begin:"("+i+"[\\*&\\s]+)+"+d,returnBegin:!0,end:/[{;=]/,excludeEnd:!0,keywords:g,illegal:/[^\w\s\*&:<>.]/,contains:[{begin:"decltype\\(auto\\)",keywords:g,relevance:0},{begin:d,returnBegin:!0,contains:[c],relevance:0},{begin:/::/,relevance:0},{begin:/:/,endsWithParent:!0,contains:[s,o]},{relevance:0,match:/,/},{className:"params",begin:/\(/,end:/\)/,keywords:g,relevance:0,contains:[t,e.C_BLOCK_COMMENT_MODE,s,o,r,{begin:/\(/,end:/\)/,keywords:g,relevance:0,contains:["self",t,e.C_BLOCK_COMMENT_MODE,s,o,r]}]},r,t,e.C_BLOCK_COMMENT_MODE,l]};return{name:"C++",aliases:["cc","c++","h++","hpp","hh","hxx","cxx"],keywords:g,illegal:"",keywords:g,contains:["self",r]},{begin:e.IDENT_RE+"::",keywords:g},{match:[/\b(?:enum(?:\s+(?:class|struct))?|class|struct|union)/,/\s+/,/\w+/],className:{1:"keyword",3:"title.class"}}])}},grmr_csharp:e=>{const n={keyword:["abstract","as","base","break","case","catch","class","const","continue","do","else","event","explicit","extern","finally","fixed","for","foreach","goto","if","implicit","in","interface","internal","is","lock","namespace","new","operator","out","override","params","private","protected","public","readonly","record","ref","return","sealed","sizeof","stackalloc","static","struct","case","this","throw","try","typeof","unchecked","unsafe","using","virtual","void","volatile","while"].concat(["add","alias","and","ascending","async","await","by","descending","equals","from","get","global","group","init","into","join","let","nameof","not","notnull","on","or","orderby","partial","remove","select","set","unmanaged","value|0","var","when","where","with","yield"]),built_in:["bool","byte","char","decimal","delegate","double","dynamic","enum","float","int","long","nint","nuint","object","sbyte","short","string","ulong","uint","ushort"],literal:["default","false","null","true"]},t=e.inherit(e.TITLE_MODE,{begin:"[a-zA-Z](\\.?\\w)*"}),a={className:"number",variants:[{begin:"\\b(0b[01']+)"},{begin:"(-?)\\b([\\d']+(\\.[\\d']*)?|\\.[\\d']+)(u|U|l|L|ul|UL|f|F|b|B)"},{begin:"(-?)(\\b0[xX][a-fA-F0-9']+|(\\b[\\d']+(\\.[\\d']*)?|\\.[\\d']+)([eE][-+]?[\\d']+)?)"}],relevance:0},i={className:"string",begin:'@"',end:'"',contains:[{begin:'""'}]},r=e.inherit(i,{illegal:/\n/}),s={className:"subst",begin:/\{/,end:/\}/,keywords:n},o=e.inherit(s,{illegal:/\n/}),l={className:"string",begin:/\$"/,end:'"',illegal:/\n/,contains:[{begin:/\{\{/},{begin:/\}\}/},e.BACKSLASH_ESCAPE,o]},c={className:"string",begin:/\$@"/,end:'"',contains:[{begin:/\{\{/},{begin:/\}\}/},{begin:'""'},s]},d=e.inherit(c,{illegal:/\n/,contains:[{begin:/\{\{/},{begin:/\}\}/},{begin:'""'},o]});s.contains=[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.C_BLOCK_COMMENT_MODE],o.contains=[d,l,r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,a,e.inherit(e.C_BLOCK_COMMENT_MODE,{illegal:/\n/})];const g={variants:[c,l,i,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},u={begin:"<",end:">",contains:[{beginKeywords:"in out"},t]},b=e.IDENT_RE+"(<"+e.IDENT_RE+"(\\s*,\\s*"+e.IDENT_RE+")*>)?(\\[\\])?",m={begin:"@"+e.IDENT_RE,relevance:0};return{name:"C#",aliases:["cs","c#"],keywords:n,illegal:/::/,contains:[e.COMMENT("///","$",{returnBegin:!0,contains:[{className:"doctag",variants:[{begin:"///",relevance:0},{begin:"\x3c!--|--\x3e"},{begin:""}]}]}),e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{className:"meta",begin:"#",end:"$",keywords:{keyword:"if else elif endif define undef warning error line region endregion pragma checksum"}},g,a,{beginKeywords:"class interface",relevance:0,end:/[{;=]/,illegal:/[^\s:,]/,contains:[{beginKeywords:"where class"},t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"namespace",relevance:0,end:/[{;=]/,illegal:/[^\s:]/,contains:[t,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"record",relevance:0,end:/[{;=]/,illegal:/[^\s:]/,contains:[t,u,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"meta",begin:"^\\s*\\[(?=[\\w])",excludeBegin:!0,end:"\\]",excludeEnd:!0,contains:[{className:"string",begin:/"/,end:/"/}]},{beginKeywords:"new return throw await else",relevance:0},{className:"function",begin:"("+b+"\\s+)+"+e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0,end:/\s*[{;=]/,excludeEnd:!0,keywords:n,contains:[{beginKeywords:"public private protected static internal protected abstract async extern override unsafe virtual new sealed partial",relevance:0},{begin:e.IDENT_RE+"\\s*(<[^=]+>\\s*)?\\(",returnBegin:!0,contains:[e.TITLE_MODE,u],relevance:0},{match:/\(\)/},{className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:n,relevance:0,contains:[g,a,e.C_BLOCK_COMMENT_MODE]},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},m]}},grmr_css:e=>{const n=e.regex,t=te(e),a=[e.APOS_STRING_MODE,e.QUOTE_STRING_MODE];return{name:"CSS",case_insensitive:!0,illegal:/[=|'\$]/,keywords:{keyframePosition:"from to"},classNameAliases:{keyframePosition:"selector-tag"},contains:[t.BLOCK_COMMENT,{begin:/-(webkit|moz|ms|o)-(?=[a-z])/},t.CSS_NUMBER_MODE,{className:"selector-id",begin:/#[A-Za-z0-9_-]+/,relevance:0},{className:"selector-class",begin:"\\.[a-zA-Z-][a-zA-Z0-9_-]*",relevance:0},t.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",variants:[{begin:":("+re.join("|")+")"},{begin:":(:)?("+se.join("|")+")"}]},t.CSS_VARIABLE,{className:"attribute",begin:"\\b("+oe.join("|")+")\\b"},{begin:/:/,end:/[;}{]/,contains:[t.BLOCK_COMMENT,t.HEXCOLOR,t.IMPORTANT,t.CSS_NUMBER_MODE,...a,{begin:/(url|data-uri)\(/,end:/\)/,relevance:0,keywords:{built_in:"url data-uri"},contains:[{className:"string",begin:/[^)]/,endsWithParent:!0,excludeEnd:!0}]},t.FUNCTION_DISPATCH]},{begin:n.lookahead(/@/),end:"[{;]",relevance:0,illegal:/:/,contains:[{className:"keyword",begin:/@-?\w[\w]*(-\w+)*/},{begin:/\s/,endsWithParent:!0,excludeEnd:!0,relevance:0,keywords:{$pattern:/[a-z-]+/,keyword:"and or not only",attribute:ie.join(" ")},contains:[{begin:/[a-z-]+(?=:)/,className:"attribute"},...a,t.CSS_NUMBER_MODE]}]},{className:"selector-tag",begin:"\\b("+ae.join("|")+")\\b"}]}},grmr_diff:e=>{const n=e.regex;return{name:"Diff",aliases:["patch"],contains:[{className:"meta",relevance:10,match:n.either(/^@@ +-\d+,\d+ +\+\d+,\d+ +@@/,/^\*\*\* +\d+,\d+ +\*\*\*\*$/,/^--- +\d+,\d+ +----$/)},{className:"comment",variants:[{begin:n.either(/Index: /,/^index/,/={3,}/,/^-{3}/,/^\*{3} /,/^\+{3}/,/^diff --git/),end:/$/},{match:/^\*{15}$/}]},{className:"addition",begin:/^\+/,end:/$/},{className:"deletion",begin:/^-/,end:/$/},{className:"addition",begin:/^!/,end:/$/}]}},grmr_go:e=>{const n={keyword:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","case","type","var"],type:["bool","byte","complex64","complex128","error","float32","float64","int8","int16","int32","int64","string","uint8","uint16","uint32","uint64","int","uint","uintptr","rune"],literal:["true","false","iota","nil"],built_in:["append","cap","close","complex","copy","imag","len","make","new","panic","print","println","real","recover","delete"]};return{name:"Go",aliases:["golang"],keywords:n,illegal:"{const n=e.regex,t={className:"number",relevance:0,variants:[{begin:/([+-]+)?[\d]+_[\d_]+/},{begin:e.NUMBER_RE}]},a=e.COMMENT();a.variants=[{begin:/;/,end:/$/},{begin:/#/,end:/$/}];const i={className:"variable",variants:[{begin:/\$[\w\d"][\w\d_]*/},{begin:/\$\{(.*?)\}/}]},r={className:"literal",begin:/\bon|off|true|false|yes|no\b/},s={className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{begin:"'''",end:"'''",relevance:10},{begin:'"""',end:'"""',relevance:10},{begin:'"',end:'"'},{begin:"'",end:"'"}]},o={begin:/\[/,end:/\]/,contains:[a,r,i,s,t,"self"],relevance:0},l=n.either(/[A-Za-z0-9_-]+/,/"(\\"|[^"])*"/,/'[^']*'/);return{name:"TOML, also INI",aliases:["toml"],case_insensitive:!0,illegal:/\S/,contains:[a,{className:"section",begin:/\[+/,end:/\]+/},{begin:n.concat(l,"(\\s*\\.\\s*",l,")*",n.lookahead(/\s*=\s*[^#\s]/)),className:"attr",starts:{end:/$/,contains:[a,o,r,i,s,t]}}]}},grmr_java:e=>{const n=e.regex,t="[À-ʸa-zA-Z_$][À-ʸa-zA-Z_$0-9]*",a=t+ue("(?:<"+t+"~~~(?:\\s*,\\s*"+t+"~~~)*>)?",/~~~/g,2),i={keyword:["synchronized","abstract","private","var","static","if","const ","for","while","strictfp","finally","protected","import","native","final","void","enum","else","break","transient","catch","instanceof","volatile","case","assert","package","default","public","try","case","continue","throws","protected","public","private","module","requires","exports","do","sealed"],literal:["false","true","null"],type:["char","boolean","long","float","int","byte","short","double"],built_in:["super","this"]},r={className:"meta",begin:"@"+t,contains:[{begin:/\(/,end:/\)/,contains:["self"]}]},s={className:"params",begin:/\(/,end:/\)/,keywords:i,relevance:0,contains:[e.C_BLOCK_COMMENT_MODE],endsParent:!0};return{name:"Java",aliases:["jsp"],keywords:i,illegal:/<\/|#/,contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{begin:/\w+@/,relevance:0},{className:"doctag",begin:"@[A-Za-z]+"}]}),{begin:/import java\.[a-z]+\./,keywords:"import",relevance:2},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,{begin:/"""/,end:/"""/,className:"string",contains:[e.BACKSLASH_ESCAPE]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{match:[/\b(?:class|interface|enum|extends|implements|new)/,/\s+/,t],className:{1:"keyword",3:"title.class"}},{match:/non-sealed/,scope:"keyword"},{begin:[n.concat(/(?!else)/,t),/\s+/,t,/\s+/,/=/],className:{1:"type",3:"variable",5:"operator"}},{begin:[/record/,/\s+/,t],className:{1:"keyword",3:"title.class"},contains:[s,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{beginKeywords:"new throw return else",relevance:0},{begin:["(?:"+a+"\\s+)",e.UNDERSCORE_IDENT_RE,/\s*(?=\()/],className:{2:"title.function"},keywords:i,contains:[{className:"params",begin:/\(/,end:/\)/,keywords:i,relevance:0,contains:[r,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,ge,e.C_BLOCK_COMMENT_MODE]},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},ge,r]}},grmr_javascript:we,grmr_json:e=>({name:"JSON",contains:[{className:"attr",begin:/"(\\.|[^\\"\r\n])*"(?=\s*:)/,relevance:1.01},{match:/[{}[\],:]/,className:"punctuation",relevance:0},e.QUOTE_STRING_MODE,{beginKeywords:"true false null"},e.C_NUMBER_MODE,e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE],illegal:"\\S"}),grmr_kotlin:e=>{const n={keyword:"abstract as val var vararg get set class object open private protected public noinline crossinline dynamic final enum if else do while for when throw try catch finally import package is in fun override companion reified inline lateinit init interface annotation data sealed internal infix operator out by constructor super tailrec where const inner suspend typealias external expect actual",built_in:"Byte Short Char Int Long Boolean Float Double Void Unit Nothing",literal:"true false null"},t={className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"@"},a={className:"subst",begin:/\$\{/,end:/\}/,contains:[e.C_NUMBER_MODE]},i={className:"variable",begin:"\\$"+e.UNDERSCORE_IDENT_RE},r={className:"string",variants:[{begin:'"""',end:'"""(?=[^"])',contains:[i,a]},{begin:"'",end:"'",illegal:/\n/,contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"',illegal:/\n/,contains:[e.BACKSLASH_ESCAPE,i,a]}]};a.contains.push(r);const s={className:"meta",begin:"@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*"+e.UNDERSCORE_IDENT_RE+")?"},o={className:"meta",begin:"@"+e.UNDERSCORE_IDENT_RE,contains:[{begin:/\(/,end:/\)/,contains:[e.inherit(r,{className:"string"})]}]},l=ge,c=e.COMMENT("/\\*","\\*/",{contains:[e.C_BLOCK_COMMENT_MODE]}),d={variants:[{className:"type",begin:e.UNDERSCORE_IDENT_RE},{begin:/\(/,end:/\)/,contains:[]}]},g=d;return g.variants[1].contains=[d],d.variants[1].contains=[g],{name:"Kotlin",aliases:["kt","kts"],keywords:n,contains:[e.COMMENT("/\\*\\*","\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+"}]}),e.C_LINE_COMMENT_MODE,c,{className:"keyword",begin:/\b(break|continue|return|this)\b/,starts:{contains:[{className:"symbol",begin:/@\w+/}]}},t,s,o,{className:"function",beginKeywords:"fun",end:"[(]|$",returnBegin:!0,excludeEnd:!0,keywords:n,relevance:5,contains:[{begin:e.UNDERSCORE_IDENT_RE+"\\s*\\(",returnBegin:!0,relevance:0,contains:[e.UNDERSCORE_TITLE_MODE]},{className:"type",begin://,keywords:"reified",relevance:0},{className:"params",begin:/\(/,end:/\)/,endsParent:!0,keywords:n,relevance:0,contains:[{begin:/:/,end:/[=,\/]/,endsWithParent:!0,contains:[d,e.C_LINE_COMMENT_MODE,c],relevance:0},e.C_LINE_COMMENT_MODE,c,s,o,r,e.C_NUMBER_MODE]},c]},{className:"class",beginKeywords:"class interface trait",end:/[:\{(]|$/,excludeEnd:!0,illegal:"extends implements",contains:[{beginKeywords:"public protected internal private constructor"},e.UNDERSCORE_TITLE_MODE,{className:"type",begin://,excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:/[,:]\s*/,end:/[<\(,]|$/,excludeBegin:!0,returnEnd:!0},s,o]},r,{className:"meta",begin:"^#!/usr/bin/env",end:"$",illegal:"\n"},l]}},grmr_less:e=>{const n=te(e),t=le,a="([\\w-]+|@\\{[\\w-]+\\})",i=[],r=[],s=e=>({className:"string",begin:"~?"+e+".*?"+e}),o=(e,n,t)=>({className:e,begin:n,relevance:t}),l={$pattern:/[a-z-]+/,keyword:"and or not only",attribute:ie.join(" ")},c={begin:"\\(",end:"\\)",contains:r,keywords:l,relevance:0};r.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,s("'"),s('"'),n.CSS_NUMBER_MODE,{begin:"(url|data-uri)\\(",starts:{className:"string",end:"[\\)\\n]",excludeEnd:!0}},n.HEXCOLOR,c,o("variable","@@?[\\w-]+",10),o("variable","@\\{[\\w-]+\\}"),o("built_in","~?`[^`]*?`"),{className:"attribute",begin:"[\\w-]+\\s*:",end:":",returnBegin:!0,excludeEnd:!0},n.IMPORTANT);const d=r.concat({begin:/\{/,end:/\}/,contains:i}),g={beginKeywords:"when",endsWithParent:!0,contains:[{beginKeywords:"and not"}].concat(r)},u={begin:a+"\\s*:",returnBegin:!0,end:/[;}]/,relevance:0,contains:[{begin:/-(webkit|moz|ms|o)-/},n.CSS_VARIABLE,{className:"attribute",begin:"\\b("+oe.join("|")+")\\b",end:/(?=:)/,starts:{endsWithParent:!0,illegal:"[<=$]",relevance:0,contains:r}}]},b={className:"keyword",begin:"@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b",starts:{end:"[;{}]",keywords:l,returnEnd:!0,contains:r,relevance:0}},m={className:"variable",variants:[{begin:"@[\\w-]+\\s*:",relevance:15},{begin:"@[\\w-]+"}],starts:{end:"[;}]",returnEnd:!0,contains:d}},p={variants:[{begin:"[\\.#:&\\[>]",end:"[;{}]"},{begin:a,end:/\{/}],returnBegin:!0,returnEnd:!0,illegal:"[<='$\"]",relevance:0,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,g,o("keyword","all\\b"),o("variable","@\\{[\\w-]+\\}"),{begin:"\\b("+ae.join("|")+")\\b",className:"selector-tag"},n.CSS_NUMBER_MODE,o("selector-tag",a,0),o("selector-id","#"+a),o("selector-class","\\."+a,0),o("selector-tag","&",0),n.ATTRIBUTE_SELECTOR_MODE,{className:"selector-pseudo",begin:":("+re.join("|")+")"},{className:"selector-pseudo",begin:":(:)?("+se.join("|")+")"},{begin:/\(/,end:/\)/,relevance:0,contains:d},{begin:"!important"},n.FUNCTION_DISPATCH]},_={begin:`[\\w-]+:(:)?(${t.join("|")})`,returnBegin:!0,contains:[p]};return i.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,b,m,_,u,p),{name:"Less",case_insensitive:!0,illegal:"[=>'/<($\"]",contains:i}},grmr_lua:e=>{const n="\\[=*\\[",t="\\]=*\\]",a={begin:n,end:t,contains:["self"]},i=[e.COMMENT("--(?!\\[=*\\[)","$"),e.COMMENT("--\\[=*\\[",t,{contains:[a],relevance:10})];return{name:"Lua",keywords:{$pattern:e.UNDERSCORE_IDENT_RE,literal:"true false nil",keyword:"and break do else elseif end for goto if in local not or repeat return then until while",built_in:"_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove"},contains:i.concat([{className:"function",beginKeywords:"function",end:"\\)",contains:[e.inherit(e.TITLE_MODE,{begin:"([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*"}),{className:"params",begin:"\\(",endsWithParent:!0,contains:i}].concat(i)},e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:"string",begin:n,end:t,contains:[a],relevance:5}])}},grmr_makefile:e=>{const n={className:"variable",variants:[{begin:"\\$\\("+e.UNDERSCORE_IDENT_RE+"\\)",contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%{const n=e.regex,t=n.concat(/[A-Z_]/,n.optional(/[A-Z0-9_.-]*:/),/[A-Z0-9_.-]*/),a={className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},i={begin:/\s/,contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]},r=e.inherit(i,{begin:/\(/,end:/\)/}),s=e.inherit(e.APOS_STRING_MODE,{className:"string"}),o=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),l={endsWithParent:!0,illegal:/`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin://,relevance:10,contains:[i,o,s,r,{begin:/\[/,end:/\]/,contains:[{className:"meta",begin://,contains:[i,r,o,s]}]}]},e.COMMENT(//,{relevance:10}),{begin://,relevance:10},a,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/,relevance:10,contains:[o]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag",begin:/)/,end:/>/,keywords:{name:"style"},contains:[l],starts:{end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",begin:/)/,end:/>/,keywords:{name:"script"},contains:[l],starts:{end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{className:"tag",begin:/<>|<\/>/},{className:"tag",begin:n.concat(//,/>/,/\s/)))),end:/\/?>/,contains:[{className:"name",begin:t,relevance:0,starts:l}]},{className:"tag",begin:n.concat(/<\//,n.lookahead(n.concat(t,/>/))),contains:[{className:"name",begin:t,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}},grmr_markdown:e=>{const n={begin:/<\/?[A-Za-z_]/,end:">",subLanguage:"xml",relevance:0},t={variants:[{begin:/\[.+?\]\[.*?\]/,relevance:0},{begin:/\[.+?\]\(((data|javascript|mailto):|(?:http|ftp)s?:\/\/).*?\)/,relevance:2},{begin:e.regex.concat(/\[.+?\]\(/,/[A-Za-z][A-Za-z0-9+.-]*/,/:\/\/.*?\)/),relevance:2},{begin:/\[.+?\]\([./?&#].*?\)/,relevance:1},{begin:/\[.*?\]\(.*?\)/,relevance:0}],returnBegin:!0,contains:[{match:/\[(?=\])/},{className:"string",relevance:0,begin:"\\[",end:"\\]",excludeBegin:!0,returnEnd:!0},{className:"link",relevance:0,begin:"\\]\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0},{className:"symbol",relevance:0,begin:"\\]\\[",end:"\\]",excludeBegin:!0,excludeEnd:!0}]},a={className:"strong",contains:[],variants:[{begin:/_{2}/,end:/_{2}/},{begin:/\*{2}/,end:/\*{2}/}]},i={className:"emphasis",contains:[],variants:[{begin:/\*(?!\*)/,end:/\*/},{begin:/_(?!_)/,end:/_/,relevance:0}]},r=e.inherit(a,{contains:[]}),s=e.inherit(i,{contains:[]});a.contains.push(s),i.contains.push(r);let o=[n,t];return[a,i,r,s].forEach(e=>{e.contains=e.contains.concat(o)}),o=o.concat(a,i),{name:"Markdown",aliases:["md","mkdown","mkd"],contains:[{className:"section",variants:[{begin:"^#{1,6}",end:"$",contains:o},{begin:"(?=^.+?\\n[=-]{2,}$)",contains:[{begin:"^[=-]*$"},{begin:"^",end:"\\n",contains:o}]}]},n,{className:"bullet",begin:"^[ \t]*([*+-]|(\\d+\\.))(?=\\s+)",end:"\\s+",excludeEnd:!0},a,i,{className:"quote",begin:"^>\\s+",contains:o,end:"$"},{className:"code",variants:[{begin:"(`{3,})[^`](.|\\n)*?\\1`*[ ]*"},{begin:"(~{3,})[^~](.|\\n)*?\\1~*[ ]*"},{begin:"```",end:"```+[ ]*$"},{begin:"~~~",end:"~~~+[ ]*$"},{begin:"`.+?`"},{begin:"(?=^( {4}|\\t))",contains:[{begin:"^( {4}|\\t)",end:"(\\n)$"}],relevance:0}]},{begin:"^[-\\*]{3,}",end:"$"},t,{begin:/^\[[^\n]+\]:/,returnBegin:!0,contains:[{className:"symbol",begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0},{className:"link",begin:/:\s*/,end:/$/,excludeBegin:!0}]}]}},grmr_objectivec:e=>{const n=/[a-zA-Z@][a-zA-Z0-9_]*/,t={$pattern:n,keyword:["@interface","@class","@protocol","@implementation"]};return{name:"Objective-C",aliases:["mm","objc","obj-c","obj-c++","objective-c++"],keywords:{"variable.language":["this","super"],$pattern:n,keyword:["while","export","sizeof","typedef","const","struct","for","union","volatile","static","mutable","if","do","return","goto","enum","else","break","extern","asm","case","default","register","explicit","typename","case","continue","inline","readonly","assign","readwrite","self","@synchronized","id","typeof","nonatomic","IBOutlet","IBAction","strong","weak","copy","in","out","inout","bycopy","byref","oneway","__strong","__weak","__block","__autoreleasing","@private","@protected","@public","@try","@property","@end","@throw","@catch","@finally","@autoreleasepool","@synthesize","@dynamic","@selector","@optional","@required","@encode","@package","@import","@defs","@compatibility_alias","__bridge","__bridge_transfer","__bridge_retained","__bridge_retain","__covariant","__contravariant","__kindof","_Nonnull","_Nullable","_Null_unspecified","__FUNCTION__","__PRETTY_FUNCTION__","__attribute__","getter","setter","retain","unsafe_unretained","nonnull","nullable","null_unspecified","null_resettable","class","instancetype","NS_DESIGNATED_INITIALIZER","NS_UNAVAILABLE","NS_REQUIRES_SUPER","NS_RETURNS_INNER_POINTER","NS_INLINE","NS_AVAILABLE","NS_DEPRECATED","NS_ENUM","NS_OPTIONS","NS_SWIFT_UNAVAILABLE","NS_ASSUME_NONNULL_BEGIN","NS_ASSUME_NONNULL_END","NS_REFINED_FOR_SWIFT","NS_SWIFT_NAME","NS_SWIFT_NOTHROW","NS_DURING","NS_HANDLER","NS_ENDHANDLER","NS_VALUERETURN","NS_VOIDRETURN"],literal:["false","true","FALSE","TRUE","nil","YES","NO","NULL"],built_in:["dispatch_once_t","dispatch_queue_t","dispatch_sync","dispatch_async","dispatch_once"],type:["int","float","char","unsigned","signed","short","long","double","wchar_t","unichar","void","bool","BOOL","id|0","_Bool"]},illegal:"/,end:/$/,illegal:"\\n"},e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE]},{className:"class",begin:"("+t.keyword.join("|")+")\\b",end:/(\{|$)/,excludeEnd:!0,keywords:t,contains:[e.UNDERSCORE_TITLE_MODE]},{begin:"\\."+e.UNDERSCORE_IDENT_RE,relevance:0}]}},grmr_perl:e=>{const n=e.regex,t=/[dualxmsipngr]{0,12}/,a={$pattern:/[\w.]+/,keyword:"abs accept alarm and atan2 bind binmode bless break caller chdir chmod chomp chop chown chr chroot close closedir connect continue cos crypt dbmclose dbmopen defined delete die do dump each else elsif endgrent endhostent endnetent endprotoent endpwent endservent eof eval exec exists exit exp fcntl fileno flock for foreach fork format formline getc getgrent getgrgid getgrnam gethostbyaddr gethostbyname gethostent getlogin getnetbyaddr getnetbyname getnetent getpeername getpgrp getpriority getprotobyname getprotobynumber getprotoent getpwent getpwnam getpwuid getservbyname getservbyport getservent getsockname getsockopt given glob gmtime goto grep gt hex if index int ioctl join keys kill last lc lcfirst length link listen local localtime log lstat lt ma map mkdir msgctl msgget msgrcv msgsnd my ne next no not oct open opendir or ord our pack package pipe pop pos print printf prototype push q|0 qq quotemeta qw qx rand read readdir readline readlink readpipe recv redo ref rename require reset return reverse rewinddir rindex rmdir say scalar seek seekdir select semctl semget semop send setgrent sethostent setnetent setpgrp setpriority setprotoent setpwent setservent setsockopt shift shmctl shmget shmread shmwrite shutdown sin sleep socket socketpair sort splice split sprintf sqrt srand stat state study sub substr symlink syscall sysopen sysread sysseek system syswrite tell telldir tie tied time times tr truncate uc ucfirst umask undef unless unlink unpack unshift untie until use utime values vec wait waitpid wantarray warn when while write x|0 xor y|0"},i={className:"subst",begin:"[$@]\\{",end:"\\}",keywords:a},r={begin:/->\{/,end:/\}/},s={variants:[{begin:/\$\d/},{begin:n.concat(/[$%@](\^\w\b|#\w+(::\w+)*|\{\w+\}|\w+(::\w*)*)/,"(?![A-Za-z])(?![@$%])")},{begin:/[$%@][^\s\w{]/,relevance:0}]},o=[e.BACKSLASH_ESCAPE,i,s],l=[/!/,/\//,/\|/,/\?/,/'/,/"/,/#/],c=(e,a,i="\\1")=>{const r="\\1"===i?i:n.concat(i,a);return n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,r,/(?:\\.|[^\\\/])*?/,i,t)},d=(e,a,i)=>n.concat(n.concat("(?:",e,")"),a,/(?:\\.|[^\\\/])*?/,i,t),g=[s,e.HASH_COMMENT_MODE,e.COMMENT(/^=\w/,/=cut/,{endsWithParent:!0}),r,{className:"string",contains:o,variants:[{begin:"q[qwxr]?\\s*\\(",end:"\\)",relevance:5},{begin:"q[qwxr]?\\s*\\[",end:"\\]",relevance:5},{begin:"q[qwxr]?\\s*\\{",end:"\\}",relevance:5},{begin:"q[qwxr]?\\s*\\|",end:"\\|",relevance:5},{begin:"q[qwxr]?\\s*<",end:">",relevance:5},{begin:"qw\\s+q",end:"q",relevance:5},{begin:"'",end:"'",contains:[e.BACKSLASH_ESCAPE]},{begin:'"',end:'"'},{begin:"`",end:"`",contains:[e.BACKSLASH_ESCAPE]},{begin:/\{\w+\}/,relevance:0},{begin:"-?\\w+\\s*=>",relevance:0}]},{className:"number",begin:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",relevance:0},{begin:"(\\/\\/|"+e.RE_STARTERS_RE+"|\\b(split|return|print|reverse|grep)\\b)\\s*",keywords:"split return print reverse grep",relevance:0,contains:[e.HASH_COMMENT_MODE,{className:"regexp",variants:[{begin:c("s|tr|y",n.either(...l,{capture:!0}))},{begin:c("s|tr|y","\\(","\\)")},{begin:c("s|tr|y","\\[","\\]")},{begin:c("s|tr|y","\\{","\\}")}],relevance:2},{className:"regexp",variants:[{begin:/(m|qr)\/\//,relevance:0},{begin:d("(?:m|qr)?",/\//,/\//)},{begin:d("m|qr",n.either(...l,{capture:!0}),/\1/)},{begin:d("m|qr",/\(/,/\)/)},{begin:d("m|qr",/\[/,/\]/)},{begin:d("m|qr",/\{/,/\}/)}]}]},{className:"function",beginKeywords:"sub",end:"(\\s*\\(.*?\\))?[;{]",excludeEnd:!0,relevance:5,contains:[e.TITLE_MODE]},{begin:"-\\w\\b",relevance:0},{begin:"^__DATA__$",end:"^__END__$",subLanguage:"mojolicious",contains:[{begin:"^@@.*",end:"$",className:"comment"}]}];return i.contains=g,r.contains=g,{name:"Perl",aliases:["pl","pm"],keywords:a,contains:g}},grmr_php:e=>{const n=e.regex,t=/(?![A-Za-z0-9])(?![$])/,a=n.concat(/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/,t),i=n.concat(/(\\?[A-Z][a-z0-9_\x7f-\xff]+|\\?[A-Z]+(?=[A-Z][a-z0-9_\x7f-\xff])){1,}/,t),r={scope:"variable",match:"\\$+"+a},s={scope:"subst",variants:[{begin:/\$\w+/},{begin:/\{\$/,end:/\}/}]},o=e.inherit(e.APOS_STRING_MODE,{illegal:null}),l="[ \t\n]",c={scope:"string",variants:[e.inherit(e.QUOTE_STRING_MODE,{illegal:null,contains:e.QUOTE_STRING_MODE.contains.concat(s)}),o,e.END_SAME_AS_BEGIN({begin:/<<<[ \t]*(\w+)\n/,end:/[ \t]*(\w+)\b/,contains:e.QUOTE_STRING_MODE.contains.concat(s)})]},d={scope:"number",variants:[{begin:"\\b0[bB][01]+(?:_[01]+)*\\b"},{begin:"\\b0[oO][0-7]+(?:_[0-7]+)*\\b"},{begin:"\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b"},{begin:"(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?"}],relevance:0},g=["false","null","true"],u=["__CLASS__","__DIR__","__FILE__","__FUNCTION__","__COMPILER_HALT_OFFSET__","__LINE__","__METHOD__","__NAMESPACE__","__TRAIT__","die","echo","exit","include","include_once","print","require","require_once","array","abstract","and","as","binary","bool","boolean","break","callable","case","catch","class","clone","const","continue","declare","default","do","double","else","elseif","empty","enddeclare","endfor","endforeach","endif","endcase","endwhile","enum","eval","extends","final","finally","float","for","foreach","from","global","goto","if","implements","instanceof","insteadof","int","integer","interface","isset","iterable","list","match|0","mixed","new","never","object","or","private","protected","public","readonly","real","return","string","case","throw","trait","try","unset","use","var","void","while","xor","yield"],b=["Error|0","AppendIterator","ArgumentCountError","ArithmeticError","ArrayIterator","ArrayObject","AssertionError","BadFunctionCallException","BadMethodCallException","CachingIterator","CallbackFilterIterator","CompileError","Countable","DirectoryIterator","DivisionByZeroError","DomainException","EmptyIterator","ErrorException","Exception","FilesystemIterator","FilterIterator","GlobIterator","InfiniteIterator","InvalidArgumentException","IteratorIterator","LengthException","LimitIterator","LogicException","MultipleIterator","NoRewindIterator","OutOfBoundsException","OutOfRangeException","OuterIterator","OverflowException","ParentIterator","ParseError","RangeException","RecursiveArrayIterator","RecursiveCachingIterator","RecursiveCallbackFilterIterator","RecursiveDirectoryIterator","RecursiveFilterIterator","RecursiveIterator","RecursiveIteratorIterator","RecursiveRegexIterator","RecursiveTreeIterator","RegexIterator","RuntimeException","SeekableIterator","SplDoublyLinkedList","SplFileInfo","SplFileObject","SplFixedArray","SplHeap","SplMaxHeap","SplMinHeap","SplObjectStorage","SplObserver","SplPriorityQueue","SplQueue","SplStack","SplSubject","SplTempFileObject","TypeError","UnderflowException","UnexpectedValueException","UnhandledMatchError","ArrayAccess","BackedEnum","Closure","Fiber","Generator","Iterator","IteratorAggregate","Serializable","Stringable","Throwable","Traversable","UnitEnum","WeakReference","WeakMap","Directory","__PHP_Incomplete_Class","parent","php_user_filter","self","static","stdClass"],m={keyword:u,literal:(e=>{const n=[];return e.forEach(e=>{n.push(e),e.toLowerCase()===e?n.push(e.toUpperCase()):n.push(e.toLowerCase())}),n})(g),built_in:b},p=e=>e.map(e=>e.replace(/\|\d+$/,"")),_={variants:[{match:[/new/,n.concat(l,"+"),n.concat("(?!",p(b).join("\\b|"),"\\b)"),i],scope:{1:"keyword",4:"title.class"}}]},h=n.concat(a,"\\b(?!\\()"),f={variants:[{match:[n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{2:"variable.constant"}},{match:[/::/,/class/],scope:{2:"variable.language"}},{match:[i,n.concat(/::/,n.lookahead(/(?!class\b)/)),h],scope:{1:"title.class",3:"variable.constant"}},{match:[i,n.concat("::",n.lookahead(/(?!class\b)/))],scope:{1:"title.class"}},{match:[i,/::/,/class/],scope:{1:"title.class",3:"variable.language"}}]},E={scope:"attr",match:n.concat(a,n.lookahead(":"),n.lookahead(/(?!::)/))},y={relevance:0,begin:/\(/,end:/\)/,keywords:m,contains:[E,r,f,e.C_BLOCK_COMMENT_MODE,c,d,_]},w={relevance:0,match:[/\b/,n.concat("(?!fn\\b|function\\b|",p(u).join("\\b|"),"|",p(b).join("\\b|"),"\\b)"),a,n.concat(l,"*"),n.lookahead(/(?=\()/)],scope:{3:"title.function.invoke"},contains:[y]};y.contains.push(w);const N=[E,f,e.C_BLOCK_COMMENT_MODE,c,d,_];return{case_insensitive:!1,keywords:m,contains:[{begin:n.concat(/#\[\s*/,i),beginScope:"meta",end:/]/,endScope:"meta",keywords:{literal:g,keyword:["new","array"]},contains:[{begin:/\[/,end:/]/,keywords:{literal:g,keyword:["new","array"]},contains:["self",...N]},...N,{scope:"meta",match:i}]},e.HASH_COMMENT_MODE,e.COMMENT("//","$"),e.COMMENT("/\\*","\\*/",{contains:[{scope:"doctag",match:"@[A-Za-z]+"}]}),{match:/__halt_compiler\(\);/,keywords:"__halt_compiler",starts:{scope:"comment",end:e.MATCH_NOTHING_RE,contains:[{match:/\?>/,scope:"meta",endsParent:!0}]}},{scope:"meta",variants:[{begin:/<\?php/,relevance:10},{begin:/<\?=/},{begin:/<\?/,relevance:.1},{begin:/\?>/}]},{scope:"variable.language",match:/\$this\b/},r,w,f,{match:[/const/,/\s/,a],scope:{1:"keyword",3:"variable.constant"}},_,{scope:"function",relevance:0,beginKeywords:"fn function",end:/[;{]/,excludeEnd:!0,illegal:"[$%\\[]",contains:[{beginKeywords:"use"},e.UNDERSCORE_TITLE_MODE,{begin:"=>",endsParent:!0},{scope:"params",begin:"\\(",end:"\\)",excludeBegin:!0,excludeEnd:!0,keywords:m,contains:["self",r,f,e.C_BLOCK_COMMENT_MODE,c,d]}]},{scope:"class",variants:[{beginKeywords:"enum",illegal:/[($"]/},{beginKeywords:"class interface trait",illegal:/[:($"]/}],relevance:0,end:/\{/,excludeEnd:!0,contains:[{beginKeywords:"extends implements"},e.UNDERSCORE_TITLE_MODE]},{beginKeywords:"namespace",relevance:0,end:";",illegal:/[.']/,contains:[e.inherit(e.UNDERSCORE_TITLE_MODE,{scope:"title.class"})]},{beginKeywords:"use",relevance:0,end:";",contains:[{match:/\b(as|const|function)\b/,scope:"keyword"},e.UNDERSCORE_TITLE_MODE]},c,d]}},grmr_php_template:e=>({name:"PHP template",subLanguage:"xml",contains:[{begin:/<\?(php|=)?/,end:/\?>/,subLanguage:"php",contains:[{begin:"/\\*",end:"\\*/",skip:!0},{begin:'b"',end:'"',skip:!0},{begin:"b'",end:"'",skip:!0},e.inherit(e.APOS_STRING_MODE,{illegal:null,className:null,contains:null,skip:!0}),e.inherit(e.QUOTE_STRING_MODE,{illegal:null,className:null,contains:null,skip:!0})]}]}),grmr_plaintext:e=>({name:"Plain text",aliases:["text","txt"],disableAutodetect:!0}),grmr_python:e=>{const n=e.regex,t=/[\p{XID_Start}_]\p{XID_Continue}*/u,a=["and","as","assert","async","await","break","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","in","is","lambda","nonlocal|10","not","or","pass","raise","return","try","while","with","yield"],i={$pattern:/[A-Za-z]\w+|__\w+__/,keyword:a,built_in:["__import__","abs","all","any","ascii","bin","bool","breakpoint","bytearray","bytes","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","exec","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","print","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip"],literal:["__debug__","Ellipsis","False","None","NotImplemented","True"],type:["Any","Callable","Coroutine","Dict","List","Literal","Generic","Optional","Sequence","Set","Tuple","Type","Union"]},r={className:"meta",begin:/^(>>>|\.\.\.) /},s={className:"subst",begin:/\{/,end:/\}/,keywords:i,illegal:/#/},o={begin:/\{\{/,relevance:0},l={className:"string",contains:[e.BACKSLASH_ESCAPE],variants:[{begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?'''/,end:/'''/,contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{begin:/([uU]|[bB]|[rR]|[bB][rR]|[rR][bB])?"""/,end:/"""/,contains:[e.BACKSLASH_ESCAPE,r],relevance:10},{begin:/([fF][rR]|[rR][fF]|[fF])'''/,end:/'''/,contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"""/,end:/"""/,contains:[e.BACKSLASH_ESCAPE,r,o,s]},{begin:/([uU]|[rR])'/,end:/'/,relevance:10},{begin:/([uU]|[rR])"/,end:/"/,relevance:10},{begin:/([bB]|[bB][rR]|[rR][bB])'/,end:/'/},{begin:/([bB]|[bB][rR]|[rR][bB])"/,end:/"/},{begin:/([fF][rR]|[rR][fF]|[fF])'/,end:/'/,contains:[e.BACKSLASH_ESCAPE,o,s]},{begin:/([fF][rR]|[rR][fF]|[fF])"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,o,s]},e.APOS_STRING_MODE,e.QUOTE_STRING_MODE]},c="[0-9](_?[0-9])*",d=`(\\b(${c}))?\\.(${c})|\\b(${c})\\.`,g="\\b|"+a.join("|"),u={className:"number",relevance:0,variants:[{begin:`(\\b(${c})|(${d}))[eE][+-]?(${c})[jJ]?(?=${g})`},{begin:`(${d})[jJ]?`},{begin:`\\b([1-9](_?[0-9])*|0+(_?0)*)[lLjJ]?(?=${g})`},{begin:`\\b0[bB](_?[01])+[lL]?(?=${g})`},{begin:`\\b0[oO](_?[0-7])+[lL]?(?=${g})`},{begin:`\\b0[xX](_?[0-9a-fA-F])+[lL]?(?=${g})`},{begin:`\\b(${c})[jJ](?=${g})`}]},b={className:"comment",begin:n.lookahead(/# type:/),end:/$/,keywords:i,contains:[{begin:/# type:/},{begin:/#/,end:/\b\B/,endsWithParent:!0}]},m={className:"params",variants:[{className:"",begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:i,contains:["self",r,u,l,e.HASH_COMMENT_MODE]}]};return s.contains=[l,u,r],{name:"Python",aliases:["py","gyp","ipython"],unicodeRegex:!0,keywords:i,illegal:/(<\/|->|\?)|=>/,contains:[r,u,{begin:/\bself\b/},{beginKeywords:"if",relevance:0},l,b,e.HASH_COMMENT_MODE,{match:[/\bdef/,/\s+/,t],scope:{1:"keyword",3:"title.function"},contains:[m]},{variants:[{match:[/\bclass/,/\s+/,t,/\s*/,/\(\s*/,t,/\s*\)/]},{match:[/\bclass/,/\s+/,t]}],scope:{1:"keyword",3:"title.class",6:"title.class.inherited"}},{className:"meta",begin:/^[\t ]*@/,end:/(?=#)|$/,contains:[u,m,l]}]}},grmr_python_repl:e=>({aliases:["pycon"],contains:[{className:"meta.prompt",starts:{end:/ |$/,starts:{end:"$",subLanguage:"python"}},variants:[{begin:/^>>>(?=[ ]|$)/},{begin:/^\.\.\.(?=[ ]|$)/}]}]}),grmr_r:e=>{const n=e.regex,t=/(?:(?:[a-zA-Z]|\.[._a-zA-Z])[._a-zA-Z0-9]*)|\.(?!\d)/,a=n.either(/0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*[pP][+-]?\d+i?/,/0[xX][0-9a-fA-F]+(?:[pP][+-]?\d+)?[Li]?/,/(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?[Li]?/),i=/[=!<>:]=|\|\||&&|:::?|<-|<<-|->>|->|\|>|[-+*\/?!$&|:<=>@^~]|\*\*/,r=n.either(/[()]/,/[{}]/,/\[\[/,/[[\]]/,/\\/,/,/);return{name:"R",keywords:{$pattern:t,keyword:"function if in break next repeat else for while",literal:"NULL NA TRUE FALSE Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 NA_complex_|10",built_in:"LETTERS letters month.abb month.name pi T F abs acos acosh all any anyNA Arg as.call as.character as.complex as.double as.environment as.integer as.logical as.null.default as.numeric as.raw asin asinh atan atanh attr attributes baseenv browser c call ceiling class Conj cos cosh cospi cummax cummin cumprod cumsum digamma dim dimnames emptyenv exp expression floor forceAndCall gamma gc.time globalenv Im interactive invisible is.array is.atomic is.call is.character is.complex is.double is.environment is.expression is.finite is.function is.infinite is.integer is.language is.list is.logical is.matrix is.na is.name is.nan is.null is.numeric is.object is.pairlist is.raw is.recursive is.single is.symbol lazyLoadDBfetch length lgamma list log max min missing Mod names nargs nzchar oldClass on.exit pos.to.env proc.time prod quote range Re rep retracemem return round seq_along seq_len seq.int sign signif sin sinh sinpi sqrt standardGeneric substitute sum case tan tanh tanpi tracemem trigamma trunc unclass untracemem UseMethod xtfrm"},contains:[e.COMMENT(/#'/,/$/,{contains:[{scope:"doctag",match:/@examples/,starts:{end:n.lookahead(n.either(/\n^#'\s*(?=@[a-zA-Z]+)/,/\n^(?!#')/)),endsParent:!0}},{scope:"doctag",begin:"@param",end:/$/,contains:[{scope:"variable",variants:[{match:t},{match:/`(?:\\.|[^`\\])+`/}],endsParent:!0}]},{scope:"doctag",match:/@[a-zA-Z]+/},{scope:"keyword",match:/\\[a-zA-Z]+/}]}),e.HASH_COMMENT_MODE,{scope:"string",contains:[e.BACKSLASH_ESCAPE],variants:[e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\(/,end:/\)(-*)"/}),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\{/,end:/\}(-*)"/}),e.END_SAME_AS_BEGIN({begin:/[rR]"(-*)\[/,end:/\](-*)"/}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\(/,end:/\)(-*)'/}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\{/,end:/\}(-*)'/}),e.END_SAME_AS_BEGIN({begin:/[rR]'(-*)\[/,end:/\](-*)'/}),{begin:'"',end:'"',relevance:0},{begin:"'",end:"'",relevance:0}]},{relevance:0,variants:[{scope:{1:"operator",2:"number"},match:[i,a]},{scope:{1:"operator",2:"number"},match:[/%[^%]*%/,a]},{scope:{1:"punctuation",2:"number"},match:[r,a]},{scope:{2:"number"},match:[/[^a-zA-Z0-9._]|^/,a]}]},{scope:{3:"operator"},match:[t,/\s+/,/<-/,/\s+/]},{scope:"operator",relevance:0,variants:[{match:i},{match:/%[^%]*%/}]},{scope:"punctuation",relevance:0,match:r},{begin:"`",end:"`",contains:[{begin:/\\./}]}]}},grmr_ruby:e=>{const n=e.regex,t="([a-zA-Z_]\\w*[!?=]?|[-+~]@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?)",a=n.either(/\b([A-Z]+[a-z0-9]+)+/,/\b([A-Z]+[a-z0-9]+)+[A-Z]+/),i=n.concat(a,/(::\w+)*/),r={"variable.constant":["__FILE__","__LINE__"],"variable.language":["self","super"],keyword:["alias","and","attr_accessor","attr_reader","attr_writer","begin","BEGIN","break","case","class","defined","do","else","elsif","end","END","ensure","for","if","in","include","module","next","not","or","redo","require","rescue","retry","return","then","undef","unless","until","when","while","yield"],built_in:["proc","lambda"],literal:["true","false","nil"]},s={className:"doctag",begin:"@[A-Za-z]+"},o={begin:"#<",end:">"},l=[e.COMMENT("#","$",{contains:[s]}),e.COMMENT("^=begin","^=end",{contains:[s],relevance:10}),e.COMMENT("^__END__",e.MATCH_NOTHING_RE)],c={className:"subst",begin:/#\{/,end:/\}/,keywords:r},d={className:"string",contains:[e.BACKSLASH_ESCAPE,c],variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/`/,end:/`/},{begin:/%[qQwWx]?\(/,end:/\)/},{begin:/%[qQwWx]?\[/,end:/\]/},{begin:/%[qQwWx]?\{/,end:/\}/},{begin:/%[qQwWx]?/},{begin:/%[qQwWx]?\//,end:/\//},{begin:/%[qQwWx]?%/,end:/%/},{begin:/%[qQwWx]?-/,end:/-/},{begin:/%[qQwWx]?\|/,end:/\|/},{begin:/\B\?(\\\d{1,3})/},{begin:/\B\?(\\x[A-Fa-f0-9]{1,2})/},{begin:/\B\?(\\u\{?[A-Fa-f0-9]{1,6}\}?)/},{begin:/\B\?(\\M-\\C-|\\M-\\c|\\c\\M-|\\M-|\\C-\\M-)[\x20-\x7e]/},{begin:/\B\?\\(c|C-)[\x20-\x7e]/},{begin:/\B\?\\?\S/},{begin:n.concat(/<<[-~]?'?/,n.lookahead(/(\w+)(?=\W)[^\n]*\n(?:[^\n]*\n)*?\s*\1\b/)),contains:[e.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,contains:[e.BACKSLASH_ESCAPE,c]})]}]},g="[0-9](_?[0-9])*",u={className:"number",relevance:0,variants:[{begin:`\\b([1-9](_?[0-9])*|0)(\\.(${g}))?([eE][+-]?(${g})|r)?i?\\b`},{begin:"\\b0[dD][0-9](_?[0-9])*r?i?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*r?i?\\b"},{begin:"\\b0[oO][0-7](_?[0-7])*r?i?\\b"},{begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*r?i?\\b"},{begin:"\\b0(_?[0-7])+r?i?\\b"}]},b={variants:[{match:/\(\)/},{className:"params",begin:/\(/,end:/(?=\))/,excludeBegin:!0,endsParent:!0,keywords:r}]},m=[d,{variants:[{match:[/class\s+/,i,/\s+<\s+/,i]},{match:[/class\s+/,i]}],scope:{2:"title.class",4:"title.class.inherited"},keywords:r},{relevance:0,match:[i,/\.new[ (]/],scope:{1:"title.class"}},{relevance:0,match:/\b[A-Z][A-Z_0-9]+\b/,className:"variable.constant"},{match:[/def/,/\s+/,t],scope:{1:"keyword",3:"title.function"},contains:[b]},{begin:e.IDENT_RE+"::"},{className:"symbol",begin:e.UNDERSCORE_IDENT_RE+"(!|\\?)?:",relevance:0},{className:"symbol",begin:":(?!\\s)",contains:[d,{begin:t}],relevance:0},u,{className:"variable",begin:"(\\$\\W)|((\\$|@@?)(\\w+))(?=[^@$?])(?![A-Za-z])(?![@$?'])"},{className:"params",begin:/\|/,end:/\|/,excludeBegin:!0,excludeEnd:!0,relevance:0,keywords:r},{begin:"("+e.RE_STARTERS_RE+"|unless)\\s*",keywords:"unless",contains:[{className:"regexp",contains:[e.BACKSLASH_ESCAPE,c],illegal:/\n/,variants:[{begin:"/",end:"/[a-z]*"},{begin:/%r\{/,end:/\}[a-z]*/},{begin:"%r\\(",end:"\\)[a-z]*"},{begin:"%r!",end:"![a-z]*"},{begin:"%r\\[",end:"\\][a-z]*"}]}].concat(o,l),relevance:0}].concat(o,l);c.contains=m,b.contains=m;const p=[{begin:/^\s*=>/,starts:{end:"$",contains:m}},{className:"meta.prompt",begin:"^([>?]>|[\\w#]+\\(\\w+\\):\\d+:\\d+[>*]|(\\w+-)?\\d+\\.\\d+\\.\\d+(p\\d+)?[^\\d][^>]+>)(?=[ ])",starts:{end:"$",keywords:r,contains:m}}];return l.unshift(o),{name:"Ruby",aliases:["rb","gemspec","podspec","thor","irb"],keywords:r,illegal:/\/\*/,contains:[e.SHEBANG({binary:"ruby"})].concat(p).concat(l).concat(m)}},grmr_rust:e=>{const n=e.regex,t={className:"title.function.invoke",relevance:0,begin:n.concat(/\b/,/(?!let\b)/,e.IDENT_RE,n.lookahead(/\s*\(/))},a="([ui](8|16|32|64|128|size)|f(32|64))?",i=["drop ","Copy","Send","Sized","Sync","Drop","Fn","FnMut","FnOnce","ToOwned","Clone","Debug","PartialEq","PartialOrd","Eq","Ord","AsRef","AsMut","Into","From","Default","Iterator","Extend","IntoIterator","DoubleEndedIterator","ExactSizeIterator","SliceConcatExt","ToString","assert!","assert_eq!","bitflags!","bytes!","cfg!","col!","concat!","concat_idents!","debug_assert!","debug_assert_eq!","env!","panic!","file!","format!","format_args!","include_bin!","include_str!","line!","local_data_key!","module_path!","option_env!","print!","println!","select!","stringify!","try!","unimplemented!","unreachable!","vec!","write!","writeln!","macro_rules!","assert_ne!","debug_assert_ne!"];return{name:"Rust",aliases:["rs"],keywords:{$pattern:e.IDENT_RE+"!?",type:["i8","i16","i32","i64","i128","isize","u8","u16","u32","u64","u128","usize","f32","f64","str","char","bool","Box","Option","Result","String","Vec"],keyword:["abstract","as","async","await","become","box","break","const","continue","crate","do","dyn","else","enum","extern","false","final","fn","for","if","impl","in","let","loop","macro","match","mod","move","mut","override","priv","pub","ref","return","self","Self","static","struct","super","trait","true","try","type","typeof","unsafe","unsized","use","virtual","where","while","yield"],literal:["true","false","Some","None","Ok","Err"],built_in:i},illegal:""},t]}},grmr_scss:e=>{const n=te(e),t=se,a=re,i="@[a-z-]+",r={className:"variable",begin:"(\\$[a-zA-Z-][a-zA-Z0-9_-]*)\\b",relevance:0};return{name:"SCSS",case_insensitive:!0,illegal:"[=/|']",contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,n.CSS_NUMBER_MODE,{className:"selector-id",begin:"#[A-Za-z0-9_-]+",relevance:0},{className:"selector-class",begin:"\\.[A-Za-z0-9_-]+",relevance:0},n.ATTRIBUTE_SELECTOR_MODE,{className:"selector-tag",begin:"\\b("+ae.join("|")+")\\b",relevance:0},{className:"selector-pseudo",begin:":("+a.join("|")+")"},{className:"selector-pseudo",begin:":(:)?("+t.join("|")+")"},r,{begin:/\(/,end:/\)/,contains:[n.CSS_NUMBER_MODE]},n.CSS_VARIABLE,{className:"attribute",begin:"\\b("+oe.join("|")+")\\b"},{begin:"\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b"},{begin:/:/,end:/[;}{]/,contains:[n.BLOCK_COMMENT,r,n.HEXCOLOR,n.CSS_NUMBER_MODE,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.IMPORTANT]},{begin:"@(page|font-face)",keywords:{$pattern:i,keyword:"@page @font-face"}},{begin:"@",end:"[{;]",returnBegin:!0,keywords:{$pattern:/[a-z-]+/,keyword:"and or not only",attribute:ie.join(" ")},contains:[{begin:i,className:"keyword"},{begin:/[a-z-]+(?=:)/,className:"attribute"},r,e.QUOTE_STRING_MODE,e.APOS_STRING_MODE,n.HEXCOLOR,n.CSS_NUMBER_MODE]},n.FUNCTION_DISPATCH]}},grmr_shell:e=>({name:"Shell Session",aliases:["console","shellsession"],contains:[{className:"meta.prompt",begin:/^\s{0,3}[/~\w\d[\]()@-]*[>%$#][ ]?/,starts:{end:/[^\\](?=\s*$)/,subLanguage:"bash"}}]}),grmr_sql:e=>{const n=e.regex,t=e.COMMENT("--","$"),a=["true","false","unknown"],i=["bigint","binary","blob","boolean","char","character","clob","date","dec","decfloat","decimal","float","int","integer","interval","nchar","nclob","national","numeric","real","row","smallint","time","timestamp","varchar","varying","varbinary"],r=["abs","acos","array_agg","asin","atan","avg","cast","ceil","ceiling","coalesce","corr","cos","cosh","count","covar_pop","covar_samp","cume_dist","dense_rank","deref","element","exp","extract","first_value","floor","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","last_value","lead","listagg","ln","log","log10","lower","max","min","mod","nth_value","ntile","nullif","percent_rank","percentile_cont","percentile_disc","position","position_regex","power","rank","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","row_number","sin","sinh","sqrt","stddev_pop","stddev_samp","substring","substring_regex","sum","tan","tanh","translate","translate_regex","treat","trim","trim_array","unnest","upper","value_of","var_pop","var_samp","width_bucket"],s=["create table","insert into","primary key","foreign key","not null","alter table","add constraint","grouping sets","on overflow","character set","respect nulls","ignore nulls","nulls first","nulls last","depth first","breadth first"],o=r,l=["abs","acos","all","allocate","alter","and","any","are","array","array_agg","array_max_cardinality","as","asensitive","asin","asymmetric","at","atan","atomic","authorization","avg","begin","begin_frame","begin_partition","between","bigint","binary","blob","boolean","both","by","call","called","cardinality","cascaded","case","cast","ceil","ceiling","char","char_length","character","character_length","check","classifier","clob","close","coalesce","collate","collect","column","commit","condition","connect","constraint","contains","convert","copy","corr","corresponding","cos","cosh","count","covar_pop","covar_samp","create","cross","cube","cume_dist","current","current_catalog","current_date","current_default_transform_group","current_path","current_role","current_row","current_schema","current_time","current_timestamp","current_path","current_role","current_transform_group_for_type","current_user","cursor","cycle","date","day","deallocate","dec","decimal","decfloat","declare","default","define","delete","dense_rank","deref","describe","deterministic","disconnect","distinct","double","drop","dynamic","each","element","else","empty","end","end_frame","end_partition","end-exec","equals","escape","every","except","exec","execute","exists","exp","external","extract","false","fetch","filter","first_value","float","floor","for","foreign","frame_row","free","from","full","function","fusion","get","global","grant","group","grouping","groups","having","hold","hour","identity","in","indicator","initial","inner","inout","insensitive","insert","int","integer","intersect","intersection","interval","into","is","join","json_array","json_arrayagg","json_exists","json_object","json_objectagg","json_query","json_table","json_table_primitive","json_value","lag","language","large","last_value","lateral","lead","leading","left","like","like_regex","listagg","ln","local","localtime","localtimestamp","log","log10","lower","match","match_number","match_recognize","matches","max","member","merge","method","min","minute","mod","modifies","module","month","multiset","national","natural","nchar","nclob","new","no","none","normalize","not","nth_value","ntile","null","nullif","numeric","octet_length","occurrences_regex","of","offset","old","omit","on","one","only","open","or","order","out","outer","over","overlaps","overlay","parameter","partition","pattern","per","percent","percent_rank","percentile_cont","percentile_disc","period","portion","position","position_regex","power","precedes","precision","prepare","primary","procedure","ptf","range","rank","reads","real","recursive","ref","references","referencing","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","release","result","return","returns","revoke","right","rollback","rollup","row","row_number","rows","running","savepoint","scope","scroll","search","second","seek","select","sensitive","session_user","set","show","similar","sin","sinh","skip","smallint","some","specific","specifictype","sql","sqlexception","sqlstate","sqlwarning","sqrt","start","static","stddev_pop","stddev_samp","submultiset","subset","substring","substring_regex","succeeds","sum","symmetric","system","system_time","system_user","table","tablesample","tan","tanh","then","time","timestamp","timezone_hour","timezone_minute","to","trailing","translate","translate_regex","translation","treat","trigger","trim","trim_array","true","truncate","uescape","union","unique","unknown","unnest","update","upper","user","using","value","values","value_of","var_pop","var_samp","varbinary","varchar","varying","versioning","when","whenever","where","width_bucket","window","with","within","without","year","add","asc","collation","desc","final","first","last","view"].filter(e=>!r.includes(e)),c={begin:n.concat(/\b/,n.either(...o),/\s*\(/),relevance:0,keywords:{built_in:o}};return{name:"SQL",case_insensitive:!0,illegal:/[{}]|<\//,keywords:{$pattern:/\b[\w\.]+/,keyword:((e,{exceptions:n,when:t}={})=>{const a=t;return n=n||[],e.map(e=>e.match(/\|\d+$/)||n.includes(e)?e:a(e)?e+"|0":e)})(l,{when:e=>e.length<3}),literal:a,type:i,built_in:["current_catalog","current_date","current_default_transform_group","current_path","current_role","current_schema","current_transform_group_for_type","current_user","session_user","system_time","system_user","current_time","localtime","current_timestamp","localtimestamp"]},contains:[{begin:n.either(...s),relevance:0,keywords:{$pattern:/[\w\.]+/,keyword:l.concat(s),literal:a,type:i}},{className:"type",begin:n.either("double precision","large object","with timezone","without timezone")},c,{className:"variable",begin:/@[a-z0-9]+/},{className:"string",variants:[{begin:/'/,end:/'/,contains:[{begin:/''/}]}]},{begin:/"/,end:/"/,contains:[{begin:/""/}]},e.C_NUMBER_MODE,e.C_BLOCK_COMMENT_MODE,t,{className:"operator",begin:/[-+*/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?/,relevance:0}]}},grmr_swift:e=>{const n={match:/\s+/,relevance:0},t=e.COMMENT("/\\*","\\*/",{contains:["self"]}),a=[e.C_LINE_COMMENT_MODE,t],i={match:[/\./,p(...ve,...ke)],className:{2:"keyword"}},r={match:m(/\./,p(...xe)),relevance:0},s=xe.filter(e=>"string"==typeof e).concat(["_|0"]),o={variants:[{className:"keyword",match:p(...xe.filter(e=>"string"!=typeof e).concat(Oe).map(Ne),...ke)}]},l={$pattern:p(/\b\w+/,/#\w+/),keyword:s.concat(Ae),literal:Me},c=[i,r,o],d=[{match:m(/\./,p(...Ce)),relevance:0},{className:"built_in",match:m(/\b/,p(...Ce),/(?=\()/)}],u={match:/->/,relevance:0},b=[u,{className:"operator",relevance:0,variants:[{match:De},{match:`\\.(\\.|${Re})+`}]}],_="([0-9a-fA-F]_*)+",h={className:"number",relevance:0,variants:[{match:"\\b(([0-9]_*)+)(\\.(([0-9]_*)+))?([eE][+-]?(([0-9]_*)+))?\\b"},{match:`\\b0x(${_})(\\.(${_}))?([pP][+-]?(([0-9]_*)+))?\\b`},{match:/\b0o([0-7]_*)+\b/},{match:/\b0b([01]_*)+\b/}]},f=(e="")=>({className:"subst",variants:[{match:m(/\\/,e,/[0\\tnr"']/)},{match:m(/\\/,e,/u\{[0-9a-fA-F]{1,8}\}/)}]}),E=(e="")=>({className:"subst",match:m(/\\/,e,/[\t ]*(?:[\r\n]|\r\n)/)}),y=(e="")=>({className:"subst",label:"interpol",begin:m(/\\/,e,/\(/),end:/\)/}),w=(e="")=>({begin:m(e,/"""/),end:m(/"""/,e),contains:[f(e),E(e),y(e)]}),N=(e="")=>({begin:m(e,/"/),end:m(/"/,e),contains:[f(e),y(e)]}),v={className:"string",variants:[w(),w("#"),w("##"),w("###"),N(),N("#"),N("##"),N("###")]},k={match:m(/`/,Be,/`/)},O=[k,{className:"variable",match:/\$\d+/},{className:"variable",match:`\\$${Le}+`}],x=[{match:/(@|#(un)?)available/,className:"keyword",starts:{contains:[{begin:/\(/,end:/\)/,keywords:Fe,contains:[...b,h,v]}]}},{className:"keyword",match:m(/@/,p(...ze))},{className:"meta",match:m(/@/,Be)}],M={match:g(/\b[A-Z]/),relevance:0,contains:[{className:"type",match:m(/(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)/,Le,"+")},{className:"type",match:$e,relevance:0},{match:/[?!]+/,relevance:0},{match:/\.\.\./,relevance:0},{match:m(/\s+&\s+/,g($e)),relevance:0}]},S={begin://,keywords:l,contains:[...a,...c,...x,u,M]};M.contains.push(S);const A={begin:/\(/,end:/\)/,relevance:0,keywords:l,contains:["self",{match:m(Be,/\s*:/),keywords:"_|0",relevance:0},...a,...c,...d,...b,h,v,...O,...x,M]},C={begin://,contains:[...a,M]},T={begin:/\(/,end:/\)/,keywords:l,contains:[{begin:p(g(m(Be,/\s*:/)),g(m(Be,/\s+/,Be,/\s*:/))),end:/:/,relevance:0,contains:[{className:"keyword",match:/\b_\b/},{className:"params",match:Be}]},...a,...c,...b,h,v,...x,M,A],endsParent:!0,illegal:/["']/},R={match:[/func/,/\s+/,p(k.match,Be,De)],className:{1:"keyword",3:"title.function"},contains:[C,T,n],illegal:[/\[/,/%/]},D={match:[/\b(?:subscript|init[?!]?)/,/\s*(?=[<(])/],className:{1:"keyword"},contains:[C,T,n],illegal:/\[|%/},I={match:[/operator/,/\s+/,De],className:{1:"keyword",3:"title"}},L={begin:[/precedencegroup/,/\s+/,$e],className:{1:"keyword",3:"title"},contains:[M],keywords:[...Se,...Me],end:/}/};for(const e of v.variants){const n=e.contains.find(e=>"interpol"===e.label);n.keywords=l;const t=[...c,...d,...b,h,v,...O];n.contains=[...t,{begin:/\(/,end:/\)/,contains:["self",...t]}]}return{name:"Swift",keywords:l,contains:[...a,R,D,{beginKeywords:"struct protocol class extension enum actor",end:"\\{",excludeEnd:!0,keywords:l,contains:[e.inherit(e.TITLE_MODE,{className:"title.class",begin:/[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/}),...c]},I,L,{beginKeywords:"import",end:/$/,contains:[...a],relevance:0},...c,...d,...b,h,v,...O,...x,M,A]}},grmr_typescript:e=>{const n=we(e),t=["any","void","number","boolean","string","object","never","symbol","bigint","unknown"],a={beginKeywords:"namespace",end:/\{/,excludeEnd:!0,contains:[n.exports.CLASS_REFERENCE]},i={beginKeywords:"interface",end:/\{/,excludeEnd:!0,keywords:{keyword:"interface extends",built_in:t},contains:[n.exports.CLASS_REFERENCE]},r={$pattern:be,keyword:me.concat(["type","namespace","interface","public","private","protected","implements","declare","abstract","readonly","enum","override"]),literal:pe,built_in:ye.concat(t),"variable.language":Ee},s={className:"meta",begin:"@[A-Za-z$_][0-9A-Za-z$_]*"},o=(e,n,t)=>{const a=e.contains.findIndex(e=>e.label===n);if(-1===a)throw Error("can not find mode to replace");e.contains.splice(a,1,t)};return Object.assign(n.keywords,r),n.exports.PARAMS_CONTAINS.push(s),n.contains=n.contains.concat([s,a,i]),o(n,"shebang",e.SHEBANG()),o(n,"use_strict",{className:"meta",relevance:10,begin:/^\s*['"]use strict['"]/}),n.contains.find(e=>"func.def"===e.label).relevance=0,Object.assign(n,{name:"TypeScript",aliases:["ts","tsx"]}),n},grmr_vbnet:e=>{const n=e.regex,t=/\d{1,2}\/\d{1,2}\/\d{4}/,a=/\d{4}-\d{1,2}-\d{1,2}/,i=/(\d|1[012])(:\d+){0,2} *(AM|PM)/,r=/\d{1,2}(:\d{1,2}){1,2}/,s={className:"literal",variants:[{begin:n.concat(/# */,n.either(a,t),/ *#/)},{begin:n.concat(/# */,r,/ *#/)},{begin:n.concat(/# */,i,/ *#/)},{begin:n.concat(/# */,n.either(a,t),/ +/,n.either(i,r),/ *#/)}]},o=e.COMMENT(/'''/,/$/,{contains:[{className:"doctag",begin:/<\/?/,end:/>/}]}),l=e.COMMENT(null,/$/,{variants:[{begin:/'/},{begin:/([\t ]|^)REM(?=\s)/}]});return{name:"Visual Basic .NET",aliases:["vb"],case_insensitive:!0,classNameAliases:{label:"symbol"},keywords:{keyword:"addhandler alias aggregate ansi as async assembly auto binary by byref byval call case catch class compare const continue custom declare default delegate dim distinct do each equals else elseif end enum erase error event exit explicit finally for friend from function get global goto group handles if implements imports in inherits interface into iterator join key let lib loop me mid module mustinherit mustoverride mybase myclass namespace narrowing new next notinheritable notoverridable of off on operator option optional order overloads overridable overrides paramarray partial preserve private property protected public raiseevent readonly redim removehandler resume return select set shadows shared skip static step stop structure strict sub synclock take text then throw to try unicode until using when where while widening with withevents writeonly yield",built_in:"addressof and andalso await directcast gettype getxmlnamespace is isfalse isnot istrue like mod nameof new not or orelse trycast typeof xor cbool cbyte cchar cdate cdbl cdec cint clng cobj csbyte cshort csng cstr cuint culng cushort",type:"boolean byte char date decimal double integer long object sbyte short single string uinteger ulong ushort",literal:"true false nothing"},illegal:"//|\\{|\\}|endif|gosub|variant|wend|^\\$ ",contains:[{className:"string",begin:/"(""|[^/n])"C\b/},{className:"string",begin:/"/,end:/"/,illegal:/\n/,contains:[{begin:/""/}]},s,{className:"number",relevance:0,variants:[{begin:/\b\d[\d_]*((\.[\d_]+(E[+-]?[\d_]+)?)|(E[+-]?[\d_]+))[RFD@!#]?/},{begin:/\b\d[\d_]*((U?[SIL])|[%&])?/},{begin:/&H[\dA-F_]+((U?[SIL])|[%&])?/},{begin:/&O[0-7_]+((U?[SIL])|[%&])?/},{begin:/&B[01_]+((U?[SIL])|[%&])?/}]},{className:"label",begin:/^\w+:/},o,l,{className:"meta",begin:/[\t ]*#(const|disable|else|elseif|enable|end|externalsource|if|region)\b/,end:/$/,keywords:{keyword:"const disable else elseif enable end externalsource if region then"},contains:[l]}]}},grmr_yaml:e=>{const n="true false yes no null",t="[\\w#;/?:@&=+$,.~*'()[\\]]+",a={className:"string",relevance:0,variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/\S+/}],contains:[e.BACKSLASH_ESCAPE,{className:"template-variable",variants:[{begin:/\{\{/,end:/\}\}/},{begin:/%\{/,end:/\}/}]}]},i=e.inherit(a,{variants:[{begin:/'/,end:/'/},{begin:/"/,end:/"/},{begin:/[^\s,{}[\]]+/}]}),r={end:",",endsWithParent:!0,excludeEnd:!0,keywords:n,relevance:0},s={begin:/\{/,end:/\}/,contains:[r],illegal:"\\n",relevance:0},o={begin:"\\[",end:"\\]",contains:[r],illegal:"\\n",relevance:0},l=[{className:"attr",variants:[{begin:"\\w[\\w :\\/.-]*:(?=[ \t]|$)"},{begin:'"\\w[\\w :\\/.-]*":(?=[ \t]|$)'},{begin:"'\\w[\\w :\\/.-]*':(?=[ \t]|$)"}]},{className:"meta",begin:"^---\\s*$",relevance:10},{className:"string",begin:"[\\|>]([1-9]?[+-])?[ ]*\\n( +)[^ ][^\\n]*\\n(\\2[^\\n]+\\n?)*"},{begin:"<%[%=-]?",end:"[%-]?%>",subLanguage:"ruby",excludeBegin:!0,excludeEnd:!0,relevance:0},{className:"type",begin:"!\\w+!"+t},{className:"type",begin:"!<"+t+">"},{className:"type",begin:"!"+t},{className:"type",begin:"!!"+t},{className:"meta",begin:"&"+e.UNDERSCORE_IDENT_RE+"$"},{className:"meta",begin:"\\*"+e.UNDERSCORE_IDENT_RE+"$"},{className:"bullet",begin:"-(?=[ ]|$)",relevance:0},e.HASH_COMMENT_MODE,{beginKeywords:n,keywords:{literal:n}},{className:"number",begin:"\\b[0-9]{4}(-[0-9][0-9]){0,2}([Tt \\t][0-9][0-9]?(:[0-9][0-9]){2})?(\\.[0-9]*)?([ \\t])*(Z|[-+][0-9][0-9]?(:[0-9][0-9])?)?\\b"},{className:"number",begin:e.C_NUMBER_RE+"\\b",relevance:0},s,o,a],c=[...l];return c.pop(),c.push(i),r.contains=c,{name:"YAML",case_insensitive:!0,aliases:["yml"],contains:l}}});const je=ne;for(const e of Object.keys(Ue)){const n=e.replace("grmr_","").replace("_","-");je.registerLanguage(n,Ue[e])}return je}();"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs); ================================================ FILE: web/playground/.eslintrc.cjs ================================================ module.exports = { root: true, env: { browser: true, es2020: true }, extends: [ "eslint:recommended", "plugin:react/recommended", "plugin:react/jsx-runtime", "plugin:react-hooks/recommended", ], ignorePatterns: ["dist", ".eslintrc.cjs"], parserOptions: { ecmaVersion: "latest", sourceType: "module" }, settings: { react: { version: "18.2" } }, plugins: ["react-refresh"], rules: { "react-refresh/only-export-components": [ "warn", { allowConstantExport: true }, ], }, }; ================================================ FILE: web/playground/.gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # data /public/data # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* book.json ================================================ FILE: web/playground/README.md ================================================ # PRQL Playground A fast-feedback compiler from PRQL to SQL, hosted at To run locally, [set up a development environment](https://prql-lang.org/book/project/contributing/development.html), and then run: ```sh task web:run-playground ``` ...or use the commands which that command calls from [our Taskfile](../Taskfile.yaml). ================================================ FILE: web/playground/generateBook.cjs ================================================ const { readdir, stat, readFile, writeFile } = require("fs/promises"); const { join, relative, sep, normalize, basename } = require("path"); const { EOL } = require("os"); /** * Get all markdown files in given dir * @param {string} dirPath */ async function* getAllFiles(dirPath) { const files = await readdir(dirPath); files.sort((a, b) => +isFile(a) - +isFile(b)); for (const file of files) { const fullPath = join(dirPath, file); if ((await stat(fullPath)).isDirectory()) { yield fullPath; yield* getAllFiles(fullPath); } else { if (fullPath.endsWith(".md")) { yield fullPath; } } } } function depth(path) { return path.split(sep).length; } function isFile(path) { return path.endsWith(".md"); } /** * Get all prql code snippets from a markdown file * @param {string} content * @param {string} file * @returns {{title:string, prql:string}[]} */ function getSnippets(content, file) { const name = file.trim().toLowerCase().replace(/\s/g, "_"); let heading = ""; let prql = null; const arr = []; let index = 1; const titles = new Set(); content.split(/\r\n|\n/).forEach((line) => { if (prql == null && line.startsWith("#") && line.includes("# ")) { const spaceIndex = line.indexOf("# "); heading = line .slice(spaceIndex + 2) .trim() .toLowerCase() .replace(/\s/g, "_"); return; } if (line.trim() === "```prql") { prql = ""; return; } if (prql != null && line.trim() === "```") { let title = heading || name; if (titles.has(title)) { title += `_${++index}`; } else { index = 1; } arr.push({ title: title + ".prql", prql: prql.trim(), }); titles.add(title); prql = null; return; } if (prql != null) { prql = prql + line + EOL; } }); if (arr.length !== titles.size) { throw new Error("duplicate titles"); } return arr; } (async () => { const fileObject = {}; const dir = join(__dirname, "..", "book", "src"); const files = []; let minDepth = 1e10; for await (const file of getAllFiles(dir)) { files.push(file); minDepth = Math.min(depth(file)); } for (const filePath of files) { const relativeFile = relative(dir, filePath); const snippets = isFile(filePath) ? getSnippets( (await readFile(filePath)).toString(), basename(filePath).replace(/\..+/g, "").trim(), ) : []; if (!snippets.length && isFile(filePath)) { continue; } const dept = depth(filePath) - minDepth; fileObject[relativeFile] = [ "", // editor "", // content dept, // depth normalize(join(relativeFile, "..")), // parent relativeFile, // id basename(relativeFile).replace(/\..+/g, "").trim(), // name ]; for (const snippet of snippets) { const id = relativeFile + "_" + snippet.title; fileObject[id] = [ "sql", snippet.prql, dept + 1, relativeFile, id, snippet.title, ]; } } // remove empty folders const keys = Object.keys(fileObject); const parents = new Set(); Object.values(fileObject).forEach((v) => parents.add(v[3])); for (const key of keys) { if (!parents.has(key) && !fileObject[key][0]) { delete fileObject[key]; } } await writeFile(join("src", "book.json"), JSON.stringify(fileObject)); })().catch((e) => { console.error(e); process.exit(1); }); ================================================ FILE: web/playground/index.html ================================================ PRQL Playground
================================================ FILE: web/playground/package.json ================================================ { "browserslist": { "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ], "production": [ ">0.2%", "not dead", "not op_mini all" ] }, "dependencies": { "@duckdb/duckdb-wasm": "^1.32.0", "@monaco-editor/react": "^4.7.0", "@testing-library/jest-dom": "^6.9.0", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.0", "monaco-editor": "^0.55.0", "prqlc": "file:../../prqlc/bindings/js", "react": "^19.2.0", "react-dom": "^19.2.0", "react-syntax-highlighter": "^16.1.0", "web-vitals": "^5.1.0", "yaml": "^2.8.0" }, "devDependencies": { "@vitejs/plugin-react": "^6.0.1", "vite": "^8.0.1", "vite-plugin-wasm": "^3.6.0" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "homepage": "https://prql-lang.org/playground/playground/", "name": "prql-playground", "type": "module", "private": true, "scripts": { "dev": "vite --host 0.0.0.0", "build": "vite build", "prepare": "rsync -ai --checksum --delete ../../prqlc/prqlc/tests/integration/data/ public/data/ && node generateBook.cjs", "preview": "vite preview" }, "version": "0.13.12" } ================================================ FILE: web/playground/public/manifest.json ================================================ { "background_color": "#ffffff", "display": "standalone", "icons": [ { "sizes": "64x64 32x32 24x24 16x16", "src": "favicon.ico", "type": "image/x-icon" }, { "sizes": "192x192", "src": "icon192.png", "type": "image/png" } ], "name": "PRQL Playground", "short_name": "PRQL Playground", "start_url": ".", "theme_color": "#222222" } ================================================ FILE: web/playground/public/robots.txt ================================================ # https://www.robotstxt.org/robotstxt.html User-agent: * Disallow: ================================================ FILE: web/playground/src/app/App.css ================================================ .main { height: 100%; display: flex; } .main > * { flex-grow: 1; } .sidebar { flex: 0 0 200px; } ================================================ FILE: web/playground/src/app/App.jsx ================================================ import "./App.css"; import Workbench from "../workbench/Workbench"; import Sidebar from "../sidebar/Sidebar"; import examples from "../examples"; import book from "../book.json"; import * as duckdb from "../workbench/duckdb"; import React from "react"; function loadLocalStorage() { return JSON.parse(localStorage.getItem("files")) || {}; } function saveLocalStorage(files) { return localStorage.setItem("files", JSON.stringify(files)); } const chinook = duckdb.CHINOOK_TABLES.reduce((lib, table) => { return Object.assign(lib, { [table + ".prql"]: ["arrow", `from ${table}\ntake 10`], }); }, {}); class App extends React.Component { workbenchActions = null; state = { library: { examples, tables: chinook, book, "local storage": loadLocalStorage(), }, }; setWorkbenchActions = (callables) => { this.workbenchActions = callables; }; componentDidMount() { let defaultFile = "introduction.prql"; this.workbenchActions.loadFile(defaultFile, examples[defaultFile]); } saveFile(filename, content) { const localStorage = { ...this.state.library["local storage"], [filename]: content, }; this.setState({ library: { ...this.state.library, "local storage": localStorage }, }); saveLocalStorage(localStorage); } render() { return (
this.workbenchActions.loadFile(f, c)} /> this.saveFile(f, c)} />
); } } export default App; ================================================ FILE: web/playground/src/examples.js ================================================ const examples = { "introduction.prql": [ "sql", `from invoices # A PRQL query begins with a table # Subsequent lines "transform" (modify) it derive { # "derive" adds columns to the result transaction_fee = 0.8, # "=" sets a column name income = total - transaction_fee # Calculations can use other column names } # starts a comment; commenting out a line leaves a valid query filter income > 5 # "filter" replaces both of SQL's WHERE & HAVING filter invoice_date >= @2010-01-16 # Clear date syntax group customer_id ( # "group" performs the pipeline in (...) on each group aggregate { # "aggregate" reduces each group to a single row sum_income = sum income, # ... using SQL SUM(), COUNT(), etc. functions ct = count customer_id, # } ) join c=customers (==customer_id) # join on "customer_id" from both tables derive name = f"{c.last_name}, {c.first_name}" # F-strings like Python derive db_version = s"version()" # S-string offers escape hatch to SQL select { # "select" passes along only the named columns c.customer_id, name, sum_income, ct, db_version, } # trailing commas always ignored sort {-sum_income} # "sort" sorts the result; "-" is decreasing order take 1..10 # Limit to a range - could also be "take 10" # # The "output.sql" tab at right shows the SQL generated from this PRQL query # The "output.arrow" tab shows the result of the query `, ], "let-table-0.prql": [ "sql", `let soundtracks = ( from playlists filter name == 'TV Shows' join pt=playlist_track (==playlist_id) select pt.track_id ) let high_energy = ( from genres filter name == 'Rock And Roll' || name == 'Hip Hop/Rap' ) from t=tracks # anti-join soundtracks join side:left s=soundtracks (==track_id) filter s.track_id == null # limit to kicker genres join g=high_energy (==genre_id) # format output select {t.track_id, track = t.name, genre = g.name} take 10 `, ], "artists-0.prql": [ "sql", `from tracks select {album_id, name, unit_price} sort {-unit_price, name} group album_id ( aggregate { track_count = count name, album_price = sum unit_price } ) join albums (==album_id) group artist_id ( aggregate { track_count = sum track_count, artist_price = sum album_price } ) join artists (==artist_id) select {artists.name, artist_price, track_count} sort {-artist_price} derive avg_track_price = artist_price / track_count `, ], }; export default examples; ================================================ FILE: web/playground/src/highlight.css ================================================ /* Highlight.js obsidian theme */ pre code.hljs { display: block; overflow-x: auto; } code.hljs { padding: 3px 5px; } .hljs { color: #e0e2e4; } .hljs-keyword, .hljs-literal, .hljs-selector-id, .hljs-selector-tag { color: #e4a24b; } .hljs-number { color: #ffcd22; } .hljs-params { color: #5b8fc5; } .hljs-link, .hljs-regexp { color: #d39745; } .hljs-meta { color: #557182; } .hljs-addition, .hljs-built_in, .hljs-bullet, .hljs-emphasis, .hljs-module, .hljs-name, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-subst, .hljs-tag, .hljs-template-tag, .hljs-template-variable, .hljs-type, .hljs-variable { color: #8cbbad; } .hljs-string, .hljs-symbol { color: #ec7600; } .hljs-comment, .hljs-deletion, .hljs-quote { color: #818e96; } .hljs-selector-class { color: #a082bd; } .hljs-doctag, .hljs-keyword, .hljs-literal, .hljs-name, .hljs-section, .hljs-selector-tag, .hljs-strong, .hljs-title, .hljs-type { font-weight: 700; } .hljs-class .hljs-title, .hljs-code, .hljs-section, .hljs-title.class_ { color: #fff; } ================================================ FILE: web/playground/src/index.css ================================================ body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; background-color: #2f2f2f; color: #fff; } #root { height: 100vh !important; } /* Inter ExtraBold */ @font-face { font-family: "InterExtraBold"; font-weight: bold; font-style: normal; font-display: swap; src: local(""), url("https://prql-lang.org/fonts/inter-extra-bold.woff2") format("woff2"), url("https://prql-lang.org/fonts/inter-extra-bold.woff") format("woff"); } h1 { font-family: "InterExtraBold", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } ================================================ FILE: web/playground/src/main.jsx ================================================ import React from "react"; import ReactDOM from "react-dom/client"; import "./index.css"; import "./highlight.css"; import App from "./app/App"; import reportWebVitals from "./reportWebVitals"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( , ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); ================================================ FILE: web/playground/src/output/Output.css ================================================ .tab-content.arrow { padding: 1.5rem; overflow-x: scroll; } .arrow table { border-collapse: collapse; } .arrow table td, .arrow table th { min-width: 2rem; padding: 2px 0.5rem; text-align: right; } .arrow table thead { border-bottom: 1px solid #444; } .arrow table tbody tr:not(:first-child) td { border-top: 1px solid #444; } /* .arrow table tbody tr:nth-child(2n) td { background-color: #333; } */ ================================================ FILE: web/playground/src/output/Output.jsx ================================================ import "./Output.css"; import React from "react"; import { Light as SyntaxHighlighter } from "react-syntax-highlighter"; import sql from "react-syntax-highlighter/dist/esm/languages/hljs/sql"; import yaml from "react-syntax-highlighter/dist/esm/languages/hljs/yaml"; SyntaxHighlighter.registerLanguage("sql", sql); SyntaxHighlighter.registerLanguage("yaml", yaml); function Tab(props) { return ( ); } class Output extends React.Component { state = { justCopied: false, }; render() { return (
{this.renderContent()}
); } renderContent() { if (!this.props.content) { return
; } if (this.props.tab === "sql") { return ( {this.props.content.sql} ); } if (this.props.tab === "arrow" && this.props.content.arrow) { const arrow = this.props.content.arrow; const header = arrow.schema.fields.map((f, index) => { return {f.name}; }); const converters = arrow.schema.fields.map((f) => { const typo = f.type.toString(); if (typo.startsWith("Timestamp")) { // TODO: handle timezone (which Date does not support) // HACK: due to bug in arrow or duckdb, we are getting MICROSECOND here, // but the values are actually milliseconds. I'm not sure what is going on, // so let's just assume the values will always be in milliseconds. /* if (typo.endsWith("")) { return (x) => new Date(x * 1000).toISOString(); } if (typo.endsWith("")) { return (x) => new Date(x).toISOString(); } if (typo.endsWith("")) { return (x) => new Date(x / 1000).toISOString(); } if (typo.endsWith("")) { return (x) => new Date(x / 1000000).toISOString(); } */ return (x) => new Date(x).toISOString(); } return (x) => x; }); const data = arrow.toArray().map((x) => [...x]); const rows = data.map((x, index) => { const cells = x.map(([_name, value], index) => ( {"" + converters[index](value)} )); return {cells}; }); // console.log(arrow, arrow.schema.fields, arrow.toArray()); return (
{header}{rows}
); } if (this.props.tab === "pl" && this.props.content.pl) { return ( {this.props.content.pl} ); } if (this.props.tab === "rq" && this.props.content.rq) { return ( {this.props.content.rq} ); } return
; } async copyOutput() { try { await navigator.clipboard.writeText(this.props.content[this.props.tab]); this.setState({ justCopied: true }); await new Promise((r) => window.setTimeout(r, 2000)); this.setState({ justCopied: false }); } catch (e) { console.error(e); } } } export default Output; ================================================ FILE: web/playground/src/reportWebVitals.js ================================================ const reportWebVitals = (onPerfEntry) => { if (onPerfEntry && onPerfEntry instanceof Function) { import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(onPerfEntry); getFID(onPerfEntry); getFCP(onPerfEntry); getLCP(onPerfEntry); getTTFB(onPerfEntry); }); } }; export default reportWebVitals; ================================================ FILE: web/playground/src/setupTests.js ================================================ // jest-dom adds custom jest matchers for asserting on DOM nodes. // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import "@testing-library/jest-dom"; ================================================ FILE: web/playground/src/sidebar/Sidebar.css ================================================ .sidebar { padding-top: 1rem; min-width: fit-content; overflow-y: auto; } section:not(:last-child) { border-bottom: 1px solid #666; padding-bottom: 1rem; } section h1 { font-size: 16px; margin: 0; } section h2 { text-transform: uppercase; font-weight: bold; font-size: 14px; margin: 0; } .fileRow::before { content: "\00a0"; /* non-breaking space to indent fileRows */ margin-right: 5px; } .fileRow:hover { cursor: pointer; background-color: #ffffff1f; } .folderRow ~ .fileRow:not(.folderRow)::before { content: ""; margin-right: 5px; } .folderRow { text-transform: capitalize; } .folderRow::before { /* disclosure triangle to show hierarchy */ content: "\00a0\00a0\25b7"; /* two non-breaking spaces and a white right triangle */ font-size: 0.75em; /* a little smaller */ margin-right: 5px; display: inline-block; transition: transform 0.3s ease; transform-origin: center; } .folderRow.open::before { transform: rotate(90deg); } section h1, section h2, section p, .fileRow { padding: 2px 1rem 2px 1rem; display: block; line-height: 1; } section ul { padding-left: 2px; } section a { color: lightblue; } section a:visited { color: plum; } ================================================ FILE: web/playground/src/sidebar/Sidebar.jsx ================================================ import "./Sidebar.css"; import React from "react"; import { useState } from "react"; function Sidebar({ library, onLoadFile }) { function loadFile(section, file) { onLoadFile(file, library[section][file]); } function toggleFolder(id) { openFolders[id] = !Boolean(openFolders[id]); setOpenFolders(() => ({ ...openFolders })); } function handleClick(section, file, id) { if (isFile(file)) { loadFile(section, file); } else { toggleFolder(id); } } function isFile(path) { return path.endsWith(".prql"); } const sections = []; const [openFolders, setOpenFolders] = useState({}); for (const [section, files] of Object.entries(library)) { const fileRows = []; for (const [index, filename] of Object.keys(files).entries()) { const array = files[filename]; const depth = array[2]; const parent = array[3]; const id = array[4]; const name = array[5]; fileRows.push( {(parent == null || openFolders[parent] || depth === 0) && (
handleClick(section, filename, id)} > {name ?? filename}
)}
, ); } sections.push(

{section}

{fileRows}
, ); } return (

PRQL Playground

External links

{sections}
); } export default Sidebar; ================================================ FILE: web/playground/src/workbench/Workbench.css ================================================ .column { height: 100%; display: flex; flex-direction: column; position: relative; overflow: hidden; } .tabs { display: flex; flex-grow: 1; overflow-y: hidden; } .tab { flex-grow: 1; flex-basis: 0; max-width: 50%; height: 100%; display: flex; flex-direction: column; overflow-x: hidden; } .tab:not(:first-child) { border-left: 1px solid #666; } .tab > * { flex-grow: 1; } .tab-top { flex-grow: 0; display: flex; align-items: center; } .tab-top button { border: 0; background: dimgrey; color: inherit; font-size: inherit; font-family: inherit; cursor: pointer; } .tab-top .action { padding: 2px 1em; font-size: 80%; text-decoration: underline; color: lightblue; } .spacer { flex-grow: 1; } .tab-title { flex-grow: 0; align-self: start; padding: 3px 0.75em; display: inline; } .tab-title.active { background: #222; } .error-pane { padding: 1rem; margin: 0; width: 100%; max-height: 50%; overflow-y: auto; flex-shrink: 0; font-family: monospace; white-space: pre-wrap; border-top: 1px solid; border-color: red; background-color: #552222; } .tab-content { background: #222; } pre.hljs { margin: 0; background: #222; height: 100%; overflow-y: scroll; padding-left: 2em; } pre.hljs code { font-size: 14px; } ================================================ FILE: web/playground/src/workbench/Workbench.jsx ================================================ import "./Workbench.css"; import * as prql from "prqlc/dist/bundler"; import React from "react"; import YAML from "yaml"; import Editor, { loader } from "@monaco-editor/react"; import * as monaco from "monaco-editor"; import * as monacoTheme from "./monaco-theme.json"; import prqlSyntax from "./prql-syntax"; import Output from "../output/Output"; import * as duckdb from "./duckdb"; loader.config({ monaco }); class Workbench extends React.Component { monaco = null; editor = null; duckdb = null; state = { filename: "input.prql", prql: "", output: null, outputTab: "arrow", prqlError: null, duckdbError: null, }; loadFile(filename, [outputTab, content]) { this.setState({ filename, outputTab, prql: content }); if (this.editor) { this.editor.setValue(content); } } componentDidMount() { this.props.setCallables({ loadFile: (f, c) => this.loadFile(f, c) }); if (!this.duckdb) { this.duckdb = duckdb.init(); } } beforeEditorMount(monaco) { this.monaco = monaco; monaco.editor.defineTheme("blackboard", monacoTheme); monaco.languages.register({ id: "prql", extensions: ["prql"] }); monaco.languages.setLanguageConfiguration("prql", { comments: { lineComment: "#", }, }); monaco.languages.setMonarchTokensProvider("prql", prqlSyntax); } onEditorMount(editor) { this.editor = editor; this.compile(editor.getValue()); } async compile(value) { this.setState({ prql: value }); let sql; try { sql = prql.compile(value); this.setState({ prqlError: null }); this.monaco.editor.setModelMarkers(this.editor.getModel(), "prql", []); } catch (e) { if (e instanceof WebAssembly.RuntimeError) { this.setState({ prqlError: "A compiler bug was encountered. Please copy/paste the PRQL query into a new report at:\n" + " https://github.com/PRQL/prql/issues/new?template=bug_report.yaml", }); return; } const errors = JSON.parse(e.message).inner; this.setState({ prqlError: errors[0].display }); const monacoErrors = errors.map((error) => ({ severity: "error", message: error.reason, startLineNumber: error.location?.start[0] + 1, startColumn: error.location?.start[1] + 1, endLineNumber: error.location?.end[0] + 1, endColumn: error.location?.end[1] + 1, })); this.monaco.editor.setModelMarkers( this.editor.getModel(), "prql", monacoErrors, ); return; } let pl; try { if (sql) { pl = prql.prql_to_pl(value); } } catch (ignored) {} let rq; try { if (pl) { rq = prql.pl_to_rq(pl); } } catch (ignored) { console.log(ignored); } let arrow; const c = await (await this.duckdb).connect(); try { arrow = await c.query(sql); this.setState({ duckdbError: null }); } catch (e) { this.setState({ duckdbError: e.toString() }); arrow = null; } finally { c.close(); } if (pl) { pl = YAML.stringify(JSON.parse(pl)); } if (rq) { rq = YAML.stringify(JSON.parse(rq)); } const output = { sql, arrow, pl, rq }; this.setState({ output }); } save() { if (!this.editor) return; this.props.onSaveFile(this.state.filename, [ this.state.outputTab, this.state.prql, ]); } rename() { let filename = prompt(`New name for ${this.state.filename}`); if (filename) { if (!filename.endsWith(".prql")) { filename += ".prql"; } this.setState({ filename }); } } render() { return (
{this.state.filename}
this.compile(v)} onMount={(e, m) => this.onEditorMount(e, m)} beforeMount={(m) => this.beforeEditorMount(m)} theme="blackboard" options={{ minimap: { enabled: false }, scrollBeyondLastLine: false, fontSize: 14, }} />
this.setState({ outputTab: tab })} >
{/* Display an error message relevant to the tab */} {this.state.prqlError && (
{this.state.prqlError}
)} {this.state.outputTab === "arrow" && this.state.duckdbError && (
{this.state.duckdbError}
)}
); } } export default Workbench; ================================================ FILE: web/playground/src/workbench/duckdb.js ================================================ import * as duckdb from "@duckdb/duckdb-wasm"; export async function init() { const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles(); // Select a bundle based on browser checks const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES); const worker_url = URL.createObjectURL( new Blob([`importScripts("${bundle.mainWorker}");`], { type: "text/javascript", }), ); // Instantiate the asynchronous version of DuckDB-wasm const worker = new Worker(worker_url); const logger = new duckdb.ConsoleLogger(); const db = new duckdb.AsyncDuckDB(logger, worker); await db.instantiate(bundle.mainModule, bundle.pthreadWorker); URL.revokeObjectURL(worker_url); await registerChinook(db); return db; } export const CHINOOK_TABLES = [ "albums", "artists", "customers", "employees", "genres", "invoice_items", "invoices", "media_types", "playlists", "playlist_track", "tracks", ]; async function registerChinook(db) { const baseUrl = `${window.location.href}/data/chinook`; await Promise.all( CHINOOK_TABLES.map(async (table) => { const res = await fetch(`${baseUrl}/${table}.csv`); const text = await res.text(); db.registerFileText(`${table}.csv`, text); }), ); const c = await db.connect(); for (const table of CHINOOK_TABLES) { await c.insertCSVFromPath(`${table}.csv`, { name: table, detect: true, header: true, }); } c.close(); } ================================================ FILE: web/playground/src/workbench/monaco-theme.json ================================================ { "base": "vs-dark", "inherit": true, "rules": [ { "background": "0C1021", "token": "" }, { "foreground": "aeaeae", "token": "comment" }, { "foreground": "d8fa3c", "token": "constant" }, { "foreground": "ff6400", "token": "entity" }, { "foreground": "fbde2d", "token": "keyword" }, { "foreground": "fbde2d", "token": "storage" }, { "foreground": "61ce3c", "token": "string" }, { "foreground": "61ce3c", "token": "meta.verbatim" }, { "foreground": "8da6ce", "token": "support" }, { "foreground": "ab2a1d", "fontStyle": "italic", "token": "invalid.deprecated" }, { "foreground": "f8f8f8", "background": "9d1e15", "token": "invalid.illegal" }, { "foreground": "ff6400", "fontStyle": "italic", "token": "entity.other.inherited-class" }, { "foreground": "ff6400", "token": "string constant.other.placeholder" }, { "foreground": "becde6", "token": "meta.function-call.py" }, { "foreground": "7f90aa", "token": "meta.tag" }, { "foreground": "7f90aa", "token": "meta.tag entity" }, { "foreground": "ffffff", "token": "entity.name.section" }, { "foreground": "d5e0f3", "token": "keyword.type.variant" }, { "foreground": "f8f8f8", "token": "source.ocaml keyword.operator.symbol" }, { "foreground": "8da6ce", "token": "source.ocaml keyword.operator.symbol.infix" }, { "foreground": "8da6ce", "token": "source.ocaml keyword.operator.symbol.prefix" }, { "fontStyle": "underline", "token": "source.ocaml keyword.operator.symbol.infix.floating-point" }, { "fontStyle": "underline", "token": "source.ocaml keyword.operator.symbol.prefix.floating-point" }, { "fontStyle": "underline", "token": "source.ocaml constant.numeric.floating-point" }, { "background": "ffffff08", "token": "text.tex.latex meta.function.environment" }, { "background": "7a96fa08", "token": "text.tex.latex meta.function.environment meta.function.environment" }, { "foreground": "fbde2d", "token": "text.tex.latex support.function" }, { "foreground": "ffffff", "token": "source.plist string.unquoted" }, { "foreground": "ffffff", "token": "source.plist keyword.operator" } ], "colors": { "editor.foreground": "#e0e2e4", "editor.background": "#222222", "editor.selectionBackground": "#253B76", "editor.lineHighlightBackground": "#FFFFFF1F", "editorCursor.foreground": "#FFFFFFA6", "editorWhitespace.foreground": "#FFFFFF40" } } ================================================ FILE: web/playground/src/workbench/prql-syntax.js ================================================ const TRANSFORMS = [ "aggregate", "append", "derive", "filter", "from_text", "from", "group", "join", "select", "sort", "take", "union", "window", ]; const MODULES = ["date", "math", "text"]; const BUILTIN_FUNCTIONS = ["case"]; // "in", "as" const KEYWORDS = ["let", "prql"]; const LITERALS = ["null", "true", "false"]; const def = { // Set defaultToken to invalid to see what you do not tokenize yet // defaultToken: 'invalid', keywords: [ ...TRANSFORMS, ...MODULES, ...BUILTIN_FUNCTIONS, ...KEYWORDS, ...LITERALS, ], operators: [ "+", "-", "*", "/", "//", "%", // "**", "==", "!=", "->", "=>", ">", "<", ">=", "<=", "~=", "&&", "||", "??", ], // The main tokenizer for our languages tokenizer: { root: [ // comments { include: "@comment" }, // named-args [/(\w+)\s*:/, { cases: { $1: "key" } }], // identifiers and keywords [ /[a-z_$][\w$]*/, { cases: { "@keywords": "keyword", "@default": "identifier" } }, ], // whitespace { include: "@whitespace" }, // delimiters [/[()[\]]/, "@brackets"], // numbers // Slightly modified from https://stackoverflow.com/a/23872060/3064736; // it requires a number after a decimal point, so ranges appear as // ranges. // We disallow a leading word character, so that we don't highlight // a number in `foo_1`, // We allow underscores, a bit more liberally than PRQL, which doesn't // allow them at the start or end (but that's difficult to express with // regex; contributions welcome). [/[+-]?[^\w](([\d_]+(\.[\d_]+])?)|(\.[\d_]+))/, "number"], // strings [/"([^"\\]|\\.)*$/, "string.invalid"], // non-terminated string [/"/, { token: "string.quote", bracket: "@open", next: "@string" }], // characters [/'[^\\']'/, "string"], ], comment: [[/#.*/, "comment"]], string: [ [/[^\\"]+/, "string"], [/"/, { token: "string.quote", bracket: "@close", next: "@pop" }], ], whitespace: [ [/[ \t\r\n]+/, "white"], [/\/\*/, "comment", "@comment"], [/\/\/.*$/, "comment"], ], }, }; export default def; ================================================ FILE: web/playground/vite.config.js ================================================ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import wasm from "vite-plugin-wasm"; // https://vitejs.dev/config/ export default defineConfig({ build: { target: "esnext", }, base: "/playground/playground", plugins: [react(), wasm()], }); ================================================ FILE: web/prql-codemirror-demo/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: web/prql-codemirror-demo/README.md ================================================ # CodeMirror with PRQL demo This is a demo of CodeMirror with PRQL. We don't have any published `lang-prql` or `prql-lezer` package yet. ## Instructions Build `prql-lezer` then copy the files from `/grammars/prql-lezer/dist/` to `src/lang-prql/prql-lezer/`. ``` mkdir src/lang-prql/prql-lezer cd ../../grammars/prql-lezer/ npm run build cp dist/* ../../web/prql-codemirror-demo/src/lang-prql/prql-lezer/ cd ../../web/prql-codemirror-demo/ npm run dev ``` ================================================ FILE: web/prql-codemirror-demo/index.html ================================================ CodeMirror with PRQL
================================================ FILE: web/prql-codemirror-demo/package.json ================================================ { "name": "prql-codemirror-demo", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview" }, "devDependencies": { "typescript": "^5.3.2", "vite": "^6.4.1" }, "dependencies": { "@codemirror/autocomplete": "^6.11.0", "@codemirror/commands": "^6.3.0", "@codemirror/language": "^6.9.2", "@codemirror/lint": "^6.4.2", "@codemirror/search": "^6.5.4", "@codemirror/theme-one-dark": "^6.1.2", "@codemirror/view": "^6.22.0" } } ================================================ FILE: web/prql-codemirror-demo/src/codemirror.ts ================================================ // This file a copy of https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts import { keymap, highlightSpecialChars, drawSelection, highlightActiveLine, dropCursor, rectangularSelection, crosshairCursor, lineNumbers, highlightActiveLineGutter, } from "@codemirror/view"; import { Extension, EditorState } from "@codemirror/state"; import { defaultHighlightStyle, syntaxHighlighting, indentOnInput, bracketMatching, foldGutter, foldKeymap, } from "@codemirror/language"; import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; import { searchKeymap, highlightSelectionMatches } from "@codemirror/search"; import { autocompletion, completionKeymap, closeBrackets, closeBracketsKeymap, } from "@codemirror/autocomplete"; import { lintKeymap } from "@codemirror/lint"; // (The superfluous function calls around the list of extensions work // around current limitations in tree-shaking software.) /// This is an extension value that just pulls together a number of /// extensions that you might want in a basic editor. It is meant as a /// convenient helper to quickly set up CodeMirror without installing /// and importing a lot of separate packages. /// /// Specifically, it includes... /// /// - [the default command bindings](#commands.defaultKeymap) /// - [line numbers](#view.lineNumbers) /// - [special character highlighting](#view.highlightSpecialChars) /// - [the undo history](#commands.history) /// - [a fold gutter](#language.foldGutter) /// - [custom selection drawing](#view.drawSelection) /// - [drop cursor](#view.dropCursor) /// - [multiple selections](#state.EditorState^allowMultipleSelections) /// - [reindentation on input](#language.indentOnInput) /// - [the default highlight style](#language.defaultHighlightStyle) (as fallback) /// - [bracket matching](#language.bracketMatching) /// - [bracket closing](#autocomplete.closeBrackets) /// - [autocompletion](#autocomplete.autocompletion) /// - [rectangular selection](#view.rectangularSelection) and [crosshair cursor](#view.crosshairCursor) /// - [active line highlighting](#view.highlightActiveLine) /// - [active line gutter highlighting](#view.highlightActiveLineGutter) /// - [selection match highlighting](#search.highlightSelectionMatches) /// - [search](#search.searchKeymap) /// - [linting](#lint.lintKeymap) /// /// (You'll probably want to add some language package to your setup /// too.) /// /// This extension does not allow customization. The idea is that, /// once you decide you want to configure your editor more precisely, /// you take this package's source (which is just a bunch of imports /// and an array literal), copy it into your own code, and adjust it /// as desired. export const basicSetup: Extension = (() => [ lineNumbers(), highlightActiveLineGutter(), highlightSpecialChars(), history(), foldGutter(), drawSelection(), dropCursor(), EditorState.allowMultipleSelections.of(true), indentOnInput(), syntaxHighlighting(defaultHighlightStyle, { fallback: true }), bracketMatching(), closeBrackets(), autocompletion(), rectangularSelection(), crosshairCursor(), highlightActiveLine(), highlightSelectionMatches(), keymap.of([ ...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap, ...historyKeymap, ...foldKeymap, ...completionKeymap, ...lintKeymap, ]), ])(); /// A minimal set of extensions to create a functional editor. Only /// includes [the default keymap](#commands.defaultKeymap), [undo /// history](#commands.history), [special character /// highlighting](#view.highlightSpecialChars), [custom selection /// drawing](#view.drawSelection), and [default highlight /// style](#language.defaultHighlightStyle). export const minimalSetup: Extension = (() => [ highlightSpecialChars(), history(), drawSelection(), syntaxHighlighting(defaultHighlightStyle, { fallback: true }), keymap.of([...defaultKeymap, ...historyKeymap]), ])(); export { EditorView } from "@codemirror/view"; ================================================ FILE: web/prql-codemirror-demo/src/lang-prql/complete.ts ================================================ import { Completion, completeFromList, ifNotIn, snippetCompletion as snip, } from "@codemirror/autocomplete"; const dontComplete = [ "Comment", "Docblock", "String", "FString", "RString", "SString", ]; const globals: readonly Completion[] = ["false", "null", "true"] .map((n) => ({ label: n, type: "constant" })) .concat( [ "bool", "float", "int", "int8", "int16", "int32", "int64", "int128", "text", "date", "time", "timestamp", ].map((n) => ({ label: n, type: "type" })), ) .concat( [ // aggregate-functions "any", "average", "concat_array", "count", "every", "max", "min", "stddev", "sum", // file-reading-functions "read_csv", "read_json", "read_parquet", // list-functions "all", "map", "zip", "_eq", "_is_null", // misc-functions "from_text", // window-functions "lag", "lead", "first", "last", "rank", "rank_dense", "row_number", ].map((n) => ({ label: n, type: "function" })), ) .concat( [ "aggregate", "derive", "filter", "from", "group", "join", "select", "sort", "take", "window", ].map((n) => ({ label: n, type: "keyword" })), ) .concat(["date", "math", "text"].map((n) => ({ label: n, type: "module" }))) .concat(["std"].map((n) => ({ label: n, type: "namespace" }))); export const snippets: readonly Completion[] = [ snip( "from ${table_table}\nselect {${column_name}}\nfilter ${column_name} == ${condition}\ntake {amount}", { label: "from-select-filter-take", detail: "snippet", type: "text", }, ), ]; /// Autocompletion for built-in PRQL globals and keywords. export const globalCompletion = ifNotIn( dontComplete, completeFromList(globals.concat(snippets)), ); ================================================ FILE: web/prql-codemirror-demo/src/lang-prql/prql.ts ================================================ import { parser } from "./prql-lezer"; import { delimitedIndent, indentNodeProp, foldNodeProp, foldInside, LRLanguage, LanguageSupport, } from "@codemirror/language"; import { globalCompletion } from "./complete"; /// A language provider based on the [Lezer PRQL /// parser](https://github.com/PRQL/prql/tree/main/grammars/prql-lezer), extended with /// highlighting and indentation information. export const prqlLanguage = LRLanguage.define({ name: "prql", parser: parser.configure({ props: [ indentNodeProp.add({ TupleExpression: delimitedIndent({ closing: "}" }), ArrayExpression: delimitedIndent({ closing: "]" }), }), foldNodeProp.add({ "ArrayExpression TupleExpression": foldInside, }), ], }), languageData: { closeBrackets: { brackets: ["(", "[", "{", "'", '"', "'''", '"""'], stringPrefixes: ["f", "r", "s"], }, commentTokens: { line: "#" }, }, }); /// PRQL language support. export function prql() { return new LanguageSupport(prqlLanguage, [ prqlLanguage.data.of({ autocomplete: globalCompletion }), ]); } ================================================ FILE: web/prql-codemirror-demo/src/main.ts ================================================ import "./style.css"; import { EditorView, basicSetup } from "./codemirror.ts"; import { prql } from "./lang-prql/prql.ts"; import { oneDark } from "@codemirror/theme-one-dark"; document.querySelector("#app")!.innerHTML = `

PRQL CodeMirror demo

PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement.

`; let doc = `from invoices filter invoice_date >= @1970-01-16 derive { transaction_fees = 0.8, income = total - transaction_fees } filter income > 1 group customer_id ( aggregate { average total, sum_income = sum income, ct = count total, } ) sort {-sum_income} take 10 join c=customers (==customer_id) derive name = f"{c.last_name}, {c.first_name}" select { c.customer_id, name, sum_income } derive db_version = s"version()" `; new EditorView({ doc, extensions: [basicSetup, oneDark, prql()], parent: document.getElementById("editor")!, }); ================================================ FILE: web/prql-codemirror-demo/src/style.css ================================================ :root { color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; } .cm-editor { max-height: 400px; border: 1px solid silver; } ================================================ FILE: web/prql-codemirror-demo/src/vite-env.d.ts ================================================ /// ================================================ FILE: web/prql-codemirror-demo/tsconfig.json ================================================ { "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, "module": "ESNext", "lib": ["ES2020", "DOM", "DOM.Iterable"], "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true }, "include": ["src"] } ================================================ FILE: web/website/.gitignore ================================================ public/ .hugo_build.lock ================================================ FILE: web/website/README.md ================================================ # PRQL Website A [hugo](https://gohugo.io/) website with PRQL theme, which uses minimal [water.css](https://watercss.kognise.dev/) styling. Serve: ```sh cd website hugo server ``` To add pages, just add files in `content/` directory. ================================================ FILE: web/website/archetypes/default.md ================================================ --- title: "{{ replace .Name "-" " " | title }}" date: {{ .Date }} draft: true --- ================================================ FILE: web/website/config.yaml ================================================ baseURL: "https://prql-lang.org/" languageCode: en-us title: PRQL theme: prql-theme disableKinds: - taxonomy - term enableGitInfo: true menu: nav: # TODO: Can we use the info from pages directly? https://gohugo.io/templates/menu-templates/#menu-entries-from-the-pages-front-matter - identifier: Docs name: Docs url: /book/ weight: 1 - identifier: Playground name: Playground url: /playground/ weight: 2 - identifier: Roadmap name: Roadmap url: /roadmap/ weight: 3 - identifier: Posts name: Posts url: /posts/ weight: 4 - identifier: FAQ name: FAQ url: /faq/ weight: 5 params: contribute: subtitle: | Contribute to PRQL — we're a welcoming community, and there are lots of ways to make an impact: list: - text: Are there people whose opinion you respect who might be interested in PRQL? Send them a link to PRQL. - text: "Know Rust (or want to learn!)? [Contribute to our compiler](https://prql-lang.org/book/project/contributing/#compiler)" - text: "Familiar with web? Spot a mistake? [Improve the website design or copy](https://prql-lang.org/book/project/contributing/#marketing)" - text: "Interested in helping people explore their data? [Help improve the playground](https://github.com/PRQL/prql/tree/main/web/playground)" - text: "Is SQL your mother-tongue? [Show us SQL queries which translate well / badly](https://prql-lang.org/book/project/contributing/#language-design). We are looking for any use-cases that expose a poor design choice, a need of a feature, or a sharp edge of the language." button: text: Contribute link: https://prql-lang.org/book/project/contributing/ follow: subtitle: "Follow us:" list: - text: Star our GitHub repo [PRQL/prql](https://github.com/PRQL/prql) - text: Join our [Discord](https://discord.com/invite/eQcfaCmsNc) - text: Follow us on [Twitter](https://twitter.com/prql_lang) description: PRQL is a modern language for transforming data images: - static/img/favicon-32x32.png title: PRQL license: "© 2022-2024, PRQL Developers. Apache 2.0 Licensed" googleAnalytics: G-LQJDD599T4 ================================================ FILE: web/website/content/_index.md ================================================ --- ####################### General ######################### layout: home title: PRQL hero_section: enable: true heading: "PRQL is a modern language for transforming data" bottom_text: "— a simple, powerful, pipelined SQL replacement" button: enable: false link: https://prql-lang.org/book/ label: "Reference" # the PRQL example is defined in data/examples/hero.yaml why_prql_section: enable: true title: "Why PRQL?" items: - title: For data engineers content: - PRQL is concise, with abstractions such as variables & functions - PRQL is database agnostic, compiling to many dialects of SQL - PRQL isn't limiting — it can contain embedded SQL where necessary - PRQL has bindings to most major languages _(and more are in progress)_ - PRQL allows for column lineage and type inspection _(in progress)_ - title: For analysts content: - PRQL is ergonomic for data exploration — for example, commenting out a filter, or a column in a list, maintains a valid query - PRQL is simple, and easy to understand, with a small number of powerful concepts - PRQL allows for powerful autocomplete, type-checking, and helpful error messages _(in progress)_ - title: For tools content: - PRQL's vision is a foundation to build on; we're open-source and will never have a commercial product - PRQL is growing into a single secular standard which tools can target - PRQL is easy for machines to read & write - title: For HackerNews enthusiasts content: - The PRQL compiler is written in Rust - We talk about "orthogonal language features" a lot showcase_section: enable: true title: "Showcase" content: - PRQL consists of a curated set of orthogonal transformations, which are combined together to form a pipeline. That makes it easy to compose and extend queries. The language also benefits from modern features, such syntax for dates, ranges and f-strings as well as functions, type checking and better null handling. buttons: - link: "/playground/" label: "Playground" - link: "/book/" label: "Book" examples: # The examples are defined in data/examples/, this list just defines their order. - basic - friendly-syntax - orthogonal - expressions - f-strings - windows - functions - top-n - s-strings - joins - null-handling - dialects # Currently excluded because it's lots of text # prql: | # # Check out how much simpler this is relative to the SQL... # let track_plays = ( # Assign with `let` # from plays # group [track] ( # aggregate [ # total = count, # unfinished = sum is_unfinished, # started = sum is_started, # ] # ) # ) principles_section: enable: true title: "Principles" items: - title: "Pipelined" main_text: "A PRQL query is a linear pipeline of transformations" content: Each line of the query is a transformation of the previous line’s result. This makes it easy to read, and simple to write. - title: "Simple" main_text: "PRQL serves both sophisticated engineers and analysts without coding experience." content: By providing a small number of powerful & orthogonal primitives, queries are simple and composable — there's only one way of expressing each operation. We can eschew the debt that SQL has built up. - title: "Open" main_text: "PRQL is open-source, with an open community" content: PRQL will always be fully open-source and will never have a commercial product. By compiling to SQL, PRQL is compatible with most databases, existing tools, and programming languages that manage SQL. We're a welcoming community for users, contributors, and other projects. - title: "Extensible" main_text: "PRQL is designed to be extended, from functions to language bindings" content: PRQL has abstractions which make it a great platform to build on. Its explicit versioning allows changes without breaking backward-compatibility. And in the cases where PRQL doesn't yet have an implementation, it allows embedding SQL with s-strings. - title: "Analytical" main_text: "PRQL's focus is analytical queries" content: PRQL was originally designed to serve the growing need of writing analytical queries, emphasizing data transformations, development speed, and readability. We de-emphasize other SQL features such as inserting data or transactions. videos_section: enable: true title: "Pipelines in action" items: - youtube_id: IQRRsfavEic tools_section: enable: true title: "Tools" sections: - link: https://prql-lang.org/playground/ label: "Playground" text: "Online in-browser playground that compiles PRQL to SQL as you type." - link: https://pyprql.readthedocs.io/ label: "pyprql" text: | Provides Jupyter/IPython cell magic and Pandas accessor. `pip install pyprql` - link: https://crates.io/crates/prqlc label: "prqlc" text: | A CLI for PRQL compiler, written in Rust. `cargo install prqlc` `brew install prqlc` `winget install prqlc` integrations_section: enable: true title: "Integrations" sections: - label: "ClickHouse" link: https://clickhouse.com/docs/en/guides/developer/alternative-query-languages text: | ClickHouse natively supports PRQL with `SET dialect = 'prql'` - label: "Jupyter/IPython" link: https://pyprql.readthedocs.io/en/latest/magic_readme.html text: "pyprql contains a Jupyter extension, which executes a PRQL cell against a database. It can also set up an in-memory DuckDB instance, populated with a pandas DataFrame." - label: "DuckDB" link: https://github.com/ywelsch/duckdb-prql text: A DuckDB extension to execute PRQL - label: "QStudio" link: https://www.timestored.com/qstudio/prql-ide text: "QStudio is a SQL GUI that lets you browse tables, run SQL scripts, and chart and export the results. QStudio runs on Windows, macOS and Linux, and works with every popular database including mysql, postgresql, mssql, kdb..." - label: "Prefect" link: https://prql-lang.org/book/project/integrations/prefect.html text: Add PRQL models to your Prefect workflows with a single function. - label: Visual Studio Code link: https://marketplace.visualstudio.com/items?itemName=prql-lang.prql-vscode text: Extension with syntax highlighting and live SQL compilation. - label: "PostgreSQL" link: https://github.com/kaspermarstal/plprql text: Write PRQL functions in PostgreSQL - label: "Databend" link: https://www.databend.com/blog/2024-04-03-databend-integrates-prql/ text: Databend natively supports PRQL bindings_section: enable: true title: "Bindings" section_id: "bindings" sections: - link: https://pypi.org/project/prqlc label: "prqlc-python" text: Python bindings for prqlc. - link: https://www.npmjs.com/package/prqlc label: "prqlc-js" text: "JavaScript bindings for prqlc." - link: https://CRAN.R-project.org/package=prqlr label: "prqlr" text: "R bindings for prqlc." - link: "https://crates.io/crates/prqlc" label: "prqlc" text: | Compiler implementation, written in Rust. Compile, format & annotate PRQL queries. - link: https://prql-lang.org/book/project/bindings/index.html label: Others text: | Java, C, C++, Elixir, .NET, and PHP have unsupported or nascent bindings. testimonials_section: enable: true title: "What people are saying" # The testimonials are in data/testimonials.yaml. --- ================================================ FILE: web/website/content/demos/ace-demo.html ================================================ Ace with PRQL
# https://ace.c9.io/ from invoices filter invoice_date >= @1970-01-16 derive { transaction_fees = 0.8, income = total - transaction_fees } filter income > 1 group customer_id ( aggregate { average total, sum_income = sum income, ct = count total, } ) sort {-sum_income} take 10 join c=customers (==customer_id) derive name = f"{c.last_name}, {c.first_name}" select { c.customer_id, name, sum_income } derive db_version = s"version()"
================================================ FILE: web/website/content/faq.md ================================================ --- title: "FAQ" --- Here are some of the most common questions we hear. Have something else you'd like to ask? Pop by our [Discord](https://discord.com/invite/eQcfaCmsNc) and ask away! {{< faq "Cool story Hansel, but what can I actually do with PRQL _now_?" >}} PRQL is ready to use by the intrepid, either with our supported integrations, or within your own tools, using one of our supported language bindings. The easiest way is with our integrations: - **Prototype your PRQL queries** in the [Playground](https://prql-lang.org/playground/) or the [VS Code extension](https://marketplace.visualstudio.com/items?itemName=PRQL-lang.prql-vscode) and copy/paste the resulting SQL into your database. It's not the perfect workflow, but it's easy to get started. - **[Jupyter](https://pyprql.readthedocs.io/en/latest/magic_readme.html)** allows writing PRQL in a Jupyter notebook or IPython repl, with a `%%prql` magic. As well as connecting to existing DBs, our integration with DuckDB enables querying pandas dataframes, CSVs & Parquet files, and writing the output to a dataframe. - **[DuckDB extension](https://github.com/ywelsch/duckdb-prql)** — is a DuckDB extension which allows querying a DuckDB instance with PRQL. It's also possible to query PRQL from your code with our [bindings](/#bindings) for R, Rust, Python & JS. For an example of using PRQL with DuckDB, check out [Querying with PRQL](https://eitsupi.github.io/querying-with-prql/). {{}} {{< faq "Something here reminds me of another project, did you take the idea from them?" >}} Yes, probably. We're standing on the shoulders of giants: - [dplyr](https://dplyr.tidyverse.org/) is a beautiful language for manipulating data, in R. It's very similar to PRQL. It only works on in-memory R data. - There's also [dbplyr](https://dbplyr.tidyverse.org/) which compiles a subset of dplyr to SQL, though requires an R runtime. - [Kusto](https://learn.microsoft.com/en-us/kusto/query/tutorials/learn-common-operators?view=azure-data-explorer&preserve-view=true&pivots=azuredataexplorer) is also a beautiful pipelined language, similar to PRQL. But it can only use Kusto-compatible DBs. - [Against SQL](https://www.scattered-thoughts.net/writing/against-sql/) gives a fairly complete description of SQL's weaknesses, both for analytical and transactional queries. [**@jamii**](https://github.com/jamii) consistently writes insightful pieces, and it's worth sponsoring him for his updates. There are [other](https://buttondown.email/jaffray/archive/sql-scoping-is-surprisingly-subtle-and-semantic/) similar pieces out there. - Julia's [DataPipes.jl](https://gitlab.com/aplavin/DataPipes.jl) & [Chain.jl](https://github.com/jkrumbiegel/Chain.jl) demonstrate how effective point-free pipelines can be, and how line breaks can work as pipes. - [OCaml](https://ocaml.org/)'s elegant and simple syntax serves as inspiration. And there are many projects similar to PRQL: - [Ecto](https://hexdocs.pm/ecto/Ecto.html#module-query) is a sophisticated ORM library in Elixir which has pipelined queries as well as more traditional ORM features. - [Morel](https://www.thestrangeloop.com/2021/morel-a-functional-query-language.html) is a functional language for data, also with a pipeline concept. It doesn't compile to SQL but states that it can access external data. - [Malloy](https://github.com/looker-open-source/malloy) from Looker & [**@lloydtabb**](https://github.com/lloydtabb) in a new language which combines a declarative syntax for querying with a modelling layer. - [EdgeDB](https://www.edgedb.com/) is an alternative to SQL focused on traditional transactional workloads (as opposed to PRQL's focus on analytical workloads). Their post [We can do better than SQL](https://www.edgedb.com/blog/we-can-do-better-than-sql) contains many of the criticisms of SQL that inspired PRQL. - [FunSQL.jl](https://github.com/MechanicalRabbit/FunSQL.jl) is a library in Julia which compiles a nice query syntax to SQL. It requires a Julia runtime. - [LINQ](https://learn.microsoft.com/en-us/dotnet/csharp/linq/get-started/write-linq-queries), is a pipelined language for the `.NET` ecosystem which can (mostly) compile to SQL. It was one of the first languages to take this approach. - [Sift](https://github.com/RCHowell/Sift) is an experimental language which heavily uses pipes and relational algebra. > If any of these descriptions can be improved, please feel free to PR changes. {{}} {{< faq "How is PRQL different from all the projects that SQL has defeated?" >}} Many languages have attempted to replace SQL, and yet SQL has massively _grown_ in usage and importance in the past decade. There are lots [of](https://twitter.com/seldo/status/1513599841355526145) [reasonable](https://benn.substack.com/p/has-sql-gone-too-far?s=r#footnote-anchor-2) [critiques](https://erikbern.com/2018/08/30/i-dont-want-to-learn-your-garbage-query-language.html) on these attempts. So a reasonable question is "Why are y'all building something that many others have failed at?". Some thoughts: - PRQL is open. It's not designed for a specific database. PRQL will always be fully open-source. There will never be a commercial product. We'll never have to balance profitability against compatibility, or try and expand up the stack to justify a valuation. Whether someone is building a new tool or writing a simple query — PRQL can be _more_ compatible across DBs than SQL. - PRQL is analytical. The biggest growth in SQL usage over the past decade has been from querying large amounts of data, often from analytical DBs that are specifically designed for this — with columnar storage and wide denormalized tables. SQL carries a lot of unrelated baggage, and focusing on the analytical use-case lets us make a better language. - PRQL is simple. There's often a tradeoff between power and accessibility — e.g. rust is powerful vs. Excel is accessible — but there are also instances where we can expand the frontier. PRQL's orthogonality is an example of synthesizing this tradeoff — have a single `filter` rather than `WHERE` & `HAVING` & `QUALIFY` brings both more power _and_ more accessibility. In the same way that "SQL was invented in the 1970s and therefore must be bad" is questionable logic, "`n` languages have tried and failed so therefore SQL cannot be improved." suffers a similar fallacy. SQL isn't bad because it's old. It's bad because — in some cases — it's bad. {{}} {{< faq "Which databases does PRQL work with?" >}} PRQL compiles to SQL, so it's compatible with any database that accepts SQL. A query's dialect can be explicitly specified, allowing for dialect-specific SQL to be generated. See the [Dialect docs](https://prql-lang.org/book/project/target.html) for more info; note that there is currently very limited implementation of this, and most dialects' implementation are identical to a generic implementation. {{}} {{< faq "What's this `aggregate` function?" >}} **...and why not just use `SELECT` & `GROUP BY`?** SQL uses `SELECT` for all of these: - Selecting or computing columns, without changing the shape of the data: ```sql SELECT x * 2 FROM y as two_x ``` - Reducing a column into a single row, with a reduction function: ```sql SELECT SUM(x) FROM y ``` - Reducing a column into groups, with a reduction function and a `GROUP BY` function: ```sql SELECT SUM(x) FROM y GROUP BY z ``` These are not orthogonal — `SELECT` does lots of different things depending on the context. It's difficult for both people and machines to evaluate the shape of the output. It's easy to mix meanings and raise an error (e.g. `SELECT x, MIN(y) FROM z`). PRQL clearly delineates two operations with two transforms: - `select` — picking & calculating columns. These calculations always produce exactly one output row for every input row. ```prql from employees select name = f"{first_name} {last_name}" ``` - `aggregate` — reducing multiple rows to a single row, with a reduction function like `sum` or `min`. ```prql from employees aggregate [total_salary = sum salary] ``` `aggregate` can then be used in a `group` transform, where it has exactly the same semantics on the group as it would on a whole table — another example of PRQL's orthogonality. ```prql from employees group department ( aggregate [total_salary = sum salary] ) ``` While you should be skeptical of new claims from new entrants [Hadley Wickham](https://twitter.com/hadleywickham), the developer of [Tidyverse](https://www.tidyverse.org/) [commented](https://news.ycombinator.com/item?id=30067406) in a discussion on PRQL: > FWIW the separate `group_by()` is one of my greatest design regrets with dplyr > — I wish I had made `by` a parameter of `summarise()`, `mutate()`, `filter()` > etc. For more detail, check out the docs in the [PRQL Book](https://prql-lang.org/book/reference/stdlib/transforms/aggregate.html). {{}} {{< faq "Can PRQL write to databases?" >}} PRQL is focused on analytical queries, so we don't currently support writing or modifying data in databases. However, PRQL queries can be used to generate SQL statements that write to databases. For example, surround the SQL output of a PRQL query in `CREATE OR REPLACE TABLE foo AS (...)`. {{}} {{< faq "Is it 'PRQL' or 'prql' or 'Prql'?" >}} It's `PRQL`, since it's a backronym! We name the repo and some libraries `prql` because of a strong convention around lowercase, but everywhere else we use `PRQL`. {{}} {{< faq "Where can I find the logos?" >}} See our [press materials](https://github.com/PRQL/prql-brand). {{}} ================================================ FILE: web/website/content/playground.html ================================================ --- title: "Playground" layout: big_iframe --- ================================================ FILE: web/website/content/posts/2022-05-19-examples.md ================================================ --- title: "Examples" date: 2022-05-19 layout: article url: examples --- ## A simple example Here's a fairly simple SQL query: ```sql SELECT TOP 20 title, country, AVG(salary) AS average_salary, SUM(salary) AS sum_salary, AVG(salary + payroll_tax) AS average_gross_salary, SUM(salary + payroll_tax) AS sum_gross_salary, AVG(salary + payroll_tax + benefits_cost) AS average_gross_cost, SUM(salary + payroll_tax + benefits_cost) AS sum_gross_cost, COUNT(*) AS ct FROM employees WHERE start_date > DATE('2021-01-01') AND salary + payroll_tax + benefits_cost > 0 GROUP BY title, country HAVING COUNT(*) > 2000 ORDER BY sum_gross_cost, country DESC ``` Even this simple query demonstrates some of the problems with SQL's lack of abstractions: - Unnecessary repetition — the calculations for each measure are repeated, despite deriving from a previous measure. The repetition in the `WHERE` clause obfuscates the meaning of the expression. - Functions have multiple operators — `HAVING` & `WHERE` are fundamentally similar operations applied at different stages of the pipeline, but SQL's lack of pipeline-based precedence requires it to have two different operators. - Operators have multiple functions — the `SELECT` operator both creates new aggregations, and selects which columns to include. - Awkward syntax — when developing the query, commenting out the final line of the `SELECT` list causes a syntax error because of how commas are handled, and we need to repeat the columns in the `GROUP BY` clause in the `SELECT` list. Here's the same query with PRQL: ```prql from employees # Each line transforms the previous result. filter start_date > @2021-01-01 # Clear date syntax. derive { # `derive` adds columns / variables. gross_salary = salary + payroll_tax, gross_cost = gross_salary + benefits_cost # Variables can use other variables. } filter gross_cost > 0 group {title, country} ( # `group` runs a pipeline over each group. aggregate { # `aggregate` reduces a column to a row. average salary, sum salary, average gross_salary, sum gross_salary, average gross_cost, sum_gross_cost = sum gross_cost, # `=` sets a column name. ct = count this, } ) sort {sum_gross_cost, -country} # `-country` means descending order. filter ct > 2_000 take 20 ``` As well as using variables to reduce unnecessary repetition, the query is also more readable — it flows from top to bottom, each line representing a transformation of the previous line's result. For example, `TOP 20` / `take 20` modify the final result in both queries — but only PRQL represents it as the final transformation. And context is localized — the `aggregate` transform is immediately wrapped in a `group` transform containing the columns to group by. While PRQL is designed for reading & writing by people, it's also much simpler for code to construct or edit PRQL queries. In SQL, adding a filter to a query involves parsing the query to find and then modify the `WHERE` statement, or wrapping the existing query in a CTE. In PRQL, adding a filter just involves appending a `filter` transformation to the query. For more examples, check out the [PRQL Book](https://prql-lang.org/book/). ================================================ FILE: web/website/content/posts/2023-01-07-functional-relations.md ================================================ --- title: A functional approach to relational queries date: 2023-01-07 authors: ["Aljaž Mur Eržen"] layout: article toc: true url: functional-relations --- I believe that many of the ideas from functional programming are very suitable for a query language. To make a case for it, I'll briefly explain a few features, adapt their syntax, and show how they are used in PRQL. ## Fancy functional features Functional programming is an old-school programming paradigm that is gradually gaining traction within a few common languages that are designed with procedural paradigm in mind. The many aspects of the paradigm have been discussed extensively, so I'll try to be brief. If you've already read them all, I suggest you skip to the next section. ### Pure functions This is the core aspect that allows us to do most of the magic later. When we say "pure function", we just mean the mathematical function and not "a procedure" or "a method". To be more precise: - Pure functions have no side effects. This means that no global state is altered. The function result is the only output that the function produces. - Pure functions produce the same output given the same args. The output does not depend on any external information; for example the current time or a random number generator. The most obvious effect is that for a pure function `my_function`, this: ```js res1 = my_function(arg); res2 = my_function(arg); res3 = my_function(arg); ``` ... is equivalent to: ```js res1 = my_function(arg); res2 = res1; res3 = res2; ``` Because `my_function` is guaranteed to return the same result when given `arg`, we can skip calling `my_function` for the second and the third time because we already know what the output will be. This may seem like a minor point, but we're just getting started. ### First-class citizens Functions being "first-class citizens" means that they are treated the same as any other value; they can be stored in a variable, they can be passed to another function as an argument or even be used in binary operations. ```js function double(x) { return 2 * x; } let preprocess = double; my_array = [4, 5, 6]; my_array.map(preprocess); // yields [8, 10, 12] ``` Here, we've stored function `double` in variable `preprocess` and then passed that to method `Array.map`, which applies a given function to each of the array elements. ### Currying Named after Haskell Curry, currying is an implicit act of converting a function call with missing arguments into a new function. ```js function add(x, y) { return x + y; } let add_one = add(1); ``` Because we didn't specify `y` parameter of `add` function, the result is a new function that is still waiting for the last argument. It is equivalent to defining add_one like this: ```js function add_one(y) { return add(1, y); } ``` ### Implicit function call Let's suppose that we have a pure function that doesn't take any arguments: ```js function my_function() { return 42; } ``` Because it is a pure function, it cannot depend on anything other than its non-existing arguments. Another way to phrase it, this is a constant expression. And as such, it might as well be defined as one: ```js let my_var = 42; ``` In conventional programming languages[^1], there is a difference between using `my_function()` and `my_var`. The first one evaluates the expression at the call site, while the second one evaluates it at the declaration site. It is also possible to express just `my_function`, a reference to a function that can be called without arguments. But if all functions are pure, we generalize by saying that all three cases are equivalent. To make this work, let's say that `my_function` is "implicitly invoked" just as it would have been expressed as `my_function()`. Also, if we declare all functions to be pure it doesn't matter exactly _when_ they are evaluated. So let's give the compiler the authority to make an informed guess about _when_ and make `my_var` and `my_function()` semantically equivalent. [^1]: Let's say that conventional languages are the first 15 from this list: ## Syntax, surly shinier First of all, let's change the function call to this: ```prql (my_function arg1 arg2 arg3) ``` It may look strange because the function name is within the parenthesis and there are no commas between arguments. But there are benefits to this syntax. Firstly, when the call has no arguments, the parenthesis can be omitted: ```prql (my_function) == my_function ``` which feels very natural because of the similar behavior with expressions. As intended, a function call with no argument and a plain reference to the function are expressed with the same syntax. Let's extend this behavior and allow bare function calls in a few places where they won't become ambiguous: ```prql # a list: [my_function arg1 arg2 arg3, my_function arg4 arg5 arg6] # a declaration: let res = my_function arg1 arg2 arg3 ``` Secondly, I'd argue that currying looks natural. ```prql let curry = my_function arg1 arg2 let res = curry arg3 # or inline: let res = (my_function arg1 arg2) arg3 ``` Now let's go one step further and introduce a "pipe" operator. It applies its left operand as an argument to the right operand: ```prql arg3 | my_function arg1 arg2 ``` This is especially useful for chaining function calls. If we declare `div`, `floor` and `mul` as common arithmetic functions, the function call syntax starts to make sense: ```prql 12 | div 5 | floor | mul 5 ``` ## Running real relations Up to this point, we've discussed language design in the abstract, without knowing what it will be used for. Designing for the sake of "language being cool" is fine, but we do have a use for it, which means that the language must be designed to be the right tool for the job. The job, in the case of PRQL, is querying databases. The core unit of data here is a "relation": an ordered set of rows, each of which contains entries for each of the columns. (More people are likely to be familiar with a "table", which is a relation stored in a DB.) Queries operate in a static environment that contains references to database tables and a library of standard functions. The important keyword here is "static" by which I mean "global immutable variables" that can be referenced from pure functions [^2]. PRQL currently supports most of the features described in previous sections, focused on the innovative function call syntax. [^2]: Many functions in SQL are not pure, and we don't have a plan on [how to deal with that](https://github.com/PRQL/prql/issues/1111). ### Basics A basic operation on a relation would be: ```prql (take 3 albums) # or with a pipeline: (albums | take 3) ``` ... where `take` is a function and `albums` is a static relation. This does exactly what it sounds like: it limits the result to the first n rows. One could say it selects the top n rows. In actual PRQL queries, we have some rules regarding references to tables, which I'll talk about it some other time. For now, let's just say that for referencing tables (i.e. static relations) use the function `from`. It has the bonus that it makes queries look a lot more like SQL: ```prql (from albums | take 3) ``` To make querying easier, we have some neat name resolution rules that allow function arguments to refer to each other. In practice, it allows referring to columns of a relation in function calls: ```prql (select [title, artist_id] default_db.albums) # and with a pipeline: (from albums | select {title, artist_id}) ``` All these queries can be simplified to an expression of relations and scalars. In PRQL, we call such expressions "Relational Queries" or RQ for short. It is an intermediate representation of prqlc and can be translated to SQL and executed on basically any relational database. This is the gist of how to express SQL queries with a functional language. At this stage a curious reader might ask "can PRQL express any SQL query?" to which I'd say: almost. Another might be wondering "but is it less cumbersome and more consistent?" and I'd reply "very much, yes", but it depends on whom you ask. And someone might say "why functional?" to which I say "exactly the question I was waiting for!". There are a few quick benefits I want to get out of the way: - Pipelines make the flow of the query "top-to-bottom". An earlier part of a the pipeline will always be a valid query, regardless of what follows. - Functions on relations (transforms) are designed to be orthogonal. Each of them has as few effects as possible and can be used at any point in the pipeline (except for `from`). But there is more. ### Aggregate At this point, I have to introduce `aggregate`. It takes a relation and produces a single row using some aggregation function: ```prql (from albums | aggregate {n_albums = count}) ``` What happens if we don't specify the leading relation? ```prql (aggregate {n_albums = count}) ``` Because `aggregate` is missing an argument, currying kicks in, and the whole expression evaluates to a function that is still waiting for a relation. In SQL it is common to use GROUP BY when aggregating. If you think about it, it essentially separates the relation into groups using some criteria and then applies `aggregate` to each of the groups. This is exactly how PRQL expressed it: ```prql from albums | group artist_id (aggregate {n_albums = count}) ``` This is a lot for one line, so let's unveil new syntactic conveniences: a new line is a pipe operator and the top-level pipeline does not need parenthesis. I'll also add a new transform at the back, don't worry about it. ```prql from albums group artist_id ( aggregate {n_albums = count} ) filter n_albums > 3 ``` Notice how `group` operates on the whole relation it gets from `from` and that `aggregate` is passed to `group` as a function to be applied to each of the groups. `aggregate` then converts each of the groups into a single row and returns that to `group` that composes these rows back together. This can now be passed on to `filter`, which will remove any rows that don't match its condition. In SQL, a similar expression would have these four parts: - projection in SELECT, - an aggregation function that implicitly triggers aggregation, - GROUP BY clause, - HAVING clause. These parts are entangled syntactically and semantically into one feature we understand as GROUPING. The beautiful realization is that these are 3 different operations that are happening and that when separated they don't need to be associated with each other. For example `filter` is more commonly expressed as WHERE and `group` has other uses than `aggregate`. But to separate these core relational operations, we need a way to express aggregate as a function. This is why the functional paradigm fits the relational data model. ### Distinct A common question when learning SQL is "how do I select the row where column x is smallest?". It has many variations, but there are two ways of doing it: ```sql -- option 1 SELECT x, y, z FROM tab ORDER BY x LIMIT 1 -- option 2 SELECT x, y, z FROM tab WHERE x = (SELECT min(x) FROM tab) ``` [A follow-up question](https://stackoverflow.com/questions/3800551/select-first-row-in-each-group-by-group) would be "how do I select the row where column x is smallest, for each group over y?". This seems like a similar problem but the solution in SQL is surprisingly different: ```sql -- option 1 (unsupported in some dialects) SELECT DISTINCT ON (y) x, y, z FROM tab ORDER BY y, x, z; -- option 2 (supported by most dialects) WITH summary AS ( SELECT x, y, z, ROW_NUMBER() OVER(PARTITION BY y ORDER BY x) AS rank FROM tab) SELECT * FROM summary WHERE rank = 1 ``` Now break the query down into core operations. Essentially we want to do the same thing we did before, but performed in groups by `y`. Before we used SQL that can be expressed as `sort x | take 1` (which evaluates to a function), so now surely this should work: ```prql from tab group y (sort x | take 1) ``` And it does. You can go and test this out in the [PRQL playground](https://prql-lang.org/playground/). Another variation of the question would be "how do I select a row, for each group over all columns?". If you phrase it differently "group by all columns and then take one row from each group". Or another way: "select distinct values of all columns". ```prql from tab group tab.* (take 1) ``` As you can see, our language has many shortcuts for expressing operations such as DISTINCT. This is convenient for us humans, but it's not a good base for a relational query language. I hope my point is clear: relational query language benefits a lot by separating operations into orthogonal transforms[^3]. These transforms are in most cases pure functions that are easiest to express in a functional language. If you want to see more of what PRQL is capable of, come and check out [the project](https://github.com/PRQL/prql). It may not have monads (yet), but it's probably better than what you are forced to use now. [^3]: Transforms in PRQL are not completely orthogonal. `select`, `derive`, `aggregate` and `join` all manipulate relation columns. So in a sense, they are much closer to each other than they are to `take`. ## Appendix ### PRQL support PRQL is a work in progress. It does not yet support all the features presented here, namely: - `let` syntax for variable declarations, - inline currying - function call must start with a function name, - syntax for declaring tables. We focused on the core features and left these out because they can be worked around. Also, PRQL may not ever get all of these features, because the ideas in this article are only my own and not necessarily of the whole PRQL core team. ### In math, function call syntax is ambiguous If you think about it, the function call syntax from math is kind of ambiguous. For example, what does this mean: ```math a(b + 1) ``` It could either be a call of function a, or it could be just multiplication where we omitted `*`. This is not a problem in conventional programming languages because they don't allow omitting the `*`. ================================================ FILE: web/website/content/posts/2023-01-27-prql-query.md ================================================ --- title: Time tracking with pq date: 2023-01-27 authors: ["Aljaž Mur Eržen"] layout: article --- Some time ago, I needed a time-tracking app that would be simple and fast. After looking into a few heavy web applications, I settled with this one-liner: ``` # time_tracker.sh echo $(date -u +"%Y-%m-%dT%H:%M:%SZ"),$1 >> ~/time-tracking.csv ``` I've made it a bit more sophisticated, but the core functionality is the same. The the script is aliased to `tt`, so I can start or stop the timer in any open terminal by writing: ``` $ tt start $ tt stop ``` I've prefilled the resulting `~/time-tracking.csv` with a header, so it is ready to be analyzed. ``` time,action 2023-01-27T09:26:33Z,start 2023-01-27T10:12:50Z,stop 2023-01-27T12:54:04Z,start 2023-01-27T15:12:07Z,stop ``` Now, I'd want to transform this data to show the total duration for each day. For this I can use [prql-query](https://github.com/PRQL/prql-query), which is a CLI which can execute PRQL queries against database engines. At the time of writing it supports duckdb and datafusion, but we can also connect to many other engines through these two. But I don't need that today, plain duckdb will do: ``` $ pq --backend=duckdb \ --from "tt=~/time-tracking.csv" \ '{here comes the PRQL query below}' ``` ```prql # function declaration that is a wrapper for substr SQL function let substr = text start len -> s"substr({text}, {start}, {len})" # start of the pipeline from tt # as declared in --from # compute a few new columns derive [ date = substr time 0 11, # call the substr function to # extract date from column `time` prev_action = lag 1 action, # lag column `action` prev_time = lag 1 time, # lag column `time` ] # pick only rows that correspond to intervals that I want to track filter action == "stop" and prev_action == "start" # for each date group date ( # sum durations of those intervals aggregate [sec = sum s"EXTRACT(EPOCH FROM {time - prev_time})"] ) # compute more columns derive [ hours = substr f"00{sec / (60 * 60)}" 0-2 2, minutes = substr f"00{(sec / 60) % 60}" 0-2 2, seconds = substr f"00{sec % 60}" 0-2 2, ] # expose only date and pretty-printed duration select [ date, duration = f"{hours}:{minutes}:{seconds}" ] ``` When run on the file above, prql-query produces this pretty table: ``` +------------+----------+ | date | duration | +------------+----------+ | 2023-01-27 | 03:04:20 | +------------+----------+ ``` The full script implementation adds some conveniences like error handling and better output formatting, but the core concept remains: simple CSV logging that can be queried with PRQL. ================================================ FILE: web/website/content/posts/2023-01-28-format-pretty-reports/_index.md ================================================ --- title: Format pretty reports date: 2023-01-28 authors: ["richb-hanover"] layout: article toc: false --- > I can no longer bring myself to write bare SQL - PRQL makes building queries > so easy. So here's how I use PRQL _functions_ and _aliases_ for naming > variables to "pretty up" a SQL report. I have an SQLite database with a table named `PropertyData` and I want to show the change of column `App_Total2020` and `App_Total2021` across years, displaying their values along with their percent change. ## Functions Of course, property values are in dollars. So I could simply display them as `450000`. But they are more compelling if they're written as `$450,000`. Writing the SQL code and format strings for each column would be tedious and error-prone. PRQL allows me to create a `dollars` function and then use it multiple times: ```prql # dollars displays a numeric value as whole dollars with commas let dollars = d -> s"""printf("$%,d",{d})""" select { (dollars App_Total2020), (dollars App_Total2021), } ``` I also want to compute the percent change between values. It's easy to create a `percent_diff` function: ```prql # percent_diff computes the amount (percent) the new differs from old let percent_diff = old new -> 100.0*( new - old ) / old ``` One final function: the `percent_diff` function returns a floating point number with many digits after the decimal place. I only want to display one place in my results, with a trailing `%`. So I wrote a `format_percent` function that uses a `printf()` to format the value. ```prql # format_percent prints a floating point number with "%" let format_percent = v -> s'printf("%1.1f%", {v})' ``` ## Column Headings Use a PRQL _alias_ to assign each column a nice name. This becomes its column heading. The examples above might be: ```prql select { Appraisal2020 = (dollars App_Total2020), `Appraisal 2021` = (dollars App_Total2021), } ``` Note how the second example puts the column heading in backticks to preserve spaces. ## Excluding certain columns I want to sort results by the (numeric) percent change, but I don't want to display that percentage value (with multiple decimal places) in the final table. So I split the query into pieces: the first `select` collects all the necessary columns, adding a new column using `percent_diff`. The query then sorts the values and passes those results to a second `select` that's responsible for formatting the column headings and contents (using aliases and `format_percent`). ## Putting it all together Here is my workflow for a typical query using the tools listed below: [^1] - Enter the PRQL query in the VS Code editor. Use a `.prql` suffix for the file. - Open the PRQL VS Code extension (Ctl-Shift-P, or Cmd-Shift-P on Mac). It'll appear and display the compiled SQL in a second pane on the right. - Copy the SQL from the right pane and paste it into the DBM program. Run the query. That's it! Here's the PRQL for this example, followed by the result from my database. _Note:_ Paste the PRQL query below into the [Playground](https://prql-lang.org/playground/) to see what the PRQL compiler produces. ```prql # dollars displays a numeric value as dollars with commas let dollars = d -> s"""printf('$%,d',{d})""" # percent_diff computes the amount (percent) the new differs from old let percent_diff = old new -> 100.0*( new - old ) / old # format_percent prints a floating point number with "%" let format_percent = v -> s"printf('%1.1f%', {v})" # Step 1: First calculate important columns from PropertyData select { Map, Lot, App_Total2020, App_Total2021, pct_change = (percent_diff App_Total2020 App_Total2021), } # Step 2: Sort the resulting table by pct_change sort {-pct_change} # Step 3: Format the column headings and contents select { Map, Lot, Appraisal2020 = (dollars App_Total2020), `Appraisal 2021` = (dollars App_Total2021), `Percent Change` = (format_percent pct_change), } take 20 ``` The image below shows the result of that query: - the column headings match the _aliases_ of the second `select` statement, - the `dollars` function formats values with `$` and `,` as expected, - the `percent_diff` function computes the percent change between the _old_ and _new_ values, - the `format_percent` function formats the value with a single decimal place and appends a `%`. ![First rows](./query_result.png) [^1]: **My Tools:** I use [Visual Studio Code](https://code.visualstudio.com/) to maintain a folder of PRQL queries for re-use. I use the [PRQL VS Code extension](https://marketplace.visualstudio.com/items?itemName=PRQL-lang.prql-vscode) to compile PRQL into SQL. My data is in a SQLite database, and I use [DB Browser for SQLite](https://sqlitebrowser.org/) to run queries. ================================================ FILE: web/website/content/posts/2023-02-02-one-year/_index.md ================================================ --- title: One year of PRQL date: 2023-02-02 layout: article toc: true --- A year ago, we posted [a proposal for a modern SQL replacement](https://news.ycombinator.com/item?id=30060784) to Hacker News. It immediately sparked the interest of many people dealing with data. The project grew a community of people who are now developing the language, tooling, and the idea of a modern relational language. Since then we've opened 1577 issues & PRs on [our main repo](https://github.com/PRQL/prql), submitted 4211 comments, and made 1176 commits. The number of stars is skyrocketing every time the project appears on Hacker News, which we believe to be an indicator that people are eager to adopt the language if the tooling is made accessible enough. ![](FQ9QSOo.png) ## Where are we? Language design & development in the last year have been focused on these areas: - design of basic [transforms](https://prql-lang.org/book/reference/stdlib/transforms/) and their [interactions](https://github.com/PRQL/prql/issues/300), - fundamentals of how [functions](https://github.com/PRQL/prql/issues/444) and pipelines are [evaluated](#define-functional-semantics). - small quality-of-life language features (e.g. syntax for [f-strings, dates, coalesce operator](https://prql-lang.org/book/reference/syntax/), [case](https://github.com/PRQL/prql/issues/504)), PRQL is now in a state where it can greatly improve the developer experience for writing complex analytical queries, but it does require a bit of fiddling to set up in your environment. In the coming year, we are aiming to improve that by providing a dbt plugin and integrations for tools like Rill-developer, Metabase, and DataGrip. Read more about our plans and ambitions in [the roadmap](https://prql-lang.org/roadmap/). ## How are people using it? For data analytics at [SuperSimple](https://gosupersimple.com/): > We've been using PRQL under the hood to power complex analytics workflows at > Supersimple for more than 6 months now. The speed of iteration and response to > user feedback has been amazing during this period! > > PRQL is what I think SQL should have been like from day 1 and adopting it has > likely literally saved us months. For making [pretty reports with SQLite](https://prql-lang.org/posts/2023-01-28-format-pretty-reports/). For quick, readable [scripts in the command line](https://prql-lang.org/posts/2023-01-27-prql-query/). ## What have you missed? In the past year, the community has created many things, some of which have not been noticed as much as they should have been. In the summary of the year, we want to fix that and put a spotlight on the amazing work that was done. ### Playground We have a [Playground](https://prql-lang.org/playground/) that can compile and execute PRQL queries in-browser. It's using prqlc and DuckDB, both compiled to WASM modules. ![PRQL Playground](URpCf29.png) Currently, this is the best way to see how the relation is manipulated instantly as you type the query. ### VS Code extension Extension that provides syntax highlighting and compiled SQL within your editor. ![VS Code extension](7cpDySb.png) ### prql-query CLI tool that uses DuckDB and DataFusion to execute PRQL queries from your terminal. Useful for wrangling your CSV and parquet files on the go. ![pq](ncVXken.png) ### Define functional semantics Given the initial proposal of the language, we constructed consistent semantics of how functions in PRQL work, what they can express and how can they be abused. To keep this post brief, we'll expose a single snippet of what's possible and invite you to read more in [a recent post](https://prql-lang.org/functional-relations/). ```prql let take_oldest = n rel -> ( rel sort [-invoice_date] take n ) from invoices take_oldest 3 ``` > Function `take_oldest` is used in the main pipeline, just any other regular > transform. ### Relational Query The design of prqlc strives to have a complexity bottleneck with an intermediate representation named [Relation Query](https://docs.rs/prqlc/latest/prqlc/ir/rq/index.html) or RQ for short. Think of it as equivalent to a [Substrait plan](https://substrait.io/). Its goal is the ability to express any operation possible in SQL while containing as few constructs as possible. This makes it easy to implement backends that compile RQ to SQL or any other language or library dealing with relations or dataframes. ![](GXLvoXn.png) > Note how prqlc inferred the structure of the table we are selecting from. It > knows that it must contain columns `billing_city` and `total`, but also notes > that there may be many other columns. ================================================ FILE: web/website/content/posts/2023-03-14-pi-day.md ================================================ --- title: Calculate the digits of Pi with DuckDB and PRQL date: 2023-03-14 authors: ["Tobias Brandt"] layout: article toc: true --- _TL;DR: PRQL recently added a `loop` construct which makes it Turing Complete and allows doing cool things like calculating Pi right in your database._ ## Background Last week saw the 0.6 release of [PRQL](https://prql-lang.org) which brought with it the capability to express Recursive CTEs in PRQL. "Recursive" CTEs aren't actually truly recursive in the sense that that term is usually used, rather they use a "recursive" (i.e. self-referential) syntax to provide a looping construct in SQL. [PRQL](https://prql-lang.org) is a modern, functional query language for transforming data. One of its goals is to simplify working with data wherever you can currently use SQL. As such it compiles to SQL while making available modern ergonomics such as [f-strings](https://prql-lang.org/book/reference/syntax/f-strings.html), as well as not so modern features such as [functions](https://prql-lang.org/book/reference/declarations/functions.html). Given that the underlying semantics of Recursive CTEs are really about iteration or "looping", we have called this feature `loop` in PRQL. ## Introducing `loop` Recursive CTEs in SQL consist of two parts, an `initial_query` and an `update_query`. First the `initial_query` is executed and then the rows produced are fed to `update_query` which is applied to the result set. The `update_query` is then iteratively applied to the rows produced in the last iteration until no more rows are produced, at which point iteration stops. (For a great review of this as well as some interesting proposals to extend the semantics of Recursive CTEs in SQL, see the paper ["A Fix for the Fixation on Fixpoints" by Denis Hirn and Torsten Grust](https://www.cidrdb.org/cidr2023/papers/p14-hirn.pdf).) This behavior can be expressed with following pseudo-code: ```python def loop(step, initial_query): result = [] current = initial_query() while current is not empty: result = append(result, current) current = update_query(current) return result ``` The minimal `loop` example from the documentation in the [PRQL book](https://prql-lang.org/book/reference/stdlib/transforms/loop.html) is: ```prql from_text format:json '[{"n": 1 }]' loop ( select n = n+1 filter n<=3 ) ``` Here we use a PRQL utility function `from_text` to conveniently turn a JSON representation of some example data into a SQL table (`from_text` currently also accepts CSV input). Initially a row with `{"n":1}` is fed in. Then the update query is applied to this which in this case just increments `n` by one and filters the result set to only the rows where `n` is less than or equal to three. This produces `{"n":2}` in the next step and `{"n":3}` after that. On the next iteration `{"n":4}` is produced but that is eliminated by the filter condition. Since that leaves no new rows, iteration stops. If you try the query above in the [PRQL Playground](https://prql-lang.org/playground), the result set you get (in the "output.arrow" tab) is: | n | | --- | | 1 | | 2 | | 3 | ## Fibonacci Numbers Inspired by this, let's try to calculate Fibonacci numbers which are often one of the first examples when recursion is introduced: ```elm= from_text format:json '[{"a":1, "b":1}]' loop ( derive b_new = a + b select [a=b, b=b_new] ) take 7 ``` which produces the first 7 Fibonacci numbers. | a | | --- | | 1 | | 1 | | 2 | | 3 | | 5 | | 8 | | 13 | You might have noticed that we didn't actually include a `filter` in our loop this time. Instead we relied on the fact that DuckDB produces results lazily and since we only took 7 numbers it only produced what we needed. Now, let's set ourselves a bigger challenge. ## Calculating the digits of pi March 14th is written on American calendars as 3/14 which reminds us of the first three digits of Pi=3.1415926535... . Therefore this day is commonly known as Pi day. In order to celebrate Pi Day 2023 and the recent release of `loop` in PRQL 0.6, why don't we try to calculate the digits of Pi in PRQL! For our query engine we will use [DuckDB](https://duckdb.org/) because it is really fast and has many modern features that make it ideally suited for the kinds of analytical queries that PRQL is targeting. There is even a [duckdb-prql](https://github.com/ywelsch/duckdb-prql) extension which allows you to write PRQL queries right inside DuckDB! We follow the algorithms in the paper ["Unbounded Spigot Algorithms for the Digits of Pi" by Jeremy Gibbons](https://www.cs.ox.ac.uk/people/jeremy.gibbons/publications/spigot.pdf), in particular Rabinowitz and Wagon's Spigot Algorithm. While it is not the most efficient algorithm presented in the paper, it has the advantage that it uses only standard data types while the more efficient algorithms rely on unbounded integer types which are not available in many database systems. For my implementation, I adapted the Python implementation by John Burkardt found [here](https://people.sc.fsu.edu/~jburkardt/py_src/pi_spigot/pi_spigot.py). A bit of care had to be taken in porting the implementation to PRQL, as PRQL like SQL, is stateless so I introduced another state variable `k` to track the position inside the body of the loop. In order to ensure that all values of `k` are handled in the query we define a PRQL function called `loop_steps`. The ability to define functions in PRQL is one of its key features and I feel the query below really shows the power that the composability of functions brings to PRQL. PRQL is a functional language with features such as currying. For more details see our previous post [A functional approach to relational queries](https://prql-lang.org/functional-relations/). The resulting PRQL query is the following: ```prql prql target:sql.duckdb let config = ( from_text format:json '[{"num_digits":50}]' derive { array_len = (10*num_digits)/3, calc_len = 1+4, loop_len = array_len + calc_len, } ) let loop_steps = step_0 step_i step_1 step_2 step_3 other -> case [ k==0 => step_0, 1 <= k and k <= array_len => step_i, k==array_len+1 => step_1, k==array_len+2 => step_2, k==array_len+3 => step_3, true => other, ] let q_steps = step_q9 step_q10 step_j2 step_jg2 other -> case [ q==9 => step_q9, q==10 => step_q10, j==2 => step_j2, j>2 => step_jg2, true => other, ] from config select [ num_digits, array_len, loop_len, j = 0, k = 0, q = 0, a = s"[2 for i in range({array_len})]", nines = 0, predigit = 0, output = '', ] loop ( filter j < num_digits + 1 derive [ j_new = case [k==0 => j+1, true => j], k_new = (k+1) % loop_len, q_step_i = (10*s"{a}[{k}]"+q*(array_len-k+1))/(2*(array_len-k)+1), q_new = loop_steps 0 q_step_i (q/10) q q q, a_step_i = s"[CASE WHEN i=={k} THEN (10*{a}[i]+{q}*({array_len}-i+1))%(2*({array_len}-i)+1) ELSE {a}[i] END for i in generate_series(1,{array_len})]", a_step_1 = s"[CASE WHEN i=={array_len} THEN {q}%10 ELSE {a}[i] END for i in generate_series(1,{array_len})]", a_new = loop_steps a a_step_i a_step_1 a a a, nines_new = loop_steps nines nines nines (q_steps (nines+1) 0 nines nines nines) (case [q!=9 and q!=10 and nines!=0 => 0, true => nines]) nines, predigit_new = loop_steps predigit predigit predigit (q_steps predigit 0 q q q) predigit predigit, output_step_2 = (q_steps '' s"({predigit}+1)::string || repeat('0', {nines})" s"{predigit}::string || '.'" s"{predigit}::string" ''), output_step_3 = (case [q!=9 and q!=10 and nines!=0 => s"repeat('9', {nines})", true => '']), output_new = loop_steps '' '' '' output_step_2 output_step_3 '', ] select { num_digits, array_len, loop_len, j = j_new, k = k_new, q = q_new, a = a_new, nines = nines_new, predigit = predigit_new, output = output_new, } ) aggregate [pi=s"string_agg({output}, '')"] ``` Go ahead and run this query right now in your browser with our [Online PRQL Playground](https://www.prql-lang.org/playground)! There you can also see the SQL that is produced in the `output.sql` tab (for you convenience reproduced in the Appendix below) as well as the result of the calculation in the `output.arrow` tab. The output you see will be something like the following: ``` ┌────────────────────────────────────────────────────┐ │ pi │ │ varchar │ ├────────────────────────────────────────────────────┤ │ 3.141592653589793238462643383279502884197169399375 │ └────────────────────────────────────────────────────┘ ``` Why don't you play around with the `num_digits` parameter to try and see how many digits you can get on your laptop? (Unfortunately this algorithm is quite a bit more inefficient than the equivalent Python implementation as Recursive CTEs currently don't allow for state to be kept outside of the result set.) Something you might have noticed is that there are some expressions surrounded by `s""`. This is called an [s-string](https://prql-lang.org/book/reference/syntax/s-strings.html) (s for SQL) and allows us to include raw SQL inside our PRQL queries. We use this to include features from DuckDB that haven't made it into PRQL yet, such as the Pythonesque [list comprehensions](https://duckdb.org/docs/sql/functions/nested.html#list-comprehension). The `loop` functionality in PRQL is brand new and is marked as _experimental_ and we will be working to stabilise the feature and iterate on the design to make it as easy and useful as possible. We felt that it was exciting enough to share it at this stage (and in time for Pi Day) and make it available for you to try out and play around with. PRQL is developed completely in the open so let us know your use cases so that we can make PRQL the best tool for the data challenges that you face. ## Conclusion Recursive CTEs in SQL make SQL Turing Complete and the same should hold for PRQL now that `loop` is included. As we saw above, not all algorithms are easily expressed in this paradigm, so while theoretically they could be, whether they should be is another question. However I hope this example demonstrates that `loop` brings great power to PRQL and in follow up posts I will demonstrate how we can use this to do tree and graph traversals which do come up in practice in the kind of analytical data work that PRQL is made for. After that I will also look at online algorithms such as moving averages an online gradient descent, so be sure to come back for those! ## Appendix The following is the SQL query that was produced and run in DuckDB: ```sql WITH table_0 AS ( SELECT 50 AS num_digits ), config AS ( SELECT num_digits, 10 * num_digits / 3 AS array_len, 5 AS calc_len, 10 * num_digits / 3 + 5 AS loop_len FROM table_0 AS table_1 ), table_6 AS ( WITH RECURSIVE loop AS ( SELECT num_digits, array_len, loop_len, 0 AS _expr_0, 0 AS _expr_1, 0 AS _expr_2, [2 for i in range(array_len)] AS _expr_3, 0 AS _expr_4, 0 AS _expr_5, '' AS _expr_6 FROM config UNION ALL SELECT num_digits, array_len, loop_len, _expr_12 AS _expr_15, _expr_11 AS _expr_16, _expr_10 AS _expr_17, _expr_9 AS _expr_18, _expr_8 AS _expr_19, _expr_7 AS _expr_20, CASE WHEN _expr_1 = 0 THEN '' WHEN 1 <= _expr_1 AND _expr_1 <= array_len THEN '' WHEN _expr_1 = array_len + 1 THEN '' WHEN _expr_1 = array_len + 2 THEN _expr_13 WHEN _expr_1 = array_len + 3 THEN _expr_14 ELSE '' END FROM ( SELECT num_digits, array_len, loop_len, CASE WHEN _expr_1 = 0 THEN _expr_5 WHEN 1 <= _expr_1 AND _expr_1 <= array_len THEN _expr_5 WHEN _expr_1 = array_len + 1 THEN _expr_5 WHEN _expr_1 = array_len + 2 THEN CASE WHEN _expr_2 = 9 THEN _expr_5 WHEN _expr_2 = 10 THEN 0 WHEN _expr_0 = 2 THEN _expr_2 WHEN _expr_0 > 2 THEN _expr_2 ELSE _expr_2 END WHEN _expr_1 = array_len + 3 THEN _expr_5 ELSE _expr_5 END AS _expr_7, CASE WHEN _expr_1 = 0 THEN _expr_4 WHEN 1 <= _expr_1 AND _expr_1 <= array_len THEN _expr_4 WHEN _expr_1 = array_len + 1 THEN _expr_4 WHEN _expr_1 = array_len + 2 THEN CASE WHEN _expr_2 = 9 THEN _expr_4 + 1 WHEN _expr_2 = 10 THEN 0 WHEN _expr_0 = 2 THEN _expr_4 WHEN _expr_0 > 2 THEN _expr_4 ELSE _expr_4 END WHEN _expr_1 = array_len + 3 THEN CASE WHEN _expr_2 <> 9 AND _expr_2 <> 10 AND _expr_4 <> 0 THEN 0 ELSE _expr_4 END ELSE _expr_4 END AS _expr_8, CASE WHEN _expr_1 = 0 THEN _expr_3 WHEN 1 <= _expr_1 AND _expr_1 <= array_len THEN [CASE WHEN i==_expr_1 THEN (10*_expr_3[i] + _expr_2 *(array_len - i + 1) ) %(2 *(array_len - i) + 1) ELSE _expr_3 [i] END for i in generate_series(1, array_len) ] WHEN _expr_1 = array_len + 1 THEN [CASE WHEN i==array_len THEN _expr_2%10 ELSE _expr_3[i] END for i in generate_series(1, array_len) ] WHEN _expr_1 = array_len + 2 THEN _expr_3 WHEN _expr_1 = array_len + 3 THEN _expr_3 ELSE _expr_3 END AS _expr_9, CASE WHEN _expr_1 = 0 THEN 0 WHEN 1 <= _expr_1 AND _expr_1 <= array_len THEN ( 10 * _expr_3 [_expr_1] + _expr_2 * (array_len - _expr_1 + 1) ) / (2 * (array_len - _expr_1) + 1) WHEN _expr_1 = array_len + 1 THEN _expr_2 / 10 WHEN _expr_1 = array_len + 2 THEN _expr_2 WHEN _expr_1 = array_len + 3 THEN _expr_2 ELSE _expr_2 END AS _expr_10, (_expr_1 + 1) % loop_len AS _expr_11, CASE WHEN _expr_1 = 0 THEN _expr_0 + 1 ELSE _expr_0 END AS _expr_12, _expr_1, CASE WHEN _expr_2 = 9 THEN '' WHEN _expr_2 = 10 THEN (_expr_5 + 1) :: string || repeat('0', _expr_4) WHEN _expr_0 = 2 THEN _expr_5 :: string || '.' WHEN _expr_0 > 2 THEN _expr_5 :: string ELSE '' END AS _expr_13, CASE WHEN _expr_2 <> 9 AND _expr_2 <> 10 AND _expr_4 <> 0 THEN repeat('9', _expr_4) ELSE '' END AS _expr_14, _expr_2, _expr_4 FROM loop AS table_2 WHERE _expr_0 < num_digits + 1 ) AS table_3 ) SELECT * FROM loop ) SELECT string_agg(_expr_6, '') AS pi FROM table_6 AS table_5 -- Generated by PRQL compiler version:0.6.1 (https://prql-lang.org) ``` ================================================ FILE: web/website/content/roadmap.md ================================================ --- title: "Roadmap" url: roadmap --- > We're excited and inspired by the level of enthusiasm behind the project, both > from individual contributors and the broader community of users who are > unsatisfied with SQL. We currently have an working version for the intrepid > users. > > We're hoping we can build a beautiful language, integrations that are > approachable & powerful, and a vibrant community. Many projects have reached > the current stage and fallen, so this requires compounding on what we've done > so far. > > -- {{< cite >}}PRQL Developers{{< /cite >}} ## Medium term {{< columns >}} #### Integrations PRQL is focused at the language layer, which means we can easily integrate with existing tools & apps. Integrations will be the primary way that people can start using PRQL day-to-day. At first, the most impactful initial integrations will be tools that engineers use to build data pipelines, like [`dbt-prql`](https://github.com/PRQL/prql/issues/375). #### Standard library Currently, the standard library is [quite limited](https://github.com/PRQL/prql/blob/main/prqlc/prqlc/src/semantic/std.prql). It contains only basic arithmetic functions (`AVERAGE`, `SUM`) and lacks functions for string manipulation, date handling and many math functions. We're looking to gradually introduce these as needed, and reduce the need for s-strings. One challenge here is the variety of functionalities and syntax of target DBMSs; e.g. there's no standard regex function. #### Type system Because PRQL is meant to be the querying interface of the database, a type system that can describe database schema as well as all intermediate results of the queries is needed. We want it to provide clear distinctions between different nullable and non-nullable values, and different kinds of containers (e.g. scalars vs. columns). Currently PRQL compiles into SQL with no understanding of the underlying tables. We plan to introduce database schema declarations into the language, so PRQL compiler and tooling can enrich the developer experience with autocomplete and early error messages. The goal here is to catch all errors at PRQL compile time, instead of at the database's PREPARE stage. <---> #### Friendliness Currently the compiler output's friendliness is variable — sometimes it produces much better error messages than SQL, but sometimes they can be confusing. Both bug reports of unfriendliness, and code contributions to improve them are welcome; there's a [friendliness label.](https://github.com/PRQL/prql/issues?q=is%3Aissue+label%3Afriendliness+is%3Aopen) #### Developer ergonomics — LSP The PRQL language can offer a vastly improved developer experience over SQL, both when exploring data and building robust data pipelines. We'd like to offer autocomplete both for PRQL itself and for columns of the underlying database, because fast iteration cycle can drastically decrease frustrations caused by banal misspellings. This requires development across multiple dimensions — writing an [LSP server](https://langserver.org/), better support for typing in the compiler, and possibly database cohesion. While PRQL compiler will never depend on a database to compile queries, an LSP server could greatly help with generating type definitions from the information schema of a database. #### Query transparency PRQL's compiler already contains structured data about the query. We'd like to offer transparency to tools which use PRQL, so they can offer lineage information, such as which tables are queried, and a DAG of transformations for each column. {{< /columns >}} ## Long term {{< columns >}} #### SQL-to-PRQL conversion While PRQL already allows for a gradual on-ramp — there's no need to switch everything to PRQL right away — it would also be useful to be able to convert existing SQL queries to PRQL, rather than having to rewrite them manually. For many queries, this should be fairly easy. (For some it will be very difficult, but we can start with the easy ones...) #### Language While the core semantics and syntax of the language are now fairly stable, we are planning [a few major features](https://github.com/PRQL/prql/issues?q=is%3Aopen+is%3Aissue+label%3Amajor-feature+label%3Alanguage-design) that will give PRQL the feeling of a real programming language and elevate it in [the chomsky hierarchy](https://en.wikipedia.org/wiki/Chomsky_hierarchy). Honorable mentions here are recursive CTEs (or rather functions), algebraic type system, pre-specified join conditions and regex. Note that these features will probably inflict breaking changes with each minor release before we stabilize the 1.0, the first indefinitely supported language edition. <---> #### Alternative backends Currently, PRQL only transpiles into SQL, using connectors such as DuckDB to access other formats, such as Pandas dataframes. But PRQL can be much more general than SQL — we could directly compile to any relational backend, offering more flexibility and performance — and a consistent experience for those who use multiple tools. For example, we could compile PRQL to RQ (Relational Query intermediate representation) and then use that to apply the transformations to an in-memory dataframe of a performance-optimized library (such as [Polars](https://www.pola.rs/)) or a Google Sheets spreadsheet. Alternatively, we could even convert RQ to [Substrait](https://substrait.io/). ### PRQL IDE We'd like to make it easier to try PRQL. We currently have the playground, which compiles PRQL and runs queries with a DuckDB wasm module, but there's much more we could do. Could we support for importing arbitrary CSV and parquet input files and then exporting the results? Could it integrate an LSP? We can balance this against building integrations with existing tools. {{< /columns >}} ## Not in focus We should focus on solving a distinct problem really well. PRQL's goal is to make reading and writing analytical queries easier, and so for the moment that means putting some things out of scope: - Building infrastructure outside of queries, like lineage. dbt is excellent at that! ([#13](https://github.com/PRQL/prql/issues/13)). - Writing DDL / index / schema manipulation / inserting data ([#16](https://github.com/PRQL/prql/issues/16)). ================================================ FILE: web/website/data/examples/basic.yaml ================================================ label: Basic example prql: | from employees select {id, first_name, age} sort age take 10 sql: | SELECT id, first_name, age FROM employees ORDER BY age LIMIT 10 ================================================ FILE: web/website/data/examples/dialects.yaml ================================================ label: Dialects prql: | prql target:sql.mssql # Will generate TOP rather than LIMIT from employees sort age take 10 sql: | SELECT * FROM employees ORDER BY age OFFSET 0 ROWS FETCH FIRST 10 ROWS ONLY ================================================ FILE: web/website/data/examples/expressions.yaml ================================================ label: Expressions prql: | from track_plays derive { finished = started - unfinished, fin_share = finished / started, # Use previous definitions fin_ratio = fin_share / (1-fin_share), # BTW, hanging commas are optional! } sql: | SELECT *, started - unfinished AS finished, (started - unfinished) / started AS fin_share, (started - unfinished) / started / (1 - (started - unfinished) / started) AS fin_ratio FROM track_plays ================================================ FILE: web/website/data/examples/f-strings.yaml ================================================ label: F-strings prql: | from web # Just like Python select url = f"https://www.{domain}.{tld}/{page}" sql: | SELECT CONCAT('https://www.', domain, '.', tld, '/', page) AS url FROM web ================================================ FILE: web/website/data/examples/friendly-syntax.yaml ================================================ label: Friendly syntax prql: | from track_plays filter plays > 10_000 # Readable numbers filter (length | in 60..240) # Ranges with `..` filter recorded > @2008-01-01 # Simple date literals filter released - recorded < 180days # Nice interval literals sort {-length} # Concise order direction sql: | SELECT * FROM track_plays WHERE plays > 10000 AND length BETWEEN 60 AND 240 AND recorded > DATE '2008-01-01' AND released - recorded < INTERVAL 180 DAY ORDER BY length DESC ================================================ FILE: web/website/data/examples/functions.yaml ================================================ label: Functions prql: | let celsius_to_fahrenheit = temp -> temp * 9/5 + 32 from weather select temp_f = (celsius_to_fahrenheit temp_c) sql: | SELECT temp_c * 9 / 5 + 32 AS temp_f FROM weather ================================================ FILE: web/website/data/examples/hero.yaml ================================================ prql: | from invoices filter invoice_date >= @1970-01-16 derive { transaction_fees = 0.8, income = total - transaction_fees } filter income > 1 group customer_id ( aggregate { average total, sum_income = sum income, ct = count total, } ) sort {-sum_income} take 10 join c=customers (==customer_id) derive name = f"{c.last_name}, {c.first_name}" select { c.customer_id, name, sum_income } derive db_version = s"version()" ================================================ FILE: web/website/data/examples/joins.yaml ================================================ label: Joins prql: | from employees join b=benefits (==employee_id) join side:left p=positions (p.id==employees.employee_id) select {employees.employee_id, p.role, b.vision_coverage} sql: | SELECT employees.employee_id, p.role, b.vision_coverage FROM employees INNER JOIN benefits AS b ON employees.employee_id = b.employee_id LEFT OUTER JOIN positions AS p ON p.id = employees.employee_id ================================================ FILE: web/website/data/examples/null-handling.yaml ================================================ label: Null handling prql: | from users filter last_login != null filter deleted_at == null derive channel = channel ?? "unknown" sql: | SELECT *, COALESCE(channel, 'unknown') AS channel FROM users WHERE last_login IS NOT NULL AND deleted_at IS NULL ================================================ FILE: web/website/data/examples/orthogonal.yaml ================================================ label: Orthogonality prql: | from employees # `filter` before aggregations... filter start_date > @2021-01-01 group country ( aggregate {max_salary = max salary} ) # ...and `filter` after aggregations! filter max_salary > 100_000 sql: | SELECT country, MAX(salary) AS max_salary FROM employees WHERE start_date > DATE '2021-01-01' GROUP BY country HAVING MAX(salary) > 100000 ================================================ FILE: web/website/data/examples/s-strings.yaml ================================================ label: S-strings prql: | # There's no `version` in PRQL, but s-strings # let us embed SQL as an escape hatch: from x derive db_version = s"version()" sql: | SELECT *, version() AS db_version FROM x ================================================ FILE: web/website/data/examples/top-n.yaml ================================================ label: Top N by group prql: | # Most recent employee in each role # Quite difficult in SQL... from employees group role ( sort join_date take 1 ) sql: | WITH table_0 AS ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY role ORDER BY join_date ) AS _expr_0 FROM employees ) SELECT * FROM table_0 WHERE _expr_0 <= 1 ================================================ FILE: web/website/data/examples/windows.yaml ================================================ label: Windows prql: | from employees group employee_id ( sort month window rolling:12 ( derive {trail_12_m_comp = sum paycheck} ) ) sql: | SELECT *, SUM(paycheck) OVER ( PARTITION BY employee_id ORDER BY month ROWS BETWEEN 11 PRECEDING AND CURRENT ROW ) AS trail_12_m_comp FROM employees ================================================ FILE: web/website/data/testimonials.yaml ================================================ # Tweets can be fetched with https://tweetic.zernonia.com # # (though FYI I (@max-sixty) couldn't get this to work and had to fill out # manually, using https://commentpicker.com/twitter-id.php to get the ID, and # manually inspecting the Twitter profile page to get the `pbs.twimg` link...) - quote: text: It starts with FROM, it fixes trailing commas, and it's called PRQL?? If this is a dream, don't wake me up. author: Jeremiah Lowin, Founder & CEO, Prefect. - tweet: user_id: "19042640" name: "Hamilton Ulmer" screen_name: "hamiltonulmer" profile_image_url_https: "https://pbs.twimg.com/profile_images/1201721914814656512/B6muDm76_normal.jpg" url: "https://twitter.com/hamiltonulmer/status/1522562664467107840" profile_url: "https://twitter.com/hamiltonulmer" created_at: "2022-05-06T13:03:21.000Z" favorite_count: 2 conversation_count: 0 text: very excited for prql! - tweet: user_id: "12963432" name: Armin Ronacher screen_name: mitsuhiko profile_image_url_https: "https://pbs.twimg.com/profile_images/1433982028/profile_normal.png" url: https://twitter.com/mitsuhiko/status/1683941196799045632?s=20 profile_url: https://twitter.com/mitsuhiko created_at: "2022-07-25T13:03:21.000Z" favorite_count: 49 conversation_count: 4 text: "Oh wow I missed this. Clickhouse now supports PRQL: https://github.com/ClickHouse/ClickHouse/pull/50686" - quote: text: I'm also really excited about efforts to create entire new query languages that compile to SQL, like Malloy and PRQL. author: Wes McKinney, Creator of Pandas link: https://wesmckinney.com/blog/looking-back-15-years/ - tweet: user_id: "16080017" name: "Swanand." screen_name: "_swanand" profile_image_url_https: "https://pbs.twimg.com/profile_images/1607146722555224064/iTL4gp7m_normal.jpg" url: "https://twitter.com/_swanand/status/1485965394880131081" profile_url: "https://twitter.com/_swanand" created_at: "2022-01-25T13:18:52.000Z" favorite_count: 20 conversation_count: 2 text: > A few years ago, I started working on a language, called "dsql", short for declarative SQL, and a pun on "the sequel (to SQL)". I kinda chickened out of it then, the amount of study and research I needed was massive. prql here is better than I imagined: github.com/max-sixty/prql - quote: text: Column aliases would have saved me hundreds of hours over the course of my career. author: "@dvasdekis" link: https://news.ycombinator.com/item?id=30064873 - tweet: user_id: "231773031" name: "Rishabh Software" screen_name: "RishabhSoft" profile_image_url_https: "https://pbs.twimg.com/profile_images/551974110566178817/JHuUzhjU_normal.png" url: "https://twitter.com/RishabhSoft/status/1514280454890872833" profile_url: "https://twitter.com/RishabhSoft" created_at: "2022-04-13T16:32:49.000Z" favorite_count: 0 conversation_count: 0 text: > SQL's hold on data retrieval is slipping! 8 new databases are emerging, and some speak entirely new languages for data querying. Know more infoworld.com/article/365490… #SQL #DataQuery #GraphQL #PRQL #WebAssembly - tweet: user_id: "40653789" name: "Burak Emir" screen_name: "burakemir" profile_image_url_https: "https://pbs.twimg.com/profile_images/215834651/BurakEmir-2007-04-23-full_normal.jpg" url: "https://twitter.com/burakemir/status/1485958835844100098" profile_url: "https://twitter.com/burakemir" created_at: "2022-01-25T12:52:48.000Z" favorite_count: 2 conversation_count: 1 text: > I want to give the PRQL a little boost here, "pipeline of transformations" is IMHO a good choice for readable query languages that need to deal with SQL-like aggregations, group by and count and sum all: github.com/max-sixty/prql - quote: text: > Having written some complex dbt projects...the first thing...it gets right is to start with the table and work down. This is an enormous readability boost in large projects and leads to great intellisense. author: Ruben Slabbert link: https://lobste.rs/s/oavgcx/prql_simpler_more_powerful_sql#c_nmzcd7 ================================================ FILE: web/website/static/CNAME ================================================ prql-lang.org ================================================ FILE: web/website/themes/prql-theme/archetypes/default.md ================================================ --- title: "{{ replace .Name "-" " " | title }}" date: {{ .Date }} draft: true --- ================================================ FILE: web/website/themes/prql-theme/layouts/404.html ================================================ {{ define "main" }}

Not found

This page does not exist.

{{ end }} ================================================ FILE: web/website/themes/prql-theme/layouts/_default/_markup/render-link.html ================================================ {{/* Formats external links with an icon, and opens in a new tab. Note that it's important not to have a blank line at the end; so we exclude this file from prettier / pre-commit's fixer */}} {{ $is_external := strings.HasPrefix .Destination "http" }} {{ .Text | safeHTML }} {{- if $is_external -}} {{- end -}} ================================================ FILE: web/website/themes/prql-theme/layouts/_default/article.html ================================================ {{ define "main" }}
{{ if not .Params.no_head }}

{{ .Title }}

{{ range .Params.Authors }} {{ . }}· {{ end }} {{ if .Date }} {{ end }} {{ if (lt .Date .Lastmod) }} ·
Updated
{{ end }}
{{ end }}
{{ .Content }}
{{ if .Params.toc }} {{ end }}
{{ end }} ================================================ FILE: web/website/themes/prql-theme/layouts/_default/baseof.html ================================================ {{ partial "head.html" . }} {{ partial "header.html" . }}
{{- block "main" . }}{{- end }} {{- block "footer" . }} {{ partial "footer.html" . }} {{- end }}
================================================ FILE: web/website/themes/prql-theme/layouts/_default/big_iframe.html ================================================ {{ define "footer" }} {{ end }} {{ define "main" }}
{{ .Content }}
{{ end }} ================================================ FILE: web/website/themes/prql-theme/layouts/_default/home.html ================================================ {{ define "main" }} {{ with .Params.hero_section }} {{ if .enable }}

Pipelined Relational Query Language, pronounced “Prequel”

{{ .heading | markdownify }}

{{ .bottom_text | markdownify }}

{{ if .button.enable }} {{ with .button }} {{ .label }} {{ end }} {{ end }}
          {{ $.Site.Data.examples.hero.prql }}
        
{{ end }} {{ end }}
{{ with .Params.why_prql_section }} {{ if .enable }}

{{ .title | markdownify }}

{{ range .items }}

{{ .title | markdownify }}

{{/* Probably there's a more general way of formatting this padding; the default is 2em, which looked too indented */}}
    {{ range .content }}
  • {{ . | markdownify }}
  • {{ end }}
{{ end }}
{{ end }} {{ end }} {{ with .Params.showcase_section }} {{ if .enable }}

{{ .title | markdownify }}

{{ range .content }}

{{ . }}

{{ end }} {{ range .buttons }} {{ .label }} {{ end }}
{{ range $index, $e := .examples }}
{{ $example := index $.Site.Data.examples $e }}
{{ $example.prql }}
{{ $example.sql }}
{{ end }}
{{ end }} {{ end }} {{ with .Params.principles_section }} {{/* TODO: can we integrate this with section-cards? It's very similar */}} {{ if .enable }}

{{ .title | markdownify }}

{{ range .items }}

{{ .title | markdownify }}

{{ .main_text | markdownify }}

{{ .content | markdownify }}

{{ end }}
{{ end }} {{ end }} {{ with .Params.videos_section }} {{ if .enable }}

{{ .title | markdownify }}

{{ range .items }}
{{ end }}
{{ end }} {{ end }} {{ partial "section-cards" .Params.integrations_section }} {{ partial "section-cards" .Params.tools_section }} {{ partial "section-cards" .Params.bindings_section }} {{ partial "section-testimonials" .Params.testimonials_section }}
{{ end }} ================================================ FILE: web/website/themes/prql-theme/layouts/_default/list.html ================================================ {{ define "main" }}

{{ .Title }}

{{ if .Content }}
{{ .Content }}
{{ end }} {{ range .Pages }}

{{ .Title }}

{{ end }}
{{ end }} ================================================ FILE: web/website/themes/prql-theme/layouts/_default/single.html ================================================ {{ define "main" }}
{{ if not .Params.no_head }}

{{ .Title }}

{{ if .Date }} {{ end }} {{ end }} {{ .Content }}
{{ end }} ================================================ FILE: web/website/themes/prql-theme/layouts/partials/footer.html ================================================

Join us

{{ with site.Params.contribute }}

{{ .subtitle | markdownify }}

    {{ range .list }}
  • {{ .text | markdownify }}
  • {{ end }}
{{ if .button }} {{ .button.text }} {{ end }} {{ end }}
{{ with site.Params.follow }}

{{ .subtitle | markdownify }}

    {{ range .list }}
  • {{ .text | markdownify }}
  • {{ end }}
{{ end }} {{/* TODO: put the content of these buttons in the same place as the links */}}

{{ site.Params.license }}

================================================ FILE: web/website/themes/prql-theme/layouts/partials/head.html ================================================ {{ template "_internal/opengraph.html" . }} {{ .Page.Title }} {{ template "_internal/google_analytics.html" . }} ================================================ FILE: web/website/themes/prql-theme/layouts/partials/header.html ================================================ ================================================ FILE: web/website/themes/prql-theme/layouts/partials/section-cards.html ================================================ {{ if .enable }}

{{ .title | markdownify }}

{{ range .sections }}
{{ if and .link .label }}

{{/* We recreate the markdown so external links will parse & display as such. */}} {{ print "[" .label "]" "(" .link ")" | markdownify }}

{{ else }}

{{ .label }}

{{ end }} {{ .text | markdownify }}
{{ end }}
{{ end }} ================================================ FILE: web/website/themes/prql-theme/layouts/partials/section-testimonials.html ================================================ {{ if .enable }}

{{ .title | markdownify }}

{{ range site.Data.testimonials }}
{{/* Margin-top & margin-bottom are set to match Tweets. Maybe there's a better way of designing this? (and maybe we design from the stylesheet?) */}} {{ with .quote }}
{{ if .link }} {{ .text }} — {{ .author }} {{ else }}

{{ .text }} — {{ .author }}

{{ end }}
{{ end }} {{ with .tweet }}
{{/* The Firefox tracking protection blocks images hosted on pbs.twitter.com, see https://bugzilla.mozilla.org/show_bug.cgi?id=1458915 So we just download the images when building the website. */}} {{ $imageSrc := (resources.GetRemote "https://abs.twimg.com/sticky/default_profile_images/default_profile_normal.png" | resources.Copy "default.png").RelPermalink }} {{ $localImgPath := printf "cached-avatars/%s.png" .screen_name }} {{ with resources.GetRemote .profile_image_url_https }} {{ $imageSrc = (resources.Copy $localImgPath .).RelPermalink }} {{ else }} {{ warnf "[section-testimonials.html] couldn't fetch remote image %v" .profile_image_url_https }} {{ end }}

{{ .name }}

@{{ .screen_name }}
{{ .text }}
{{ .favorite_count }}
{{ .created_at | time.Format "15:04 · Jan 2, 2006" }}
Twitter
{{ end }}
{{ end }}
{{ end }} ================================================ FILE: web/website/themes/prql-theme/layouts/shortcodes/cite.html ================================================ {{ .Inner }} ================================================ FILE: web/website/themes/prql-theme/layouts/shortcodes/columns.html ================================================
{{ range split .Inner "<--->" }}
{{ . | $.Page.RenderString }}
{{ end }}
================================================ FILE: web/website/themes/prql-theme/layouts/shortcodes/faq.html ================================================

{{ (.Get 0) | markdownify }}

{{ .Inner | markdownify }}
================================================ FILE: web/website/themes/prql-theme/layouts/shortcodes/link.html ================================================ {{/* Compile a link as markdown so it runs the `render-link` template */}} {{ print "[" (.Get "label") "]" "(" .Get "link" ")" | markdownify }} ================================================ FILE: web/website/themes/prql-theme/static/main.js ================================================ (function () { "use strict"; /** * Easy selector helper function */ const select = (el, all = false) => { el = el.trim(); if (all) { return [...document.querySelectorAll(el)]; } else { return document.querySelector(el); } }; /** * Easy event listener function */ const on = (type, el, listener, all = false) => { let selectEl = select(el, all); if (selectEl) { if (all) { selectEl.forEach((e) => e.addEventListener(type, listener)); } else { selectEl.addEventListener(type, listener); } } }; /** * Easy on scroll event listener */ const onscroll = (el, listener) => { el.addEventListener("scroll", listener); }; /** * Back to top button */ let backtotop = select(".back-to-top"); if (backtotop) { const toggleBacktotop = () => { if (window.scrollY > 100) { backtotop.classList.add("active"); } else { backtotop.classList.remove("active"); } }; window.addEventListener("load", toggleBacktotop); onscroll(document, toggleBacktotop); } })(); ================================================ FILE: web/website/themes/prql-theme/static/plugins/highlight/highlight.css ================================================ /* Highlight.js obsidian theme */ pre code.hljs { display: block; overflow-x: auto; } code.hljs { padding: 3px 5px; } .hljs { color: #e0e2e4; } .hljs-keyword, .hljs-literal, .hljs-selector-id, .hljs-selector-tag { color: #e4a24b; } .hljs-number { color: #71b0eb; } .hljs-params { color: #5b8fc5; } .hljs-link, .hljs-regexp { color: #d39745; } .hljs-meta { color: #557182; } .hljs-addition, .hljs-built_in, .hljs-bullet, .hljs-emphasis, .hljs-module, .hljs-name, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-subst, .hljs-tag, .hljs-template-tag, .hljs-template-variable, .hljs-type, .hljs-variable { color: #8cbbad; } .hljs-string, .hljs-symbol { color: #d84444; } .hljs-comment, .hljs-deletion, .hljs-quote { color: #818e96; } .hljs-selector-class { color: #a082bd; } .hljs-doctag, .hljs-keyword, .hljs-literal, .hljs-name, .hljs-section, .hljs-selector-tag, .hljs-strong, .hljs-title, .hljs-type { font-weight: 700; } .hljs-class .hljs-title, .hljs-code, .hljs-section, .hljs-title.class_ { color: #fff; } ================================================ FILE: web/website/themes/prql-theme/static/plugins/highlight/prql.js ================================================ /* Language: PRQL Description: PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement. Category: common, database Requires: markdown.js Website: https://prql-lang.org/ */ // !!keep consistent with // https://github.com/PRQL/prql/blob/main/reference/highlight-prql.js // // TODO: can we import one from the other at build time? // We should probably grab more from other languages at // https://github.com/highlightjs/highlightjs-tsql/tree/main/src/languages. // Possibly we can even import parts at runtime, simplifying this file? formatting = function (hljs) { const BUILTIN_FUNCTIONS = [ // Aggregate functions "any", "average", "concat_array", "count", "every", "max", "min", "stddev", "sum", // File reading functions "read_csv", "read_json", "read_parquet", // List functions "all", "map", "zip", "_eq", "_is_null", // Misc functions "from_text", // Window functions "lag", "lead", "first", "last", "rank", "rank_dense", "row_number", ]; const MODULES = ["date", "math", "text"]; const DATATYPES = [ "bool", "float", "int", "int8", "int16", "int32", "int64", "text", "timestamp", ]; const TRANSFORMS = [ "aggregate", "append", "derive", "filter", "from", "group", "join", "select", "sort", "take", "union", "window", ]; const KEYWORDS = ["let", "prql", "into", "case", "in", "as", "module"]; const CHAR_ESCAPE = { scope: "char.escape", match: /\\\\|\\([bfnrt]|u{[0-9A-Fa-f]{1,6}}|x[0-9A-Fa-f]{2})/, }; return { name: "PRQL", case_insensitive: true, keywords: { built_in: BUILTIN_FUNCTIONS, module: MODULES, keyword: [...TRANSFORMS, ...BUILTIN_FUNCTIONS, ...KEYWORDS], literal: "false true null", type: DATATYPES, }, contains: [ { // docblock begin: "#!", end: "$", subLanguage: "markdown", relevance: 0, }, hljs.COMMENT("#", "$"), { // named arg scope: "params", begin: /\w+\s*:/, end: "", relevance: 10, }, { // meta prql for target and version scope: "meta", match: /^prql/, }, // This seems much too strong at the moment, so disabling. I think ideally // we'd have it for aliases but not assigns. // { // // assign // scope: { 1: "variable" }, // match: [/\w+\s*/, /=[^=]/], // relevance: 10, // }, { // date scope: "string", match: /@(\d*|-|\.\d|:|T)+Z?/, relevance: 10, }, { // interval scope: "string", // Add more as needed match: /\d+(years|months|weeks|days|hours|minutes|seconds|milliseconds|microseconds)/, relevance: 10, }, { scope: "string", relevance: 10, variants: [ { begin: 'r"""', end: '"""', }, { begin: 'r"', end: '"', }, ], }, { // interpolation strings: s-strings are variables and f-strings are // strings? (Though possibly that's too cute, open to adjusting) // scope: "variable", relevance: 10, variants: [ { begin: 's"""', end: '"""', }, { begin: 's"', end: '"', }, ], contains: [ // I tried having the `f` / `s` be marked differently, but I don't // think it's possible to have a subscope within the begin / end. { // I think `variable` is the right scope rather than defaulting to // white, but not 100% sure; using `subst` is suggested in the docs. scope: "variable", begin: /\{/, end: /\}/, }, ], }, { scope: "string", relevance: 10, variants: [ { begin: 'f"""', end: '"""', }, { begin: 'f"', end: '"', }, ], contains: [ CHAR_ESCAPE, { scope: "variable", begin: "f", end: '"', // excludesEnd: true, }, // TODO: would be nice to have this be a different color, but I don't // think it's possible to have a subscope within the begin / end. // { // scope: "punctuation", // match: /{|}/, // }, { scope: "variable", begin: /\{/, end: /\}/, }, ], }, { // normal string scope: "string", relevance: 10, variants: [ // TODO: is there a way of encoding the actual rule here? Otherwise // we're just adding the variants we use... { begin: '"""""', end: '"""""', }, { begin: '"""', end: '"""', }, { begin: '"', end: '"', }, { begin: "'", end: "'", }, ], contains: [CHAR_ESCAPE], }, { scope: "punctuation", match: /[\[\]{}(),]/ }, { scope: "operator", match: /==|~=|\+|\-|\/|\*|!=|->|=>|<=|>=|&&|\|\||<|>/, relevance: 10, }, { scope: "number", // Regex explanation: // 1. `\b`: asserts a word boundary. This ensures that the pattern matches numbers that are distinct words or at the boundaries of words. // 2. `(\d[_\d]*(e|E)\d[_\d]*)`: This is the first alternative in the main group and matches numbers in scientific notation: // - `\d`: matches a digit (0-9). // - `[_\d]*`: matches zero or more underscores or digits, representing the numbers before the `e` in scientific notation. // - `(e|E)`: matches the letter 'e' or 'E' for scientific notation. // - `\d`: matches a digit (0-9), the beginning of the exponent. // - `[_\d]*`: matches zero or more underscores or digits, representing the numbers after the `e` in scientific notation. // 3. `(\d[_\d]*|(\d\.[\d_]*\d))`: This is the second alternative in the main group and matches standard numbers without the scientific notation: // - `\d[_\d]*`: matches a sequence starting with a digit and followed by zero or more digits or underscores. // - `|`: OR // - `(\d\.[\d_]*\d)`: matches numbers with a decimal point: // - `\d`: matches the digit(s) before the decimal point. // - `\.`: matches the decimal point. // - `[\d_]*\d`: matches digits after the decimal point, ensuring the sequence ends in a digit and not a trailing underscore. // 4. `(\.[\d_]+)`: This is the third alternative in the main group: // - `\.`: matches a literal dot, so this alternative captures numbers that begin with a decimal point. // - `[\d_]+`: matches one or more digits or underscores, for the sequence after the initial dot. match: /\b((\d[_\d]*(e|E)\d[_\d]*)|(\d[_\d]*|(\d\.[\d_]*\d))|(\.[\d_]+))/, relevance: 10, }, { // range scope: "symbol", match: /\.{2}/, relevance: 10, }, // Unfortunately this just overrides any keywords. It's also not // complete — it only handles functions at the beginning of a line. // I spent several hours trying to get hljs to handle this, but // because there's no recursion, I'm not sure it's possible. // Possibly we could hook into `on:begin` and implement it // ourselves, but this would be a lot of overhead. // { // function // keywords: TRANSFORMS.join(' '), // beginScope: { 1: 'title.function' }, // begin: [/^\s*[a-zA-Z]+/, /(\s+[a-zA-Z]+)+/], // relevance: 10 // }, ], }; }; hljs.registerLanguage("prql", formatting); // This line should only exists in the website, not the book. hljs.highlightAll(); ================================================ FILE: web/website/themes/prql-theme/static/style.css ================================================ /* Inter ExtraBold */ @font-face { font-family: "InterExtraBold"; font-weight: 800; font-style: normal; font-display: swap; src: local(""), url("../fonts/inter-extra-bold.woff2") format("woff2"), url("../fonts/inter-extra-bold.woff") format("woff"); } /*-------------------------------------------------------------- # General --------------------------------------------------------------*/ :root { --main-color: #202b38; --secondary-color: #c92a2a; --third-color: #1864ab; --title-font: InterExtraBold, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; --body-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } body { font-family: var(--body-font); color: var(--main-color); } a:hover { color: var(--secondary-color); } h1, h2, h3, h4, h5, h6 { font-family: var(--title-font); font-weight: 900; } a.btn { background-color: var(--main-color); color: #fff; font-size: 20px; padding: 10px 20px; } a.btn:hover { color: #fff; } a.btn-icon { display: inline-flex !important; font-size: 25px; padding: 5px !important; margin: 0.5rem 1rem; color: #fff !important; background: var(--third-color); border-radius: 50%; transition: all 0.3s; text-decoration: none; } a.btn-icon:hover { background: var(--main-color); } a.btn-icon i { width: 1.5em; line-height: 1.5; text-align: center; } /*-------------------------------------------------------------- # Back to top button --------------------------------------------------------------*/ .back-to-top { position: fixed; visibility: hidden; opacity: 0; right: 15px; bottom: 15px; z-index: 996; background: var(--main-color); width: 40px; height: 40px; border-radius: 4px; transition: all 0.4s; text-decoration: none; } .back-to-top i { font-size: 28px; color: #fff; line-height: 0; } .back-to-top:hover { background: var(--secondary-color); color: #fff; } .back-to-top.active { visibility: visible; opacity: 1; } /* Code */ code.hljs { padding: 0.75rem 1rem; border-radius: 8px; background-color: var(--main-color); color: #ffffff; } code:not(.hljs) { padding: 1px 0.4rem; border-radius: 4px; background-color: #ccc; color: var(--main-color); } /* width */ pre ::-webkit-scrollbar { height: 6px; } /* Track */ pre ::-webkit-scrollbar-track { background: var(--main-color); } /* Handle */ pre ::-webkit-scrollbar-thumb { background: #000; border-radius: 4px; width: 2px; } /* Handle on hover */ pre ::-webkit-scrollbar-thumb:hover { background: rgb(109, 109, 109); } /*-------------------------------------------------------------- # Header --------------------------------------------------------------*/ #header { background: white; z-index: 997; padding: 15px 0; border-bottom: 1px solid rgba(0, 0, 0, 0.125); } #header .logo { max-height: 50px; text-decoration: none; } #header .logo h1 { margin: 0; } #header a:not(.btn-icon) { color: var(--main-color); transition: 0.2s; } #header a:hover, #header .active, #header .active:focus, #header li:hover > a { color: var(--secondary-color); } /*-------------------------------------------------------------- # Hero Section --------------------------------------------------------------*/ .hero { width: 100%; min-height: calc(100vh - 81px); background: #f2f9ff; background: repeating-linear-gradient( 45deg, transparent, transparent 150px, #f2f9ff 150px, #f2f9ff 300px ), linear-gradient(#daeaff, #f2f9ff); display: flex; flex-direction: column; justify-content: center; flex: 1 0 fit-content; } .hero h4 { font-size: 20px; color: var(--main-color); } .hero h4 span { color: #c92a2a; } .hero h2 { font-size: 45px; color: var(--main-color); font-weight: 700; } .hero .bottom-text { font-size: 20px; color: #4b5157; } .hero a.btn { color: #fff; background-color: var(--third-color); padding: 10px 20px; font-size: 23px; font-weight: 500; margin-top: 20px; transition: 0.3s; } .hero a.btn:hover { background-color: var(--main-color); } @media (max-width: 992px) { #hero { height: 100vh; } #hero .carousel-container { top: 8px; } } @media (max-width: 768px) { #hero h2 { font-size: 28px; } } @media (min-width: 1024px) { #hero .carousel-control-prev, #hero .carousel-control-next { width: 5%; } } @media (max-height: 500px) { #hero { height: 120vh; } } .callout { padding: 1.25rem 1.25rem 0.5rem 1.25rem; margin-top: 1.25rem; margin-bottom: 1.25rem; background-color: #fff; border-left: 0.25rem solid #c92a2a; } /*-------------------------------------------------------------- # Sections General --------------------------------------------------------------*/ section { padding: 2rem 0; overflow: hidden; } .section-bg, .services .icon-box { background: #f2f9ff; background: repeating-linear-gradient( 45deg, transparent, transparent 150px, #f2f9ff 150px, #f2f9ff 300px ), linear-gradient(#daeaff, #f2f9ff); } .section-title h2 { font-size: 45px; font-weight: 700; padding: 0; color: var(--main-color); } .section-title h3 { font-size: 22px; font-weight: 600; padding: 0; color: var(--third-color); } .section-title p { margin: 0; margin: 0; font-size: 18px; font-weight: 400; color: var(--main-color); } /*-------------------------------------------------------------- # Cards --------------------------------------------------------------*/ .big-cards-section { background: #f2f9ff; } .big-cards-section .card { padding: 30px; height: 100%; } .big-cards-section .card i { color: #202b38; font-size: 52px; } .big-cards-section .card h4 { font-weight: 600; font-size: 24px; line-height: 60px; margin-bottom: 20px; color: var(--secondary-color); } .big-cards-section .card h4::after { content: ""; width: 50%; height: 2px; display: block; background: var(--third-color); margin: 4px 0px; } .big-cards-section .card p { color: var(--main-color); } /*-------------------------------------------------------------- # showcase-section --------------------------------------------------------------*/ .showcase-section { background: #f2f9ff; } .showcase-section #v-pills-tabContent { flex-grow: 1; } .nav-pills .nav-link { color: var(--third-color); } .nav-pills .nav-link.active { color: #fff; background-color: var(--main-color); } /*-------------------------------------------------------------- # content-section --------------------------------------------------------------*/ .cards-section { background: #f2f9ff; } .content-section .icon { font-size: 250px; color: var(--main-color); } .content-section .card { padding: 1.5em; height: 100%; flex: 100%; } .content-section .card a { color: var(--third-color); } .content-section .card a:hover { color: var(--secondary-color); } .content-section h3 { color: var(--third-color); font-weight: 600; } .content-section ul { list-style: none; } .content-section ul li P { font-size: 22px; color: var(--main-color); } .content-section ul li P i { font-size: 12px; padding: 3px; } /*-------------------------------------------------------------- # testimonials-section --------------------------------------------------------------*/ .testimonials-section { background: repeating-linear-gradient( 45deg, transparent, transparent 150px, #f2f9ff 150px, #f2f9ff 300px ), linear-gradient(#f2f9ff, #daeaff); } .quote .quote-text { height: 100%; margin: 0; display: flex; flex-direction: column; gap: 2rem; justify-content: space-between; } .quote span { display: block; font-weight: bold; } .quote a:link, .quote a:visited { text-decoration: none; color: inherit; } .tweet-header { display: flex; align-items: center; justify-content: space-between; } .tweet-author { display: flex; position: relative; align-items: center; } .tweet-author-image { width: 48px; height: 48px; border-radius: 9999px; } .tweet-author-info { margin-left: 1rem; } .tweet-author-name { line-height: 1rem; font-weight: 500; margin: 0; } .tweet-author-handler { line-height: 1.8rem; } .tweet-logo { width: 20px; height: 20px; position: absolute; top: -4px; left: -8px; background: var(--third-color); color: #fff; border-radius: 9999px; padding: 0.2rem; } /*-------------------------------------------------------------- # Article --------------------------------------------------------------*/ article { padding: 4rem 0; font-size: 1.1em; } article h1 { margin-bottom: 2rem; } article h2 { margin: 2rem 0 1rem 0; } article img { max-width: 100%; } blockquote p { margin: 0 0 1rem 0.5rem; padding: 0.25rem 1.25rem; border-left: 0.25rem solid lightgrey; } article > blockquote > *:last-child { margin-bottom: 0; } article > pre, article > .highlight { margin: 0 -0.5rem 1rem -0.5rem; } article > .highlight > pre { background-color: transparent !important; } /*-------------------------------------------------------------- # Footer --------------------------------------------------------------*/ #footer { /* TODO: the background was a dark color, but it made the links quite difficult to read. Probably I (@max-sixty) shouldn't be choosing colors — so please feel free to adjust. */ background: #cbdcef; /* background: linear-gradient(#58718d, var(--main-color)); */ padding: 1rem 0 2rem 0; /* color: #fff; */ } /* #footer p { color: #bbb; } */ /* #footer a.btn { background-color: var(--secondary-color); } */ #footer ul { padding-left: 1rem; /* color: #bbb; */ } /* #footer .copyright { display: flex; justify-content: space-between; align-items: center; padding-top: 30px; } #footer .copyright i { font-size: 28px; color: rgba(219, 219, 219, 0.712); padding-left: 20px; } #footer .copyright i:hover { color: #fff; } #footer .copyright p { display: flex; } */ /*-------------------------------------------------------------- # FAQ sections --------------------------------------------------------------*/ details.faq h2 { display: inline-flex; align-items: center; margin-bottom: 0; font-size: 26px; white-space: pre-wrap; } details.faq[open] summary h2 { margin-bottom: 1rem; } details.faq summary::-webkit-details-marker { display: none; } details.faq summary { list-style: none; } details.faq summary h2::before { content: "►"; font-size: 16px; margin-right: 5px; } details.faq[open] summary h2:before { content: "▼"; } /*-------------------------------------------------------------- # Table of contents --------------------------------------------------------------*/ .toc > * { position: fixed; padding: 4rem 0; } .toc ul li { list-style: none; margin: 0; padding-top: 0.5em; } .toc ul li ul { display: none; } /*-------------------------------------------------------------- # List --------------------------------------------------------------*/ .list h1 { margin-bottom: 2rem; } .list .post-item { padding: 0.75rem 0; } .list .post-item > a { font-family: var(--title-font); } /*-------------------------------------------------------------- # "Full screen" iframe views for book, playground etc --------------------------------------------------------------*/ .big-iframe { flex-grow: 1; } .big-iframe iframe { height: 100%; width: 100vw; display: block; border: none; } ================================================ FILE: web/website/themes/prql-theme/theme.toml ================================================ # theme.toml template for a Hugo theme # See https://github.com/gohugoio/hugoThemes#themetoml for an example license = "Apache 2.0" min_version = "0.41.0" name = "PRQL Theme"