Skip to content
Closed
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ ruff_text_size = "=0.0.4"
rustc-hash = "2"
# Recall's local store: SQLite compiled in, so every build has the same engine version.
rusqlite = { version = "0.37", features = ["bundled"] }
rustix = { version = "1.1.4", default-features = false, features = ["fs", "std"] }
rustix = { version = "1.1.4", default-features = false, features = ["fs", "std", "termios"] }
saphyr-parser = "0.1"
regex = "1"
serde = { version = "1", features = ["derive"] }
Expand Down
4 changes: 3 additions & 1 deletion crates/sqlbuild-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "sqlbuild-cli"
version.workspace = true
description = "The native `sqb` executable: reused compiles served without Python, every other command handed to the Python CLI."
description = "The native `sqb` executable: reused compiles and Python-free commands served without Python, every other command handed to the Python CLI."
edition.workspace = true
license-file = "LICENSE"
publish.workspace = true
Expand All @@ -21,7 +21,9 @@ getrandom.workspace = true
serde.workspace = true
serde_json.workspace = true
sqlbuild-cache.workspace = true
sqlbuild-config.workspace = true
sqlbuild-core.workspace = true
sqlbuild-rules.workspace = true

[target.'cfg(unix)'.dependencies]
rustix.workspace = true
Expand Down
5 changes: 5 additions & 0 deletions crates/sqlbuild-cli/src/compute_logs/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,8 @@ pub const DEFAULT_LOG_DIRECTORY: &str = "logs";
pub const MONTHS_PER_YEAR: i64 = 12;
pub const MAX_DAY_OF_MONTH: i64 = 31;
pub const HOURS_PER_DAY: i64 = 24;
/// The debug record Python's capture logs when its streams could not be finalized.
pub const CAPTURE_FAILURE_LOGGER: &str = "sqlbuild.cli.compute_logs";
pub const CAPTURE_FAILURE_MESSAGE: &str = "local compute log capture unavailable";
pub const DIAGNOSTIC_PRODUCER: &str = "sqlbuild";
pub const DIAGNOSTIC_SCHEMA_VERSION: i64 = 1;
72 changes: 72 additions & 0 deletions crates/sqlbuild-cli/src/compute_logs/main/abandon_capture.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//! Leave a capture incomplete after its final flush failed, as Python's capture cleanup does.

use std::io::{self, Write};
use std::time::SystemTime;

use sqlbuild_core::json::main::dumps::dumps;
use sqlbuild_core::json::models::{JsonDialect, JsonInteger, JsonValue, OrjsonOptions};

use crate::compute_logs::_helpers::timestamps::{epoch_microseconds, utc_timestamp};
use crate::compute_logs::constants::{
CAPTURE_FAILURE_LOGGER, CAPTURE_FAILURE_MESSAGE, DIAGNOSTIC_PRODUCER, DIAGNOSTIC_SCHEMA_VERSION,
};
use crate::compute_logs::models::{CaptureEnd, OpenCapture};

fn text(value: &str) -> JsonValue {
JsonValue::String(value.to_owned())
}

/// The debug record `log_compute_capture_failure` writes to the capture's diagnostics.
fn capture_failure_record(invocation_id: &str, occurred_at: SystemTime) -> String {
let record: JsonValue = JsonValue::Object(vec![
("fields".to_owned(), JsonValue::Object(Vec::new())),
("invocation_id".to_owned(), text(invocation_id)),
("log_stream_id".to_owned(), JsonValue::Null),
("logger".to_owned(), text(CAPTURE_FAILURE_LOGGER)),
("message".to_owned(), text(CAPTURE_FAILURE_MESSAGE)),
(
"occurred_at".to_owned(),
text(&utc_timestamp(epoch_microseconds(occurred_at))),
),
("operation_id".to_owned(), JsonValue::Null),
("producer".to_owned(), text(DIAGNOSTIC_PRODUCER)),
(
"producer_version".to_owned(),
text(env!("CARGO_PKG_VERSION")),
),
("resource_attempt_id".to_owned(), JsonValue::Null),
("resource_id".to_owned(), JsonValue::Null),
("run_id".to_owned(), JsonValue::Null),
(
"schema_version".to_owned(),
JsonValue::Integer(JsonInteger::from(DIAGNOSTIC_SCHEMA_VERSION)),
),
("severity".to_owned(), text("debug")),
("source".to_owned(), text("python_logging")),
("statement_id".to_owned(), JsonValue::Null),
]);
let options: OrjsonOptions = OrjsonOptions {
sort_keys: true,
..OrjsonOptions::default()
};
dumps(&record, &JsonDialect::Orjson(options)).unwrap_or_default() + "\n"
}

/// Append the streams the command wrote and the capture failure, without completing it.
pub fn abandon_capture(capture: OpenCapture, end: &CaptureEnd) -> io::Result<()> {
let OpenCapture {
invocation_id,
mut streams,
..
} = capture;
let [stdout, stderr, diagnostics] = streams.as_mut_slice() else {
return Err(io::Error::other("capture streams are not open"));
};
stdout.write_all(&end.stdout)?;
stderr.write_all(&end.stderr)?;
diagnostics.write_all(capture_failure_record(&invocation_id, SystemTime::now()).as_bytes())?;
for stream in [stdout, stderr, diagnostics] {
stream.flush()?;
}
Ok(())
}
1 change: 1 addition & 0 deletions crates/sqlbuild-cli/src/compute_logs/main/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod abandon_capture;
pub mod capture_inventory;
pub mod capture_is_complete;
pub mod check_completion;
Expand Down
17 changes: 12 additions & 5 deletions crates/sqlbuild-cli/src/launcher/_helpers/dispatch.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Decide how one invocation runs: natively, from a launch record, or in the Python CLI.
//! Decide how one invocation runs: from a launch record, natively, or in the Python CLI.

use std::ffi::OsString;
use std::path::{Path, PathBuf};
Expand All @@ -10,6 +10,7 @@ use crate::launcher::_helpers::messages::{print_version, report_missing_python_c
use crate::launcher::constants::{
HANDOFF_FAILURE_EXIT_CODE, LAUNCH_RECORD_OPTION, VERSION_OPTIONS,
};
use crate::native_commands::main::run_native_invocation::run_native_invocation;
use crate::replay::main::serve_reused_compile::serve_reused_compile;

/// The launch record path and key for these arguments, when the directory is known.
Expand Down Expand Up @@ -43,10 +44,16 @@ pub(crate) fn launch(arguments: Vec<OsString>, started: (Instant, SystemTime)) -
return HANDOFF_FAILURE_EXIT_CODE;
};
let mut handed: Vec<OsString> = Vec::with_capacity(arguments.len() + 1);
if let Some((path, key)) = launch_record(&program, &script, &arguments) {
if let Some(code) = serve_reused_compile(&path, &key, started) {
return exit_code(code);
}
let record: Option<(PathBuf, String)> = launch_record(&program, &script, &arguments);
if let Some((path, key)) = &record
&& let Some(code) = serve_reused_compile(path, key, started)
{
return exit_code(code);
}
if let Ok(code) = run_native_invocation(&arguments, &script) {
return code;
}
if let Some((path, _)) = record {
let mut option = OsString::from(LAUNCH_RECORD_OPTION);
option.push(path.as_os_str());
handed.push(option);
Expand Down
11 changes: 8 additions & 3 deletions crates/sqlbuild-cli/src/launcher/_helpers/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@
use std::io::{self, Write};
use std::path::Path;

use crate::launcher::constants::{PROGRAM_NAME, PYTHON_CLI_SCRIPT};
use crate::launcher::constants::{PROGRAM_NAME, PROGRAM_VERSION, PYTHON_CLI_SCRIPT};

/// argparse's `--version` line: `sqb <version>` on stdout.
/// argparse's `--version` line: `sqb <version>`.
pub(crate) fn version_line() -> String {
format!("{PROGRAM_NAME} {PROGRAM_VERSION}\n")
}

/// Print the `--version` line on stdout.
pub(crate) fn print_version() {
let _ = writeln!(io::stdout(), "{PROGRAM_NAME} {}", env!("CARGO_PKG_VERSION"));
let _ = io::stdout().write_all(version_line().as_bytes());
}

/// The Python CLI script is not installed beside this program.
Expand Down
2 changes: 2 additions & 0 deletions crates/sqlbuild-cli/src/launcher/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ pub const LAUNCH_RECORD_OPTION: &str = "--sqlbuild-launch-record=";
/// The program name argparse reports, fixed by the Python parser.
pub const PROGRAM_NAME: &str = "sqb";
pub const VERSION_OPTIONS: [&str; 2] = ["--version", "-V"];
/// The version `--version` prints, alone or among other options; the wheels share it.
pub const PROGRAM_VERSION: &str = env!("CARGO_PKG_VERSION");
/// Global options naming the project directory, exactly as the Python parser spells them.
pub const PROJECT_DIR_OPTIONS: [&str; 2] = ["--project-dir", "--sqb-project-dir"];
/// Prefixes argparse could expand to a project directory option.
Expand Down
3 changes: 2 additions & 1 deletion crates/sqlbuild-cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! The native `sqb` executable: replays reused compiles and hands other invocations to Python.
//! The native `sqb` executable: replays reused compiles, runs Python-free commands and hands the rest to Python.

#![forbid(unsafe_code)]

Expand All @@ -7,4 +7,5 @@ pub mod compute_logs;
pub mod diff_report;
pub mod dispatch;
pub mod launcher;
pub mod native_commands;
pub mod replay;
120 changes: 120 additions & 0 deletions crates/sqlbuild-cli/src/native_commands/_helpers/argv_host.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
//! The parse's interpreter side without an interpreter: Python's `int`, `repr` and the version.

use sqlbuild_core::text::main::decimal_digit_value::decimal_digit_value;
use sqlbuild_core::text::main::python_isspace::python_isspace;
use sqlbuild_core::text::main::python_str_repr::python_str_repr;

use crate::argv::main::cli_namespace_defaults::cli_namespace_defaults;
use crate::argv::models::{NamespaceDefault, TypeFailure, Value};
use crate::argv::types::ArgvHost;
use crate::launcher::constants::PROGRAM_VERSION;
use crate::native_commands::constants::{
DIGIT_SEPARATOR, INT_ASCII_SPACES, INT_CONVERTER, NEGATIVE_SIGN, POSITIVE_INTEGER_CONVERTER,
POSITIVE_INTEGER_MESSAGE, POSITIVE_SIGN, PYTHON_INT_MAX_STR_DIGITS, ZERO_TEXT,
};

/// The ASCII text CPython parses: other decimal digits become ASCII, other spaces a space.
fn ascii_digits_and_spaces(text: &str) -> Option<String> {
text.chars()
.map(|character| {
if character.is_ascii() {
Some(character)
} else if python_isspace(character) {
Some(' ')
} else {
decimal_digit_value(character).and_then(|digit| char::from_digit(digit, 10))
}
})
.collect()
}

/// The canonical decimal text of Python's `int(text)`, or `None` where it raises `ValueError`.
pub(crate) fn python_int(text: &str) -> Option<String> {
let ascii: String = ascii_digits_and_spaces(text)?;
let trimmed: &str = ascii.trim_matches(|character: char| INT_ASCII_SPACES.contains(character));
let (negative, body): (bool, &str) = match trimmed.strip_prefix(NEGATIVE_SIGN) {
Some(body) => (true, body),
None => (
false,
trimmed.strip_prefix(POSITIVE_SIGN).unwrap_or(trimmed),
),
};
let mut digits: String = String::with_capacity(body.len());
let mut after_digit: bool = false;
for character in body.chars() {
if character.is_ascii_digit() {
digits.push(character);
after_digit = true;
} else if character == DIGIT_SEPARATOR && after_digit {
after_digit = false;
} else {
return None;
}
}
if !after_digit || digits.len() > PYTHON_INT_MAX_STR_DIGITS {
return None;
}
let significant: &str = digits.trim_start_matches('0');
Some(match (significant.is_empty(), negative) {
(true, _) => ZERO_TEXT.to_owned(),
(false, true) => format!("{NEGATIVE_SIGN}{significant}"),
(false, false) => significant.to_owned(),
})
}

/// Converts what it can as Python would and records any converter only Python can apply.
#[derive(Debug, Default)]
pub(crate) struct NativeArgvHost {
/// Whether the parse met a converter, such as `--vars`, that needs the interpreter.
pub(crate) needs_python: bool,
}

impl ArgvHost for NativeArgvHost {
fn convert(&mut self, converter: &str, text: &str) -> Result<Value, TypeFailure> {
match converter {
INT_CONVERTER => python_int(text).map(Value::Int).ok_or(TypeFailure::Invalid),
POSITIVE_INTEGER_CONVERTER => {
let number: String = python_int(text).ok_or(TypeFailure::Invalid)?;
if number == ZERO_TEXT || number.starts_with(NEGATIVE_SIGN) {
return Err(TypeFailure::ArgumentType(
POSITIVE_INTEGER_MESSAGE.to_owned(),
));
}
Ok(Value::Int(number))
}
_ => {
self.needs_python = true;
Ok(Value::None)
}
}
}

fn repr(&self, text: &str) -> String {
python_str_repr(text)
}

fn program_version(&mut self) -> String {
PROGRAM_VERSION.to_owned()
}
}

/// The parse's starting namespace: every `CliNamespace` default.
pub(crate) fn namespace_defaults() -> Vec<(String, Value)> {
cli_namespace_defaults()
.iter()
.map(|(name, default)| {
let value: Value = match default {
NamespaceDefault::None => Value::None,
NamespaceDefault::Bool(flag) => Value::Bool(*flag),
NamespaceDefault::Int(number) => Value::Int(number.to_string()),
NamespaceDefault::Text(text) => Value::Str((*text).to_owned()),
NamespaceDefault::EmptyList => Value::List(Vec::new()),
NamespaceDefault::EmptyDict => Value::EmptyDict,
NamespaceDefault::CompileLineageFast | NamespaceDefault::ColumnLineageRich => {
Value::Host(0)
}
};
((*name).to_owned(), value)
})
.collect()
}
Loading
Loading