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
37 changes: 37 additions & 0 deletions crates/sqlbuild-analysis/src/semantic_checks/_helpers/bindings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//! Python's `update_binding_models`: model binding facts among project diagnostics.

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

/// One diagnostic as `update_binding_models` reads it: code, whether it is a model's, and name.
pub(crate) struct BindingOwner<'a> {
pub(crate) code: &'a str,
pub(crate) is_model: bool,
pub(crate) resource_name: Option<&'a str>,
}

/// Positions in `diagnostics` of each model's diagnostics whose codes it already binds.
pub(crate) fn binding_positions(
models: &[(&str, HashSet<&str>)],
diagnostics: &[BindingOwner<'_>],
) -> Vec<Vec<usize>> {
let mut by_model: HashMap<&str, Vec<usize>> = HashMap::new();
for (position, diagnostic) in diagnostics.iter().enumerate() {
if diagnostic.is_model {
by_model
.entry(diagnostic.resource_name.unwrap_or(""))
.or_default()
.push(position);
}
}
let mut result: Vec<Vec<usize>> = Vec::with_capacity(models.len());
for (name, codes) in models {
let mut kept: Vec<usize> = Vec::new();
for position in by_model.get(name).map_or(&[][..], Vec::as_slice) {
if codes.contains(diagnostics[*position].code) {
kept.push(*position);
}
}
result.push(kept);
}
result
}
87 changes: 87 additions & 0 deletions crates/sqlbuild-analysis/src/semantic_checks/_helpers/columns.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//! Python's column suggestions: edit distance, evidence order and the closest column.

use std::cmp::Ordering;

use crate::semantic_checks::_helpers::text::{ascii, casefold};
use crate::semantic_checks::constants::{MAX_EDIT_DISTANCE, MIN_ABBREVIATION_LENGTH};
use crate::semantic_checks::models::SemanticDeferral;

/// Python's `column_distance`: Levenshtein distance between case-folded names.
pub(crate) fn column_distance(left: &str, right: &str) -> usize {
let left: Vec<u8> = casefold(left).into_bytes();
let right: Vec<u8> = casefold(right).into_bytes();
let mut row: Vec<usize> = (0..=right.len()).collect();
for (i, a) in left.iter().enumerate() {
let mut following: Vec<usize> = Vec::with_capacity(right.len() + 1);
following.push(i + 1);
for (j, b) in right.iter().enumerate() {
let last = following[j];
following.push(
(last + 1)
.min(row[j + 1] + 1)
.min(row[j] + usize::from(a != b)),
);
}
row = following;
}
row[right.len()]
}

/// Python's `ordered_columns`: columns by relative edit distance, then by name.
pub(crate) fn ordered_columns<'a>(
name: &str,
columns: &'a [(String, String)],
) -> Result<Vec<&'a str>, SemanticDeferral> {
ascii(name)?;
let mut keyed: Vec<(f64, &'a str)> = Vec::with_capacity(columns.len());
for (column, _) in columns {
ascii(column)?;
let longest = name.len().max(column.len()).max(1);
keyed.push((
column_distance(name, column) as f64 / longest as f64,
column.as_str(),
));
}
keyed.sort_by(|left, right| {
left.0
.partial_cmp(&right.0)
.unwrap_or(Ordering::Equal)
.then_with(|| left.1.cmp(right.1))
});
Ok(keyed.into_iter().map(|(_, column)| column).collect())
}

/// Python's `closest_column`: the first ordered column within two edits or abbreviating the name.
pub(crate) fn closest_column<'a>(
name: &str,
columns: &'a [(String, String)],
) -> Result<Option<&'a str>, SemanticDeferral> {
let folded_name: String = casefold(name);
for candidate in ordered_columns(name, columns)? {
let distance = column_distance(name, candidate);
let folded_candidate: String = casefold(candidate);
let abbreviation = name.len() >= MIN_ABBREVIATION_LENGTH
&& folded_candidate.as_bytes().first() == folded_name.as_bytes().first()
&& folded_candidate.as_bytes().last() == folded_name.as_bytes().last()
&& is_subsequence(folded_name.as_bytes(), folded_candidate.as_bytes());
if distance <= MAX_EDIT_DISTANCE || abbreviation {
return Ok(Some(candidate));
}
}
Ok(None)
}

/// Python's `all(character in remaining for character in name)` over one shared iterator.
fn is_subsequence(name: &[u8], candidate: &[u8]) -> bool {
let mut position: usize = 0;
for character in name {
match candidate[position..]
.iter()
.position(|value| value == character)
{
Some(offset) => position += offset + 1,
None => return false,
}
}
true
}
Loading
Loading