use std::env::current_dir;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::str::FromStr;
use insta_cmd::assert_cmd_snapshot;
use insta_cmd::get_cargo_bin;
use tempfile::TempDir;
use walkdir::WalkDir;
#[cfg(not(windows))] #[test]
fn help() {
assert_cmd_snapshot!(prqlc_command().arg("--help"), @r"
success: true
exit_code: 0
----- stdout -----
Usage: prqlc [OPTIONS] [COMMAND]
Commands:
parse Parse into PL AST
lex Lex into Lexer Representation
fmt Parse & generate PRQL code back
collect Parse the whole project and collect it into a single PRQL source file
debug Commands for meant for debugging, prone to change
experimental Experimental commands are prone to change
compile Parse, resolve, lower into RQ & compile to SQL
watch Watch a directory and compile .prql files to .sql files
list-targets Show available compile target names
shell-completion Print a shell completion for supported shells
help Print this message or the help of the given subcommand(s)
Options:
--color <WHEN> Controls when to use color [default: auto] [possible values: auto, always,
never]
-h, --help Print help
-V, --version Print version
----- stderr -----
");
assert_cmd_snapshot!(prqlc_command(), @r"
success: true
exit_code: 0
----- stdout -----
Usage: prqlc [OPTIONS] [COMMAND]
Commands:
parse Parse into PL AST
lex Lex into Lexer Representation
fmt Parse & generate PRQL code back
collect Parse the whole project and collect it into a single PRQL source file
debug Commands for meant for debugging, prone to change
experimental Experimental commands are prone to change
compile Parse, resolve, lower into RQ & compile to SQL
watch Watch a directory and compile .prql files to .sql files
list-targets Show available compile target names
shell-completion Print a shell completion for supported shells
help Print this message or the help of the given subcommand(s)
Options:
--color <WHEN> Controls when to use color [default: auto] [possible values: auto, always,
never]
-h, --help Print help
-V, --version Print version
----- stderr -----
");
}
#[test]
fn get_targets() {
assert_cmd_snapshot!(prqlc_command().arg("list-targets"), @r"
success: true
exit_code: 0
----- stdout -----
sql.any
sql.ansi
sql.bigquery
sql.clickhouse
sql.duckdb
sql.generic
sql.glaredb
sql.mssql
sql.mysql
sql.postgres
sql.redshift
sql.sqlite
sql.snowflake
----- stderr -----
");
}
#[test]
fn compile() {
assert_cmd_snapshot!(prqlc_command()
.args(["compile", "--hide-signature-comment"])
.pass_stdin("from tracks"), @r"
success: true
exit_code: 0
----- stdout -----
SELECT
*
FROM
tracks
----- stderr -----
");
}
#[cfg(not(windows))] #[test]
fn compile_help() {
assert_cmd_snapshot!(prqlc_command().args(["compile", "--help"]), @r"
success: true
exit_code: 0
----- stdout -----
Parse, resolve, lower into RQ & compile to SQL
Only displays the main pipeline and does not handle loop.
Usage: prqlc compile [OPTIONS] [INPUT] [OUTPUT] [MAIN_PATH]
Arguments:
[INPUT]
[default: -]
[OUTPUT]
[default: -]
[MAIN_PATH]
Identifier of the main pipeline
Options:
--hide-signature-comment
Exclude the signature comment containing the PRQL version
--no-format
Emit unformatted, dense SQL
-t, --target <TARGET>
Target to compile to
[env: PRQLC_TARGET=]
[default: sql.any]
--debug-log <DEBUG_LOG>
File path into which to write the debug log to
[env: PRQLC_DEBUG_LOG=]
--color <WHEN>
Controls when to use color
[default: auto]
[possible values: auto, always, never]
-h, --help
Print help (see a summary with '-h')
----- stderr -----
");
}
#[test]
fn long_query() {
assert_cmd_snapshot!(prqlc_command()
.args(["compile", "--hide-signature-comment", "--debug-log=log_test.html"])
.pass_stdin(r#"
let long_query = (
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
)
from long_query
"#), @r"
success: true
exit_code: 0
----- stdout -----
WITH table_2 AS (
SELECT
title,
COUNT(*) AS ct
FROM
employees
WHERE
gross_cost > 0
GROUP BY
title
HAVING
COUNT(*) > 200
ORDER BY
ct
LIMIT
20
), table_1 AS (
SELECT
title,
ct
FROM
table_2
WHERE
ct > 200
ORDER BY
ct
LIMIT
20
), table_0 AS (
SELECT
title,
ct
FROM
table_1
WHERE
ct > 200
ORDER BY
ct
LIMIT
20
), long_query AS (
SELECT
title,
ct
FROM
table_0
WHERE
ct > 200
ORDER BY
ct
LIMIT
20
)
SELECT
title,
ct
FROM
long_query
ORDER BY
ct
----- stderr -----
");
assert!(PathBuf::from_str("./log_test.html").unwrap().is_file());
}
#[test]
fn compile_project() {
let mut cmd = prqlc_command();
cmd.args([
"compile",
"--hide-signature-comment",
"--debug-log=log_test.json",
project_path().to_str().unwrap(),
"-",
"main",
]);
assert_cmd_snapshot!(cmd, @r"
success: true
exit_code: 0
----- stdout -----
WITH table_1 AS (
SELECT
120 AS artist_id,
DATE '2023-05-18' AS last_listen
UNION
ALL
SELECT
7 AS artist_id,
DATE '2023-05-16' AS last_listen
),
favorite_artists AS (
SELECT
artist_id,
last_listen
FROM
table_1
),
table_0 AS (
SELECT
*
FROM
read_parquet('artists.parquet')
),
input AS (
SELECT
*
FROM
table_0
)
SELECT
favorite_artists.artist_id,
favorite_artists.last_listen,
input.*
FROM
favorite_artists
LEFT OUTER JOIN input ON favorite_artists.artist_id = input.artist_id
----- stderr -----
");
assert!(PathBuf::from_str("./log_test.json").unwrap().is_file());
assert_cmd_snapshot!(prqlc_command()
.args([
"compile",
"--hide-signature-comment",
project_path().to_str().unwrap(),
"-",
"favorite_artists",
]), @r"
success: true
exit_code: 0
----- stdout -----
WITH table_0 AS (
SELECT
120 AS artist_id,
DATE '2023-05-18' AS last_listen
UNION
ALL
SELECT
7 AS artist_id,
DATE '2023-05-16' AS last_listen
)
SELECT
artist_id,
last_listen
FROM
table_0
----- stderr -----
");
}
#[test]
fn format() {
assert_cmd_snapshot!(prqlc_command().args(["fmt"]).pass_stdin("from tracks | take 20"), @r"
success: true
exit_code: 0
----- stdout -----
from tracks
take 20
----- stderr -----
");
let temp_dir = TempDir::new().expect("Failed to create temp directory");
copy_dir(&project_path(), temp_dir.path());
let _result = prqlc_command()
.args(["fmt", temp_dir.path().to_str().unwrap()])
.status()
.unwrap();
compare_directories(&project_path(), temp_dir.path());
}
fn copy_dir(src: &Path, dst: &Path) {
for entry in WalkDir::new(src) {
let entry = entry.unwrap();
let path = entry.path();
if path.is_file() {
let relative_path = path.strip_prefix(src).unwrap();
let target_path = dst.join(relative_path);
fs::create_dir_all(target_path.parent().unwrap()).unwrap();
fs::copy(path, target_path).unwrap();
}
}
}
fn compare_directories(dir1: &Path, dir2: &Path) {
for entry in WalkDir::new(dir1).into_iter().filter_map(|e| e.ok()) {
let path1 = entry.path();
if path1.is_file() {
let relative_path = path1.strip_prefix(dir1).unwrap();
let path2 = dir2.join(relative_path);
assert!(
path2.exists(),
"File {relative_path:?} doesn't exist in the formatted directory"
);
similar_asserts::assert_eq!(
fs::read_to_string(path1).unwrap(),
fs::read_to_string(path2).unwrap()
);
}
}
}
#[test]
fn debug() {
assert_cmd_snapshot!(prqlc_command()
.args(["debug", "lineage"])
.pass_stdin("from tracks | select {artist, album}"), @r"
success: true
exit_code: 0
----- stdout -----
frames:
- - 1:14-36
- columns:
- !Single
name:
- tracks
- artist
target_id: 118
target_name: null
- !Single
name:
- tracks
- album
target_id: 119
target_name: null
inputs:
- id: 116
name: tracks
table:
- default_db
- tracks
nodes:
- id: 116
kind: Ident
span: 1:0-11
ident: !Ident
- default_db
- tracks
parent: 121
- id: 118
kind: Ident
span: 1:22-28
ident: !Ident
- this
- tracks
- artist
targets:
- 116
parent: 120
- id: 119
kind: Ident
span: 1:30-35
ident: !Ident
- this
- tracks
- album
targets:
- 116
parent: 120
- id: 120
kind: Tuple
span: 1:21-36
children:
- 118
- 119
parent: 121
- id: 121
kind: 'TransformCall: Select'
span: 1:14-36
children:
- 116
- 120
ast:
name: Project
stmts:
- VarDef:
kind: Main
name: main
value:
Pipeline:
exprs:
- FuncCall:
name:
Ident:
- from
span: 1:0-4
args:
- Ident:
- tracks
span: 1:5-11
span: 1:0-11
- FuncCall:
name:
Ident:
- select
span: 1:14-20
args:
- Tuple:
- Ident:
- artist
span: 1:22-28
- Ident:
- album
span: 1:30-35
span: 1:21-36
span: 1:14-36
span: 1:0-36
span: 1:0-36
----- stderr -----
");
prqlc_command()
.args(["debug", "ast"])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.unwrap();
}
#[test]
fn debug_json_schema() {
use serde_json::Value;
let output = prqlc_command()
.args(["debug", "json-schema", "--ir-type", "pl"])
.output()
.unwrap();
assert!(output.status.success());
let stdout = std::str::from_utf8(&output.stdout).unwrap();
let parsed: Value = serde_json::from_str(stdout).unwrap();
assert_eq!(
parsed["$schema"],
"https://json-schema.org/draft/2020-12/schema"
);
assert_eq!(parsed["type"], "object");
assert_eq!(parsed["title"], "ModuleDef");
}
#[test]
fn shell_completion() {
for shell in ["bash", "fish", "powershell", "zsh"].iter() {
assert_cmd_snapshot!(prqlc_command().args(["shell-completion", shell]));
}
}
fn project_path() -> PathBuf {
current_dir()
.unwrap()
.canonicalize()
.unwrap()
.join("tests/integration/project")
}
fn prqlc_command() -> Command {
let mut cmd = Command::new(get_cargo_bin("prqlc"));
normalize_prqlc(&mut cmd);
cmd
}
fn normalize_prqlc(cmd: &mut Command) -> &mut Command {
cmd
.env_remove("CLICOLOR_FORCE")
.env("NO_COLOR", "1")
.args(["--color=never"])
.env_remove("RUST_BACKTRACE")
.env_remove("RUST_LOG")
}
#[test]
fn compile_no_prql_files() {
assert_cmd_snapshot!(prqlc_command().args(["compile", "README.md"]), @r"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Error: No `.prql` files found in the source tree
");
}
#[test]
fn lex() {
assert_cmd_snapshot!(prqlc_command().args(["lex"]).pass_stdin("from tracks"), @r"
success: true
exit_code: 0
----- stdout -----
- kind: Start
span:
start: 0
end: 0
- kind: !Ident from
span:
start: 0
end: 4
- kind: !Ident tracks
span:
start: 5
end: 11
----- stderr -----
");
assert_cmd_snapshot!(prqlc_command().args(["lex", "--format=json"]).pass_stdin("from tracks"), @r#"
success: true
exit_code: 0
----- stdout -----
[
{
"kind": "Start",
"span": {
"start": 0,
"end": 0
}
},
{
"kind": {
"Ident": "from"
},
"span": {
"start": 0,
"end": 4
}
},
{
"kind": {
"Ident": "tracks"
},
"span": {
"start": 5,
"end": 11
}
}
]
----- stderr -----
"#);
}
#[cfg(feature = "lsp")]
#[test]
fn lsp() {
let init = serde_json::to_string(&lsp_server::Message::Request(lsp_server::Request {
method: "initialize".into(),
id: lsp_server::RequestId::from(1),
params: serde_json::json!({"capabilities": {}}),
}))
.unwrap();
let initialized = serde_json::to_string(&lsp_server::Message::Notification(
lsp_server::Notification {
method: "initialized".into(),
params: serde_json::json!({}),
},
))
.unwrap();
let ex1 = serde_json::to_string(&lsp_server::Message::Notification(
lsp_server::Notification {
method: "exit".into(),
params: serde_json::Value::Null,
},
))
.unwrap();
assert_cmd_snapshot!(prqlc_command().args(["lsp"])
.pass_stdin(format!("Content-Length: {}\r\n\r\n{}Content-Length: {}\r\n\r\n{}Content-Length: {}\r\n\r\n{}",
init.len(), init,
initialized.len(), initialized,
ex1.len(), ex1))
, @r###"
success: true
exit_code: 0
----- stdout -----
Content-Length: 78
{"jsonrpc":"2.0","id":1,"result":{"capabilities":{"definitionProvider":true}}}
----- stderr -----
starting PRQL LSP server
starting main loop
got msg: Notification(Notification { method: "exit", params: Null })
got notification: Notification { method: "exit", params: Null }
shutting down server
"###);
}