Skip to content

Commit 29d7034

Browse files
committed
chore: update version april 24
1 parent bc5fb8b commit 29d7034

25 files changed

+2044
-267
lines changed

Cargo.lock

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

proto/quary/service/v1/connection_config.proto

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ message ConnectionConfig {
1919
ConnectionConfigBigQuery big_query = 5;
2020
ConnectionConfigSnowflake snowflake = 6;
2121
ConnectionConfigPostgres postgres = 7;
22+
ConnectionConfigRedshift redshift = 9;
2223
}
24+
2325
repeated Var vars = 8;
2426

2527
message ConnectionConfigSqLite {
@@ -40,16 +42,10 @@ message ConnectionConfig {
4042
message ConnectionConfigPostgres {
4143
string schema = 1;
4244
}
43-
//
44-
// message ConnectionConfigMySql {
45-
// string username = 1;
46-
// string password = 2;
47-
// string protocol = 3;
48-
// string host = 4;
49-
// string port = 5;
50-
// string database = 6;
51-
// map<string, string> params = 7;
52-
// }
45+
46+
message ConnectionConfigRedshift {
47+
string schema = 1;
48+
}
5349

5450
message ConnectionConfigBigQuery {
5551
string project_id = 1;

rust/cli/src/main.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -489,9 +489,13 @@ async fn main_wrapped() -> Result<(), String> {
489489
let database = database_from_config(&config).await?;
490490
let query_generator = database.query_generator();
491491
let (project, file_system) = parse_project(&query_generator).await?;
492-
let snapshots_sql =
493-
project_and_fs_to_sql_for_snapshots(&project, &file_system, &query_generator)
494-
.await?;
492+
let snapshots_sql = project_and_fs_to_sql_for_snapshots(
493+
&project,
494+
&file_system,
495+
&query_generator,
496+
&database,
497+
)
498+
.await?;
495499

496500
if snapshot_args.dry_run {
497501
println!("\n-- Create snapshots\n");

rust/cli/tests/cli_test.rs

Lines changed: 189 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use assert_cmd::Command;
2+
use chrono::NaiveDateTime;
23
use chrono::Utc;
34
use quary_core::databases::DatabaseConnection;
45
use quary_databases::databases_duckdb;
6+
use quary_databases::databases_redshift;
57
use std::fs;
68
use tempfile::tempdir;
79

@@ -331,8 +333,9 @@ async fn test_duckdb_snapshots() {
331333
let current_date = Utc::now().date_naive();
332334
let quary_valid_from_str = &result.rows[0][4];
333335
let quary_valid_from_date = quary_valid_from_str.split_whitespace().next().unwrap();
336+
println!("quary_valid_from_date: {}", quary_valid_from_date);
334337
let quary_valid_from =
335-
chrono::NaiveDate::parse_from_str(quary_valid_from_date, "%Y-%m-%d").unwrap();
338+
chrono::NaiveDate::parse_from_str(quary_valid_from_date, "%Y-%m-%dT%H:%M:%SZ").unwrap();
336339
assert_eq!(current_date, quary_valid_from);
337340

338341
// Update orders.csv data
@@ -382,7 +385,191 @@ async fn test_duckdb_snapshots() {
382385
.next()
383386
.unwrap();
384387
let updated_quary_valid_from =
385-
chrono::NaiveDate::parse_from_str(updated_quary_valid_from_date, "%Y-%m-%d").unwrap();
388+
chrono::NaiveDate::parse_from_str(updated_quary_valid_from_date, "%Y-%m-%dT%H:%M:%SZ")
389+
.unwrap();
386390
assert_eq!(current_date, updated_quary_valid_from);
387391
}
388392
}
393+
394+
/// This test simulates a workflow where a model references a snapshot in redshift.
395+
/// 1. The initial snapshot is taken which builds the orders_snapshot table in the database.
396+
/// 2. The project is built which references the orders_snapshot table.
397+
/// 3. The initial state of the snapshot is asserted.
398+
/// 4. The data is updated and a new snapshot is taken.
399+
/// 5. The updated state of the snapshot is asserted. (from the stg_orders_snapshot table)
400+
#[tokio::test]
401+
#[ignore]
402+
async fn test_redshift_snapshots() {
403+
// Prepare the database
404+
let database =
405+
databases_redshift::Redshift::new("", None, "", "", "", "", None, None, None, None, None)
406+
.await
407+
.ok()
408+
.unwrap();
409+
database
410+
.exec("DROP TABLE analytics.orders CASCADE")
411+
.await
412+
.unwrap();
413+
database
414+
.exec("DROP TABLE transform.orders_snapshot CASCADE")
415+
.await
416+
.unwrap();
417+
418+
database
419+
.exec(
420+
"
421+
CREATE TABLE analytics.orders (
422+
order_id character varying(255) ENCODE lzo,
423+
customer_id character varying(255) ENCODE lzo,
424+
order_date timestamp without time zone ENCODE az64,
425+
total_amount numeric(10, 2) ENCODE az64,
426+
status character varying(255) ENCODE lzo
427+
) DISTSTYLE AUTO;
428+
",
429+
)
430+
.await
431+
.unwrap();
432+
433+
database.exec(
434+
"
435+
INSERT INTO analytics.orders (order_id, customer_id, order_date, total_amount, status) VALUES ('1', '1', '2022-01-01 00:00:00', 100, 'in_progress')
436+
"
437+
)
438+
.await
439+
.unwrap();
440+
441+
// Setup
442+
let name = "quary";
443+
let temp_dir = tempdir().unwrap();
444+
let project_dir = temp_dir.path();
445+
446+
// create a .env file
447+
let env_file_path = project_dir.join(".env");
448+
let env_content =
449+
"REDSHIFT_HOST=\nREDSHIFT_PORT=\nREDSHIFT_USER=\nREDSHIFT_PASSWORD=\nREDSHIFT_DATABASE=";
450+
fs::write(&env_file_path, env_content).unwrap();
451+
452+
// Create snapshots directory and orders_snapshot.snapshot.sql file
453+
let snapshots_dir = project_dir.join("models").join("staging").join("snapshots");
454+
fs::create_dir_all(&snapshots_dir).unwrap();
455+
let orders_snapshot_file = snapshots_dir.join("orders_snapshot.snapshot.sql");
456+
let orders_snapshot_content =
457+
"SELECT order_id, customer_id, order_date, total_amount, status FROM q.raw_orders";
458+
fs::write(&orders_snapshot_file, orders_snapshot_content).unwrap();
459+
460+
// Create a model which references the snapshot
461+
let staging_models_dir = project_dir.join("models").join("staging");
462+
fs::create_dir_all(&staging_models_dir).unwrap();
463+
let stg_orders_snapshot_file = staging_models_dir.join("stg_orders_snapshot.sql");
464+
let stg_orders_snapshot_content = "SELECT * FROM q.orders_snapshot";
465+
fs::write(&stg_orders_snapshot_file, stg_orders_snapshot_content).unwrap();
466+
467+
// Create quary.yaml file
468+
let quary_yaml_content = "redshift:\n schema: transform";
469+
let quary_yaml_path = project_dir.join("quary.yaml");
470+
fs::write(&quary_yaml_path, quary_yaml_content).unwrap();
471+
472+
// Create schema.yaml file
473+
let schema_file = snapshots_dir.join("schema.yaml");
474+
let schema_content = r#"
475+
sources:
476+
- name: raw_orders
477+
path: analytics.orders
478+
snapshots:
479+
- name: orders_snapshot
480+
unique_key: order_id
481+
strategy:
482+
timestamp:
483+
updated_at: order_date
484+
"#;
485+
fs::write(&schema_file, schema_content).unwrap();
486+
487+
// Take the initial snapshot and build the project which references the snapshot
488+
Command::cargo_bin(name)
489+
.unwrap()
490+
.current_dir(project_dir)
491+
.args(vec!["snapshot"])
492+
.assert()
493+
.success();
494+
Command::cargo_bin(name)
495+
.unwrap()
496+
.current_dir(project_dir)
497+
.args(vec!["build"])
498+
.assert()
499+
.success();
500+
501+
{
502+
let result = database
503+
.query("SELECT order_id, customer_id, order_date, total_amount, status, quary_valid_from, quary_valid_to, quary_scd_id FROM transform.orders_snapshot")
504+
.await
505+
.unwrap();
506+
507+
assert_eq!(result.rows.len(), 1);
508+
assert_eq!(result.rows[0][0], "1"); // id
509+
assert_eq!(result.rows[0][4], "in_progress"); // status
510+
assert_eq!(result.rows[0][6], "NULL"); // quary_valid_to
511+
512+
// Check that quary_valid_from has the same date as the current date
513+
let current_date: NaiveDateTime = Utc::now().date_naive().into();
514+
let quary_valid_from_str = &result.rows[0][5];
515+
let quary_valid_from_date = quary_valid_from_str.split_whitespace().next().unwrap();
516+
517+
let quary_valid_from =
518+
chrono::NaiveDateTime::parse_from_str(quary_valid_from_date, "%Y-%m-%dT%H:%M:%S%.f%:z")
519+
.unwrap();
520+
assert_eq!(current_date.date(), quary_valid_from.date());
521+
522+
database
523+
.exec(
524+
"
525+
UPDATE analytics.orders
526+
SET order_date = '2099-06-01 00:00:00', status = 'completed'
527+
WHERE order_id = '1'
528+
",
529+
)
530+
.await
531+
.unwrap();
532+
}
533+
534+
// Take updated snapshot
535+
Command::cargo_bin(name)
536+
.unwrap()
537+
.current_dir(project_dir)
538+
.args(vec!["snapshot"])
539+
.assert()
540+
.success();
541+
542+
{
543+
// Assert updated snapshot
544+
let updated_result = database
545+
.query("SELECT order_id, customer_id, order_date, total_amount, status, quary_valid_from, quary_valid_to, quary_scd_id FROM transform.stg_orders_snapshot ORDER BY quary_valid_from")
546+
.await
547+
.unwrap();
548+
549+
assert_eq!(updated_result.rows.len(), 2);
550+
551+
// Check the initial row
552+
assert_eq!(updated_result.rows[0][0], "1"); // id
553+
assert_eq!(updated_result.rows[0][4], "in_progress"); // status
554+
assert_ne!(updated_result.rows[0][6], "NULL"); // quary_valid_to should not be NULL
555+
556+
// Check the updated row
557+
assert_eq!(updated_result.rows[1][0], "1"); // id
558+
assert_eq!(updated_result.rows[1][4], "completed"); // status
559+
assert_eq!(updated_result.rows[1][6], "NULL"); // quary_valid_to should be NULL
560+
561+
// Check that quary_valid_from of the updated row has the same date as the current date
562+
let current_date: NaiveDateTime = Utc::now().date_naive().into();
563+
let updated_quary_valid_from_str = &updated_result.rows[1][5];
564+
let updated_quary_valid_from_date = updated_quary_valid_from_str
565+
.split_whitespace()
566+
.next()
567+
.unwrap();
568+
let updated_quary_valid_from = chrono::NaiveDateTime::parse_from_str(
569+
updated_quary_valid_from_date,
570+
"%Y-%m-%dT%H:%M:%S%.f%:z",
571+
)
572+
.unwrap();
573+
assert_eq!(current_date.date(), updated_quary_valid_from.date());
574+
}
575+
}

rust/core/src/database_duckdb.rs

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,11 @@ impl DatabaseQueryGeneratorDuckDB {
2424
}
2525

2626
fn get_now(&self) -> String {
27-
if let Some(override_now) = &self.override_now {
28-
let datetime: DateTime<Utc> = (*override_now).into();
29-
format!("'{}'", datetime.format("%Y-%m-%dT%H:%M:%SZ"))
30-
} else {
31-
"CURRENT_TIMESTAMP".to_string()
32-
}
27+
let datetime = self
28+
.override_now
29+
.map(|time| -> DateTime<Utc> { time.into() })
30+
.unwrap_or(SystemTime::now().into());
31+
format!("'{}'", datetime.format("%Y-%m-%dT%H:%M:%SZ"))
3332
}
3433
}
3534

@@ -45,7 +44,12 @@ impl DatabaseQueryGenerator for DatabaseQueryGeneratorDuckDB {
4544
templated_select: &str,
4645
unique_key: &str,
4746
strategy: &StrategyType,
47+
table_exists: Option<bool>,
4848
) -> Result<Vec<String>, String> {
49+
assert_eq!(
50+
table_exists, None,
51+
"table_exists is not necessary for DuckDB snapshots."
52+
);
4953
match strategy {
5054
StrategyType::Timestamp(timestamp) => {
5155
let updated_at = &timestamp.updated_at;
@@ -56,7 +60,7 @@ impl DatabaseQueryGenerator for DatabaseQueryGeneratorDuckDB {
5660
SELECT
5761
*,
5862
{now} AS quary_valid_from,
59-
CAST(NULL AS TIMESTAMP) AS quary_valid_to,
63+
CAST(NULL AS TIMESTAMP WITH TIME ZONE) AS quary_valid_to,
6064
MD5(CAST(CONCAT({unique_key}, CAST({updated_at} AS STRING)) AS STRING)) AS quary_scd_id
6165
FROM ({templated_select})
6266
)"
@@ -189,15 +193,17 @@ mod tests {
189193
let expected_datetime: DateTime<Utc> = override_now.into();
190194
let expected_result = format!("'{}'", expected_datetime.format("%Y-%m-%dT%H:%M:%SZ"));
191195
assert_eq!(result, expected_result);
192-
193-
let database = DatabaseQueryGeneratorDuckDB::new(None, None);
194-
let result = database.get_now();
195-
assert_eq!(result, "CURRENT_TIMESTAMP".to_string());
196196
}
197197

198198
#[test]
199199
fn test_generate_snapshot_sql() {
200-
let database = DatabaseQueryGeneratorDuckDB::new(None, None);
200+
let time_override = "2021-01-01T00:00:00Z";
201+
let override_now = DateTime::parse_from_rfc3339(time_override)
202+
.unwrap()
203+
.with_timezone(&Utc);
204+
let system_time = SystemTime::from(override_now);
205+
206+
let database = DatabaseQueryGeneratorDuckDB::new(None, Some(system_time));
201207
let path = "mytable";
202208
let templated_select = "SELECT * FROM mytable";
203209
let unique_key = "id";
@@ -209,9 +215,9 @@ mod tests {
209215
);
210216

211217
let result = database
212-
.generate_snapshot_sql(path, templated_select, unique_key, &strategy)
218+
.generate_snapshot_sql(path, templated_select, unique_key, &strategy, None)
213219
.unwrap();
214220

215-
assert_eq!(result.iter().map(|s| s.as_str()).collect::<Vec<&str>>(), vec!["CREATE TABLE IF NOT EXISTS mytable AS (\n SELECT\n *,\n CURRENT_TIMESTAMP AS quary_valid_from,\n CAST(NULL AS TIMESTAMP) AS quary_valid_to,\n MD5(CAST(CONCAT(id, CAST(updated_at AS STRING)) AS STRING)) AS quary_scd_id\n FROM (SELECT * FROM mytable)\n )", "UPDATE mytable AS target\n SET quary_valid_to = source.quary_valid_from\n FROM (\n SELECT\n *,\n CURRENT_TIMESTAMP AS quary_valid_from,\n MD5(CAST(CONCAT(id, CAST(updated_at AS STRING)) AS STRING)) AS quary_scd_id\n FROM (SELECT * FROM mytable)\n ) AS source\n WHERE target.id = source.id\n AND target.quary_valid_to IS NULL\n AND CAST(source.updated_at AS TIMESTAMP) > CAST(target.updated_at AS TIMESTAMP)", "INSERT INTO mytable\n SELECT\n *,\n CURRENT_TIMESTAMP AS quary_valid_from,\n NULL AS quary_valid_to,\n MD5(CAST(CONCAT(id, CAST(updated_at AS STRING)) AS STRING)) AS quary_scd_id\n FROM (SELECT * FROM mytable) AS source\n WHERE NOT EXISTS (\n SELECT 1\n FROM mytable AS target\n WHERE target.quary_scd_id = MD5(CAST(CONCAT(source.id, CAST(source.updated_at AS STRING)) AS STRING))\n )"]);
221+
assert_eq!(result.iter().map(|s| s.as_str()).collect::<Vec<&str>>(), vec!["CREATE TABLE IF NOT EXISTS mytable AS (\n SELECT\n *,\n '2021-01-01T00:00:00Z' AS quary_valid_from,\n CAST(NULL AS TIMESTAMP WITH TIME ZONE) AS quary_valid_to,\n MD5(CAST(CONCAT(id, CAST(updated_at AS STRING)) AS STRING)) AS quary_scd_id\n FROM (SELECT * FROM mytable)\n )", "UPDATE mytable AS target\n SET quary_valid_to = source.quary_valid_from\n FROM (\n SELECT\n *,\n '2021-01-01T00:00:00Z' AS quary_valid_from,\n MD5(CAST(CONCAT(id, CAST(updated_at AS STRING)) AS STRING)) AS quary_scd_id\n FROM (SELECT * FROM mytable)\n ) AS source\n WHERE target.id = source.id\n AND target.quary_valid_to IS NULL\n AND CAST(source.updated_at AS TIMESTAMP) > CAST(target.updated_at AS TIMESTAMP)", "INSERT INTO mytable\n SELECT\n *,\n '2021-01-01T00:00:00Z' AS quary_valid_from,\n NULL AS quary_valid_to,\n MD5(CAST(CONCAT(id, CAST(updated_at AS STRING)) AS STRING)) AS quary_scd_id\n FROM (SELECT * FROM mytable) AS source\n WHERE NOT EXISTS (\n SELECT 1\n FROM mytable AS target\n WHERE target.quary_scd_id = MD5(CAST(CONCAT(source.id, CAST(source.updated_at AS STRING)) AS STRING))\n )"]);
216222
}
217223
}

0 commit comments

Comments
 (0)