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
20 changes: 17 additions & 3 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ on:
branches: [dev, main]
pull_request:
branches: [dev, main]
# Once a night, and by hand, the same check runs with the slow work added.
schedule:
- cron: '30 3 * * *'
workflow_dispatch:

permissions:
contents: read
Expand All @@ -20,8 +24,9 @@ env:
# The size of the shipped code, rounded up to 500 lines, and the number of tests, rounded up
# to 10. A change that crosses either number edits it here, in the same change, where its
# reader sees that the code grew or shrank. Neither ever passes 150,000 lines or 1,000 tests.
SHIPPED_LINES: 15500
TESTS: 260
SHIPPED_LINES: 21000
TESTS: 380
TEST_LINES: 38000

jobs:
lint:
Expand Down Expand Up @@ -60,18 +65,27 @@ jobs:
with:
tool: cargo-nextest@0.9.138
- run: cargo nextest run --manifest-path v2/Cargo.toml --workspace --locked --no-tests=pass
- name: Slow work, in the nightly run only
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
run: cargo nextest run --manifest-path v2/Cargo.toml --workspace --locked --profile nightly
- run: cargo test --manifest-path v2/Cargo.toml --workspace --locked --doc
- name: The storage library's own tests
run: cargo test --manifest-path v2/redb/Cargo.toml --locked --lib --tests
# A test is one the runner lists, or one script file. Counted on Linux, where every test runs.
- name: Tests are at most the stated number and within 10 of it
if: matrix.os == 'ubuntu-latest'
run: |
tests=$(( $(cargo nextest list --manifest-path v2/Cargo.toml --workspace --locked -T oneline | wc -l) + $(find v2/contextdb/tests/sql -name '*.sql' | wc -l) ))
tests=$(( $(cargo nextest list --manifest-path v2/Cargo.toml --workspace --locked --ignore-default-filter -T oneline | wc -l) + $(find v2/contextdb/tests/sql -name '*.sql' | wc -l) ))
echo "$tests tests; the stated number is $TESTS"
test "$TESTS" -le 1000
test "$tests" -le "$TESTS"
test "$tests" -gt "$((TESTS - 10))"
- name: Test lines are at most the stated number
if: matrix.os == 'ubuntu-latest'
run: |
lines=$(find v2/*/tests v2/*/src -type f \( -path '*/tests/*' -o -name tests.rs \) -exec cat {} + | wc -l)
echo "$lines test lines; the stated number is $TEST_LINES"
test "$lines" -le "$TEST_LINES"

minimum-rust:
runs-on: ubuntu-latest
Expand Down
13 changes: 9 additions & 4 deletions v2/.config/nextest.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# A test that runs past its limit is stopped and fails by name. Nothing is retried.
# The pull-request check (the default profile) runs every test except the million-row load,
# which is slow work and runs every night in the `nightly` profile.
[profile.default]
retries = 0
slow-timeout = { period = "30s", terminate-after = 2 }
default-filter = 'not test(=a_million_rows_load_and_scan_under_64_mib)'

# The million-row load is the one test that needs longer.
[[profile.default.overrides]]
filter = 'test(a_million_rows_load_and_scan_under_64_mib)'
slow-timeout = { period = "120s", terminate-after = 5 }
# The nightly profile runs only the million-row load. Its limit is a hang detector, well clear
# of the slowest hosted run seen (428 seconds).
[profile.nightly]
retries = 0
default-filter = 'test(=a_million_rows_load_and_scan_under_64_mib)'
slow-timeout = { period = "300s", terminate-after = 6 }
36 changes: 27 additions & 9 deletions v2/AGENTS.md

Large diffs are not rendered by default.

81 changes: 46 additions & 35 deletions v2/contextdb-cli/src/output.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
//! Everything the shell prints: results on the output, refusals on the error stream, each as
//! text for a person or, under `--json`, as one JSON document a line.
//! text for a person or, under `--json`, as one JSON document a line. A result that cannot be
//! written is handed back to the shell as the failure it is.

use std::io::{Result, Write, stdout};

use contextdb::serde_json::{Map, Value as Json, json};
use contextdb::{Error, QueryResult, Value};
Expand All @@ -12,9 +15,9 @@ pub struct Output {

impl Output {
/// The result of one statement: the rows of a SELECT, or how many rows a write changed.
pub fn result(self, result: &QueryResult) {
pub fn result(self, result: &QueryResult) -> Result<()> {
if result.columns().is_empty() {
self.rows_affected(result.rows_affected());
self.rows_affected(result.rows_affected())
} else if self.json {
// A row is written key by key so that its keys come in the order of `columns`.
let mut rows = String::new();
Expand All @@ -31,81 +34,81 @@ impl Output {
}
rows.push('}');
}
println!(
writeln!(
stdout(),
"{{\"result\":{{\"columns\":{},\"rows\":[{rows}]}}}}",
json!(result.columns())
);
)
} else {
print!("{}", table(result.columns(), result.rows()));
write!(stdout(), "{}", table(result.columns(), result.rows()))
}
}

/// The rows of an inspection statement: one `name: value` to a line, with an empty line
/// between rows.
pub fn fields(self, result: &QueryResult) {
pub fn fields(self, result: &QueryResult) -> Result<()> {
if self.json {
return self.result(result);
}
for (at, row) in result.rows().iter().enumerate() {
if at > 0 {
println!();
writeln!(stdout())?;
}
for (name, value) in result.columns().iter().zip(row) {
println!("{name}: {}", text(value));
writeln!(stdout(), "{name}: {}", text(value))?;
}
}
Ok(())
}

/// What `.explain` answers: its lines.
pub fn explain(self, lines: &str) {
pub fn explain(self, lines: &str) -> Result<()> {
if self.json {
let lines: Vec<&str> = lines.lines().collect();
println!("{}", json!({ "explain": { "lines": lines } }));
writeln!(stdout(), "{}", json!({ "explain": { "lines": lines } }))
} else {
println!("{lines}");
writeln!(stdout(), "{lines}")
}
}

/// A statement that returns no rows succeeded and changed this many.
pub fn rows_affected(self, rows: u64) {
pub fn rows_affected(self, rows: u64) -> Result<()> {
if self.json {
println!("{}", json!({ "rows_affected": rows }));
writeln!(stdout(), "{}", json!({ "rows_affected": rows }))
} else {
println!("ok (rows_affected={rows})");
writeln!(stdout(), "ok (rows_affected={rows})")
}
}

/// What `.tables` answers: the table names, one a line.
pub fn tables(self, names: &[String]) {
pub fn tables(self, names: &[String]) -> Result<()> {
if self.json {
println!("{}", json!({ "tables": { "items": names } }));
writeln!(stdout(), "{}", json!({ "tables": { "items": names } }))
} else {
for name in names {
println!("{name}");
}
names
.iter()
.try_for_each(|name| writeln!(stdout(), "{name}"))
}
}

/// What `.schema` answers: the table's declaration as SQL.
pub fn schema(self, table: &str, declaration: &str) {
pub fn schema(self, table: &str, declaration: &str) -> Result<()> {
if self.json {
println!(
"{}",
json!({ "schema": { "table": table, "ddl": declaration } })
);
let schema = json!({ "schema": { "table": table, "ddl": declaration } });
writeln!(stdout(), "{schema}")
} else {
println!("{}", declaration.trim_end());
writeln!(stdout(), "{}", declaration.trim_end())
}
}

/// What `.help` answers.
pub fn help(self, lines: &[&str]) {
pub fn help(self, lines: &[&str]) -> Result<()> {
if self.json {
println!("{}", json!({ "help": lines }));
writeln!(stdout(), "{}", json!({ "help": lines }))
} else {
for line in lines {
println!("{line}");
}
lines
.iter()
.try_for_each(|line| writeln!(stdout(), "{line}"))
}
}

Expand Down Expand Up @@ -157,16 +160,24 @@ impl Output {
self.refused(class, &message, Some(detail));
}

/// A warning: the statement still answers and the run goes on, as in Postgres.
pub fn warning(self, message: &str) {
eprintln!("warning: {message}");
}

/// A refusal of the shell's own: `class` is `usage` for a command line or a shell command
/// it cannot act on, `sql` for a statement.
pub fn refusal(self, class: &str, message: &str) {
self.refused(class, message, None);
}

/// A line that starts like a shell command and is not one. For a person it is a plain line
/// with no `Error:` before it.
pub fn unknown_command(self, line: &str) {
let message = format!("Unknown command: {line}. Type \\? for help.");
/// A line that starts like a shell command and is not one, or a command used wrongly, with
/// its `usage`. For a person it is a plain line with no `Error:` before it.
pub fn unknown_command(self, line: &str, usage: Option<&str>) {
let message = match usage {
Some(usage) => format!("Usage: {usage}"),
None => format!("Unknown command: {line}. Type \\? for help."),
};
if self.json {
self.refused("usage", &message, None);
} else {
Expand Down
Loading
Loading