#![cfg(feature = "serde")]
use std::path::{Path, PathBuf};
use serde_json::{Map, Value, json};
use squonk::ast::dialect::{SupportEvidence, SupportTier};
use squonk::bindings::{
BindingToken, BindingTokenKind, BindingTrivia, DialectInfo, KeywordSymbol, ParseDiagnostic,
ParseDocument, RecoveredDocument, ResolverMetadata, SourceSpan, TokenizeDocument,
WIRE_SCHEMA_VERSION,
};
use squonk::{BuiltinDialect, Parsed, parse, parse_recovering_builtin};
fn snapshot_path() -> PathBuf {
schema_dir().join(format!("wire-schema.v{WIRE_SCHEMA_VERSION}.json"))
}
fn compat_baseline_path() -> PathBuf {
schema_dir().join("compat").join("parsed.baseline.json")
}
fn schema_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../release/schema")
}
fn minimal_resolver() -> ResolverMetadata {
ResolverMetadata {
dynamic_base: 3,
keyword_symbols: vec![
KeywordSymbol {
symbol: 1,
text: "select",
},
KeywordSymbol {
symbol: 2,
text: "from",
},
],
}
}
fn ast_sample() -> Parsed {
parse("SELECT a + 1 AS n, b FROM t WHERE a IN (1, 2) ORDER BY b").expect("sample parses")
}
fn canonical_snapshot() -> String {
let ast = ast_sample();
let wrapped = parse("SELECT 1 FROM t").expect("wrapper sample parses");
let recovered = parse_recovering_builtin("SELECT 1; SELECT FROM t", BuiltinDialect::Ansi)
.expect("recovering parse produces a partial tree");
let parse_document = ParseDocument {
dialect: "ansi",
parsed: &wrapped,
trivia: Some(vec![BindingTrivia {
kind: "Whitespace",
span: SourceSpan { start: 8, end: 9 },
text: " ".to_owned(),
}]),
resolver: minimal_resolver(),
};
let recovered_document = RecoveredDocument {
parsed: ParseDocument {
dialect: "ansi",
parsed: recovered.parsed(),
trivia: None,
resolver: minimal_resolver(),
},
errors: recovered
.errors()
.iter()
.map(ParseDiagnostic::from)
.collect(),
};
let diagnostic_error = parse("SELECT FROM t").expect_err("invalid SQL yields a diagnostic");
let parse_diagnostic = ParseDiagnostic::from(&diagnostic_error);
let tokenize_document = TokenizeDocument {
source: "SELECT 1 + 2".to_owned(),
dialect: "ansi",
tokens: vec![
BindingToken {
kind: BindingTokenKind::Keyword { keyword: "select" },
span: SourceSpan { start: 0, end: 6 },
text: "SELECT".to_owned(),
},
BindingToken {
kind: BindingTokenKind::Number,
span: SourceSpan { start: 7, end: 8 },
text: "1".to_owned(),
},
BindingToken {
kind: BindingTokenKind::Operator { operator: "Plus" },
span: SourceSpan { start: 9, end: 10 },
text: "+".to_owned(),
},
BindingToken {
kind: BindingTokenKind::Number,
span: SourceSpan { start: 11, end: 12 },
text: "2".to_owned(),
},
],
trivia: Some(vec![BindingTrivia {
kind: "Whitespace",
span: SourceSpan { start: 6, end: 7 },
text: " ".to_owned(),
}]),
};
let dialect_info = vec![
DialectInfo {
name: "ansi",
aliases: &["ansi", "sql"],
tier: SupportTier::Stable,
evidence: SupportEvidence::StandardReference {
note: "ISO/IEC 9075 baseline",
},
},
DialectInfo {
name: "postgres",
aliases: &["postgres", "postgresql", "pg"],
tier: SupportTier::Stable,
evidence: SupportEvidence::EngineDifferential {
engine: "libpg_query",
version: "pg_query 6.1.1",
method: "raw-parse-tree differential",
},
},
];
let binding_token_kinds = vec![
BindingTokenKind::Word,
BindingTokenKind::Keyword { keyword: "select" },
BindingTokenKind::Operator { operator: "Plus" },
BindingTokenKind::Punctuation {
punctuation: "Comma",
},
];
let mut roots = Map::new();
roots.insert("parsed".to_owned(), to_value("parsed", &ast));
roots.insert(
"parse_document".to_owned(),
to_value("parse_document", &parse_document),
);
roots.insert(
"recovered_document".to_owned(),
to_value("recovered_document", &recovered_document),
);
roots.insert(
"parse_diagnostic".to_owned(),
to_value("parse_diagnostic", &parse_diagnostic),
);
roots.insert(
"tokenize_document".to_owned(),
to_value("tokenize_document", &tokenize_document),
);
roots.insert(
"dialect_info".to_owned(),
to_value("dialect_info", &dialect_info),
);
roots.insert(
"resolver_metadata".to_owned(),
to_value("resolver_metadata", &minimal_resolver()),
);
roots.insert(
"binding_token_kinds".to_owned(),
to_value("binding_token_kinds", &binding_token_kinds),
);
let document = json!({
"schema_version": WIRE_SCHEMA_VERSION,
"roots": Value::Object(roots),
});
let mut text = serde_json::to_string_pretty(&document).expect("snapshot serializes");
text.push('\n');
text
}
fn to_value<T: serde::Serialize>(name: &str, value: &T) -> Value {
serde_json::to_value(value).unwrap_or_else(|err| panic!("serialize `{name}`: {err}"))
}
#[test]
fn wire_schema_snapshot_is_current() {
let generated = canonical_snapshot();
let path = snapshot_path();
if std::env::var_os("UPDATE_SCHEMA_SNAPSHOT").is_some() {
std::fs::create_dir_all(path.parent().expect("snapshot has a parent"))
.expect("create release/schema");
std::fs::write(&path, &generated).expect("write shape snapshot");
let baseline = compat_baseline_path();
if !baseline.exists() {
std::fs::create_dir_all(baseline.parent().expect("baseline has a parent"))
.expect("create release/schema/compat");
let baseline_value =
serde_json::to_value(ast_sample()).expect("baseline document serializes");
let mut frozen =
serde_json::to_string_pretty(&baseline_value).expect("baseline serializes");
frozen.push('\n');
std::fs::write(&baseline, frozen).expect("write frozen compat baseline");
}
return;
}
let committed = std::fs::read_to_string(&path).unwrap_or_else(|err| {
panic!(
"{}: {err}\nregenerate with `UPDATE_SCHEMA_SNAPSHOT=1 cargo nextest run -p squonk \
--features serde wire_schema` (see docs/schema-contract.md)",
path.display(),
)
});
if committed != generated {
let line = first_divergent_line(&committed, &generated);
panic!(
"the serialized binding wire shape changed (first divergence at line {line} of {}).\n\
This is a WIRE CONTRACT change the Rust semver gate cannot see. Follow \
docs/schema-contract.md:\n\
- additive-optional change (new skip_serializing_if field / #[non_exhaustive] variant): \
keep WIRE_SCHEMA_VERSION, regenerate the snapshot;\n\
- breaking change (renamed/removed field, changed enum representation or omission \
behaviour): bump WIRE_SCHEMA_VERSION in crates/squonk/src/bindings.rs and add a new \
release/schema/wire-schema.v{{N}}.json, keeping this one frozen.\n\
Regenerate with `UPDATE_SCHEMA_SNAPSHOT=1 cargo nextest run -p squonk --features \
serde wire_schema`.",
snapshot_path().display(),
);
}
}
#[test]
fn schema_version_matches_binding_constant() {
if std::env::var_os("UPDATE_SCHEMA_SNAPSHOT").is_some() {
return;
}
let path = snapshot_path();
let committed =
std::fs::read_to_string(&path).unwrap_or_else(|err| panic!("{}: {err}", path.display()));
let document: Value = serde_json::from_str(&committed).expect("snapshot is valid JSON");
assert_eq!(
document["schema_version"].as_u64(),
Some(u64::from(WIRE_SCHEMA_VERSION)),
"release/schema/wire-schema.v{WIRE_SCHEMA_VERSION}.json records a different schema_version \
than the WIRE_SCHEMA_VERSION constant; regenerate after a version bump",
);
}
#[test]
fn frozen_baseline_still_deserializes() {
if std::env::var_os("UPDATE_SCHEMA_SNAPSHOT").is_some() {
return;
}
let path = compat_baseline_path();
let frozen = std::fs::read_to_string(&path).unwrap_or_else(|err| {
panic!(
"{}: {err}\ngenerate the frozen baseline once with `UPDATE_SCHEMA_SNAPSHOT=1 cargo \
nextest run -p squonk --features serde wire_schema`",
path.display(),
)
});
let restored: Parsed = serde_json::from_str(&frozen).unwrap_or_else(|err| {
panic!(
"the frozen v1 baseline no longer deserializes ({err}). This is a BREAKING wire change: \
bump WIRE_SCHEMA_VERSION and follow docs/schema-contract.md instead of editing the \
frozen baseline.",
)
});
assert!(
!restored.to_sql().is_empty(),
"the baseline document rendered empty SQL",
);
}
fn first_divergent_line(committed: &str, generated: &str) -> usize {
committed
.lines()
.zip(generated.lines())
.position(|(a, b)| a != b)
.map_or_else(
|| committed.lines().count().min(generated.lines().count()) + 1,
|index| index + 1,
)
}