Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from sqlbuild.orchestration import calendar_schedule, job

orders = job("orders", description="Build order reporting.", select="orders")
orders = job("order_reporting", description="Build order reporting.", select="orders")


@calendar_schedule(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from sqlbuild.orchestration import job

orders = job(
"orders",
"refresh_orders",
description="Refresh orders.",
select="orders",
settings={"load_sources": False, "reload": True},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from sqlbuild.orchestration import job

orders = job(
"orders",
"refresh_orders",
description="Refresh orders.",
select="orders",
settings={"select": "customers"},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""A job may not reuse a model's name: jobs share the project's one resource namespace."""

from sqlbuild.orchestration import job

orders = job("orders", description="Build orders.", select="orders")
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"description": "Three minutely cron jobs start ten hours back with the default latest catch-up. Two installed schedulers catch up while being SIGKILLed at seeded random moments; every lapse of a killed leader's lease is simulated. After a final uninterrupted scheduler, each trigger has ticked its latest due minute exactly once and nothing off schedule, each tick requests or is covered by exactly one run, and every requested run has one request event.",
"jobs": [
"orders",
"customers",
"inventory"
"refresh_orders",
"refresh_customers",
"refresh_inventory"
],
"backlog_minutes": 600,
"rounds": 16,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,73 +1,149 @@
//! Python's `_validate_unique_project_resource_names`: one namespace across resource kinds.
//! One namespace across resource kinds, orchestration declarations included.

use std::collections::{HashMap, HashSet};

use crate::project_validation::_helpers::conflicts::{conflict, schema_seeds};
use crate::project_validation::_helpers::identities::block_or_file_name;
use crate::project_validation::models::{NodeKind, ProjectFacts, ValidationFailure};
use crate::project_validation::constants::ORCHESTRATION_RENAME_ADVICE;
use crate::project_validation::models::{
DeclaredName, NamedPath, NodeKind, ProjectFacts, ValidationFailure,
};

/// Reject the first resource name declared twice across models, sources, seeds and the rest.
pub(crate) fn validate_unique_project_resource_names(
facts: &ProjectFacts,
) -> Result<(), ValidationFailure> {
let mut seen: HashMap<String, (String, String)> = HashMap::new();
let mut declare = |name: &str, kind: &str, path: &str| -> Result<(), ValidationFailure> {
if let Some((existing_kind, existing_path)) = seen.get(name) {
return Err(conflict(format!(
"Project resource name '{name}' is declared as both {existing_kind} in \
{existing_path} and {kind} in {path}; model, source, seed, function, loader, \
task, asset, check, provider, hook, unit test, and scenario names must be \
globally unique"
)));
let seeds: Vec<NamedPath> = schema_seeds(facts);
let mut seen: HashMap<&str, DeclaredName<'_>> = HashMap::new();
for declared in declarations(facts, &seeds) {
if let Some(first) = seen.get(declared.name) {
return Err(name_conflict(first, &declared));
}
seen.insert(name.to_owned(), (kind.to_owned(), path.to_owned()));
Ok(())
};
seen.insert(declared.name, declared);
}
Ok(())
}

/// Every declared name in the order conflicts are reported.
fn declarations<'a>(facts: &'a ProjectFacts, seeds: &'a [NamedPath]) -> Vec<DeclaredName<'a>> {
let mut declared: Vec<DeclaredName<'a>> = Vec::new();
for model in &facts.models {
declare(&model.file.name, "model", &model.file.path)?;
declared.push(DeclaredName {
name: &model.file.name,
kind: "model",
path: &model.file.path,
orchestration: false,
});
}
let mut managed_sources: HashSet<&str> = HashSet::new();
for file in &facts.sources {
for entry in &file.entries {
if entry.managed {
managed_sources.insert(&entry.name);
}
declare(&entry.name, "source", &file.path)?;
declared.push(DeclaredName {
name: &entry.name,
kind: "source",
path: &file.path,
orchestration: false,
});
}
}
for seed in schema_seeds(facts) {
declare(&seed.name, "seed", &seed.path)?;
for seed in seeds {
declared.push(DeclaredName {
name: &seed.name,
kind: "seed",
path: &seed.path,
orchestration: false,
});
}
for function in facts.sql_functions.iter().chain(&facts.python_functions) {
declare(&function.name, "function", &function.path)?;
declared.push(DeclaredName {
name: &function.name,
kind: "function",
path: &function.path,
orchestration: false,
});
}
for node in &facts.nodes {
if node.kind == NodeKind::Loader && managed_sources.contains(node.name.as_str()) {
continue;
}
declare(&node.name, node.kind.label(), &node.path)?;
declared.push(DeclaredName {
name: &node.name,
kind: node.kind.label(),
path: &node.path,
orchestration: false,
});
}
for provider in &facts.providers {
declare(&provider.name, "provider", &provider.path)?;
declared.push(DeclaredName {
name: &provider.name,
kind: "provider",
path: &provider.path,
orchestration: false,
});
}
for hook in &facts.sql_hooks {
declare(&hook.name, "sql hook", &hook.path)?;
declared.push(DeclaredName {
name: &hook.name,
kind: "sql hook",
path: &hook.path,
orchestration: false,
});
}
for hook in &facts.hook_functions {
declare(&hook.name, "python hook", &hook.path)?;
declared.push(DeclaredName {
name: &hook.name,
kind: "python hook",
path: &hook.path,
orchestration: false,
});
}
for scenario in &facts.scenarios {
declare(&scenario.name, "scenario", &scenario.path)?;
declared.push(DeclaredName {
name: &scenario.name,
kind: "scenario",
path: &scenario.path,
orchestration: false,
});
}
let mut test_names: HashSet<&str> = HashSet::new();
for test in &facts.tests {
for block in &test.blocks {
let name: &str = block_or_file_name(block.name.as_deref(), &test.file.name);
if !test_names.insert(name) {
continue;
if test_names.insert(name) {
declared.push(DeclaredName {
name,
kind: "unit test",
path: &test.file.path,
orchestration: false,
});
}
declare(name, "unit test", &test.file.path)?;
}
}
Ok(())
for declaration in &facts.orchestration {
declared.push(DeclaredName {
name: &declaration.name,
kind: declaration.kind,
path: &declaration.path,
orchestration: true,
});
}
declared
}

/// Name both declarations and say how to rename one of them.
fn name_conflict(first: &DeclaredName<'_>, second: &DeclaredName<'_>) -> ValidationFailure {
let advice: &str = if first.orchestration || second.orchestration {
ORCHESTRATION_RENAME_ADVICE
} else {
""
};
conflict(format!(
"Project resource name '{}' is declared as both {} in {} and {} in {}; model, source, seed, function, loader, task, asset, check, provider, \
hook, unit test, scenario, job, job group, sensor, and calendar schedule names must be \
globally unique; rename one of them{advice}",
first.name, first.kind, first.path, second.kind, second.path
))
}
4 changes: 4 additions & 0 deletions crates/sqlbuild-discovery/src/project_validation/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ pub(crate) const RESERVED_MODEL_NAMES: [&str; 1] = ["_chain_"];
pub(crate) const INITIAL_SEED_READ_BYTES: u64 = 64 * 1024;
/// A header and two data rows: enough to know a seed holds more than one data row.
pub(crate) const RECORDS_FOR_MANY_ROWS: usize = 3;

/// How a colliding orchestration declaration is renamed, added to its name conflict.
pub(crate) const ORCHESTRATION_RENAME_ADVICE: &str = " (a job, job group or sensor takes its name \
from its first argument, and a calendar schedule from the function it decorates)";
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod at_most_one_csv_data_row;
pub mod validate_orchestration_names;
pub mod validate_project_inputs;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//! Orchestration declarations share the project's one resource namespace.

use crate::project_validation::_helpers::resource_names::validate_unique_project_resource_names;
use crate::project_validation::models::{ProjectFacts, ValidationFailure};

/// Reject the first name a job, group, sensor or calendar schedule shares with anything else.
pub fn validate_orchestration_names(facts: &ProjectFacts) -> Result<(), ValidationFailure> {
validate_unique_project_resource_names(facts)
}
20 changes: 20 additions & 0 deletions crates/sqlbuild-discovery/src/project_validation/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,26 @@ pub struct ProjectFacts {
pub project_model_paths: Option<Vec<String>>,
/// Python's `csv.field_size_limit()`, or `None` for its default.
pub csv_field_limit: Option<usize>,
/// Jobs, job groups, sensors and calendar schedules, known only when a location publishes.
pub orchestration: Vec<OrchestrationNameFact>,
}

/// One name in the project's resource namespace, as a name conflict reports it.
#[derive(Debug)]
pub(crate) struct DeclaredName<'a> {
pub(crate) name: &'a str,
pub(crate) kind: &'a str,
pub(crate) path: &'a str,
/// A job, job group, sensor or calendar schedule, whose conflicts say how it is named.
pub(crate) orchestration: bool,
}

/// A named orchestration declaration: its name, its kind as conflicts word it, and its module.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OrchestrationNameFact {
pub name: String,
pub kind: &'static str,
pub path: String,
}

/// Why discovered inputs are rejected: a discovery error, or a seed CSV Python cannot read.
Expand Down
2 changes: 2 additions & 0 deletions crates/sqlbuild-discovery/src/project_validation/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ mod csv_header;
mod helpers;
#[path = "tests/test_types.rs"]
mod test_types;
#[path = "tests/test_validate_orchestration_names.rs"]
mod validate_orchestration_names;
#[path = "tests/test_validate_project_inputs.rs"]
mod validate_project_inputs;
27 changes: 25 additions & 2 deletions crates/sqlbuild-discovery/src/project_validation/tests/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use crate::models::FailureKind;
use crate::project_validation::_helpers::csv_header::csv_header;
use crate::project_validation::main::validate_project_inputs::validate_project_inputs;
use crate::project_validation::models::{
CsvSettings, DependencyFact, ModelFact, NamedPath, NodeFact, NodeKind, ProjectFacts,
SchemaFileFact, SeedEntryFact, SeedFileFact, ValidationFailure,
CsvSettings, DependencyFact, ModelFact, NamedPath, NodeFact, NodeKind, OrchestrationNameFact,
ProjectFacts, SchemaFileFact, SeedEntryFact, SeedFileFact, ValidationFailure,
};
use crate::project_validation::tests::test_types::failure;

/// The header the reader returns, or its error's message.
pub(super) fn read_header(
Expand Down Expand Up @@ -88,3 +90,24 @@ pub(super) fn path_default_facts(paths: &[&str], keys: &[&str]) -> ProjectFacts
..ProjectFacts::default()
}
}

/// An orchestration declaration named `name`, of `kind`, in one module.
pub(super) fn declared(name: &str, kind: &'static str) -> OrchestrationNameFact {
OrchestrationNameFact {
name: name.to_owned(),
kind,
path: "orchestration/daily.py".to_owned(),
}
}

/// A global unique-name conflict that `message` begins.
pub(super) fn collision(message: &str, advice: &str) -> Result<(), String> {
failure(
FailureKind::Conflict,
&format!(
"{message}; model, source, seed, function, loader, task, asset, check, provider, \
hook, unit test, scenario, job, job group, sensor, and calendar schedule names must \
be globally unique; rename one of them{advice}"
),
)
}
Loading
Loading