use crate::model::{StarGraph, StarTerm, StarTriple};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use thiserror::Error;
use tracing::{debug, info, warn};
#[derive(Error, Debug)]
pub enum MigrationError {
#[error("Migration failed: {0}")]
MigrationFailed(String),
#[error("Pattern detection failed: {0}")]
PatternDetectionFailed(String),
#[error("Conversion error: {0}")]
ConversionError(String),
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Invalid RDF data: {0}")]
InvalidRdfData(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrationConfig {
pub detection_strategy: DetectionStrategy,
pub auto_convert: bool,
pub preserve_original: bool,
pub add_metadata: bool,
pub confidence_threshold: f64,
pub max_depth: usize,
pub validate: bool,
}
impl Default for MigrationConfig {
fn default() -> Self {
Self {
detection_strategy: DetectionStrategy::Automatic,
auto_convert: true,
preserve_original: false,
add_metadata: true,
confidence_threshold: 0.8,
max_depth: 10,
validate: true,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DetectionStrategy {
Automatic,
StandardReification,
SingletonProperties,
NamedGraphs,
Custom,
}
#[derive(Debug, Clone)]
pub struct ReificationPattern {
pub pattern_type: ReificationType,
pub subject: String,
pub original_subject: String,
pub original_predicate: String,
pub original_object: String,
pub metadata: HashMap<String, Vec<String>>,
pub confidence: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ReificationType {
Standard,
Singleton,
NamedGraph,
Custom,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrationResult {
pub input_triples: usize,
pub patterns_detected: usize,
pub patterns_converted: usize,
pub output_triples: usize,
pub warnings: Vec<String>,
pub errors: Vec<String>,
pub statistics: MigrationStatistics,
pub migrated_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrationStatistics {
pub standard_reifications: usize,
pub singleton_properties: usize,
pub named_graphs: usize,
pub custom_patterns: usize,
pub nested_patterns: usize,
pub avg_metadata_per_triple: f64,
}
pub struct RdfStarMigrator {
config: MigrationConfig,
detected_patterns: Vec<ReificationPattern>,
warnings: Vec<String>,
errors: Vec<String>,
}
impl RdfStarMigrator {
pub fn new(config: MigrationConfig) -> Self {
Self {
config,
detected_patterns: Vec::new(),
warnings: Vec::new(),
errors: Vec::new(),
}
}
pub fn detect_patterns(&mut self, graph: &StarGraph) -> Result<usize, MigrationError> {
info!(
"Detecting reification patterns in graph with {} triples",
graph.len()
);
self.detected_patterns.clear();
match self.config.detection_strategy {
DetectionStrategy::Automatic => {
self.detect_standard_reification(graph)?;
self.detect_singleton_properties(graph)?;
self.detect_named_graph_patterns(graph)?;
}
DetectionStrategy::StandardReification => {
self.detect_standard_reification(graph)?;
}
DetectionStrategy::SingletonProperties => {
self.detect_singleton_properties(graph)?;
}
DetectionStrategy::NamedGraphs => {
self.detect_named_graph_patterns(graph)?;
}
DetectionStrategy::Custom => {
warn!("Custom pattern detection not yet implemented");
}
}
info!(
"Detected {} reification patterns",
self.detected_patterns.len()
);
Ok(self.detected_patterns.len())
}
fn detect_standard_reification(&mut self, graph: &StarGraph) -> Result<(), MigrationError> {
let rdf_type = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
let rdf_statement = "http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement";
let rdf_subject = "http://www.w3.org/1999/02/22-rdf-syntax-ns#subject";
let rdf_predicate = "http://www.w3.org/1999/02/22-rdf-syntax-ns#predicate";
let rdf_object = "http://www.w3.org/1999/02/22-rdf-syntax-ns#object";
let mut reification_nodes = HashSet::new();
for triple in graph.iter() {
if let (Some(pred_nn), Some(obj_nn)) = (
triple.predicate.as_named_node(),
triple.object.as_named_node(),
) {
if pred_nn.iri == rdf_type && obj_nn.iri == rdf_statement {
let node_id = if let Some(nn) = triple.subject.as_named_node() {
nn.iri.to_string()
} else if let Some(bn) = triple.subject.as_blank_node() {
bn.id.to_string()
} else {
continue;
};
reification_nodes.insert(node_id);
}
}
}
debug!("Found {} rdf:Statement nodes", reification_nodes.len());
for node in reification_nodes {
let mut subj = None;
let mut pred = None;
let mut obj = None;
let mut metadata = HashMap::new();
for triple in graph.iter() {
let subject_matches = if let Some(s_nn) = triple.subject.as_named_node() {
s_nn.iri == node
} else if let Some(s_bn) = triple.subject.as_blank_node() {
s_bn.id == node
} else {
false
};
if subject_matches {
if let Some(p_nn) = triple.predicate.as_named_node() {
match p_nn.iri.as_str() {
p if p == rdf_subject => {
subj = triple.object.as_named_node().map(|nn| nn.iri.to_string());
}
p if p == rdf_predicate => {
pred = triple.object.as_named_node().map(|nn| nn.iri.to_string());
}
p if p == rdf_object => {
obj = Some(Self::term_to_string(&triple.object));
}
_ => {
if p_nn.iri != rdf_type {
metadata
.entry(p_nn.iri.to_string())
.or_insert_with(Vec::new)
.push(Self::term_to_string(&triple.object));
}
}
}
}
}
}
if let (Some(s), Some(p), Some(o)) = (subj, pred, obj) {
let pattern = ReificationPattern {
pattern_type: ReificationType::Standard,
subject: node,
original_subject: s,
original_predicate: p,
original_object: o,
metadata,
confidence: 1.0,
};
self.detected_patterns.push(pattern);
}
}
Ok(())
}
fn detect_singleton_properties(&mut self, graph: &StarGraph) -> Result<(), MigrationError> {
let singleton_of = "http://www.w3.org/1999/02/22-rdf-syntax-ns#singletonPropertyOf";
let mut singleton_props = HashMap::new();
for triple in graph.iter() {
if let (Some(subj_nn), Some(pred_nn), Some(obj_nn)) = (
triple.subject.as_named_node(),
triple.predicate.as_named_node(),
triple.object.as_named_node(),
) {
if pred_nn.iri == singleton_of {
singleton_props.insert(subj_nn.iri.to_string(), obj_nn.iri.to_string());
}
}
}
debug!("Found {} singleton properties", singleton_props.len());
for (singleton_prop, original_pred) in singleton_props {
for triple in graph.iter() {
if let (Some(subj_nn), Some(pred_nn)) = (
triple.subject.as_named_node(),
triple.predicate.as_named_node(),
) {
if pred_nn.iri == singleton_prop {
let pattern = ReificationPattern {
pattern_type: ReificationType::Singleton,
subject: singleton_prop.clone(),
original_subject: subj_nn.iri.to_string(),
original_predicate: original_pred.clone(),
original_object: Self::term_to_string(&triple.object),
metadata: HashMap::new(), confidence: 0.9,
};
self.detected_patterns.push(pattern);
}
}
}
}
Ok(())
}
fn detect_named_graph_patterns(&mut self, _graph: &StarGraph) -> Result<(), MigrationError> {
debug!("Named graph pattern detection not yet implemented");
Ok(())
}
pub fn migrate(&mut self, graph: &StarGraph) -> Result<MigrationResult, MigrationError> {
info!("Starting migration of graph with {} triples", graph.len());
let input_triples = graph.len();
let patterns_detected = self.detect_patterns(graph)?;
let patterns_to_convert: Vec<ReificationPattern> = self
.detected_patterns
.iter()
.filter(|p| p.confidence >= self.config.confidence_threshold)
.cloned()
.collect();
let patterns_converted = if self.config.auto_convert {
self.convert_patterns_owned(patterns_to_convert)?
} else {
0
};
let statistics = self.generate_statistics();
let result = MigrationResult {
input_triples,
patterns_detected,
patterns_converted,
output_triples: graph.len(), warnings: self.warnings.clone(),
errors: self.errors.clone(),
statistics,
migrated_at: Utc::now().to_rfc3339(),
};
info!(
"Migration completed: {} patterns detected, {} converted",
patterns_detected, patterns_converted
);
Ok(result)
}
#[allow(dead_code)]
fn convert_patterns(
&mut self,
patterns: &[&ReificationPattern],
) -> Result<usize, MigrationError> {
let mut converted = 0;
for pattern in patterns {
match self.convert_pattern(pattern) {
Ok(_) => {
converted += 1;
}
Err(e) => {
self.errors
.push(format!("Failed to convert pattern: {}", e));
}
}
}
Ok(converted)
}
fn convert_patterns_owned(
&mut self,
patterns: Vec<ReificationPattern>,
) -> Result<usize, MigrationError> {
let mut converted = 0;
for pattern in &patterns {
match self.convert_pattern(pattern) {
Ok(_) => {
converted += 1;
}
Err(e) => {
self.errors
.push(format!("Failed to convert pattern: {}", e));
}
}
}
Ok(converted)
}
fn convert_pattern(
&mut self,
pattern: &ReificationPattern,
) -> Result<StarTriple, MigrationError> {
let subject = StarTerm::iri(&pattern.original_subject)
.map_err(|e| MigrationError::ConversionError(e.to_string()))?;
let predicate = StarTerm::iri(&pattern.original_predicate)
.map_err(|e| MigrationError::ConversionError(e.to_string()))?;
let object = self.string_to_term(&pattern.original_object)?;
let base_triple = StarTriple::new(subject, predicate, object);
if self.config.add_metadata {
debug!(
"Converting pattern with {} metadata predicates",
pattern.metadata.len()
);
}
Ok(base_triple)
}
fn generate_statistics(&self) -> MigrationStatistics {
let standard_reifications = self
.detected_patterns
.iter()
.filter(|p| p.pattern_type == ReificationType::Standard)
.count();
let singleton_properties = self
.detected_patterns
.iter()
.filter(|p| p.pattern_type == ReificationType::Singleton)
.count();
let named_graphs = self
.detected_patterns
.iter()
.filter(|p| p.pattern_type == ReificationType::NamedGraph)
.count();
let custom_patterns = self
.detected_patterns
.iter()
.filter(|p| p.pattern_type == ReificationType::Custom)
.count();
let total_metadata: usize = self
.detected_patterns
.iter()
.map(|p| p.metadata.len())
.sum();
let avg_metadata_per_triple = if !self.detected_patterns.is_empty() {
total_metadata as f64 / self.detected_patterns.len() as f64
} else {
0.0
};
MigrationStatistics {
standard_reifications,
singleton_properties,
named_graphs,
custom_patterns,
nested_patterns: 0, avg_metadata_per_triple,
}
}
fn term_to_string(term: &StarTerm) -> String {
match term {
StarTerm::NamedNode(nn) => nn.iri.clone(),
StarTerm::Literal(lit) => lit.value.clone(),
StarTerm::BlankNode(bn) => format!("_:{}", bn.id),
StarTerm::QuotedTriple(qt) => format!(
"<< {} {} {} >>",
Self::term_to_string(&qt.subject),
Self::term_to_string(&qt.predicate),
Self::term_to_string(&qt.object)
),
_ => String::new(),
}
}
fn string_to_term(&self, s: &str) -> Result<StarTerm, MigrationError> {
if s.starts_with("http://") || s.starts_with("https://") {
StarTerm::iri(s).map_err(|e| MigrationError::ConversionError(e.to_string()))
} else if let Some(stripped) = s.strip_prefix("_:") {
StarTerm::blank_node(stripped)
.map_err(|e| MigrationError::ConversionError(e.to_string()))
} else {
StarTerm::literal(s).map_err(|e| MigrationError::ConversionError(e.to_string()))
}
}
pub fn get_patterns(&self) -> &[ReificationPattern] {
&self.detected_patterns
}
pub fn get_warnings(&self) -> &[String] {
&self.warnings
}
pub fn get_errors(&self) -> &[String] {
&self.errors
}
}
pub struct MigrationValidator;
impl MigrationValidator {
pub fn validate(
original: &StarGraph,
migrated: &StarGraph,
) -> Result<ValidationReport, MigrationError> {
let mut report = ValidationReport {
is_valid: true,
issues: Vec::new(),
warnings: Vec::new(),
};
if migrated.len() < original.len() / 2 {
report.issues.push(format!(
"Significant data loss detected: {} -> {} triples",
original.len(),
migrated.len()
));
report.is_valid = false;
}
let quoted_count = migrated
.iter()
.filter(|t| {
matches!(t.subject, StarTerm::QuotedTriple(_))
|| matches!(t.object, StarTerm::QuotedTriple(_))
})
.count();
if quoted_count == 0 {
report
.warnings
.push("No quoted triples found in migrated data".to_string());
}
info!(
"Validation complete: {} issues, {} warnings",
report.issues.len(),
report.warnings.len()
);
Ok(report)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationReport {
pub is_valid: bool,
pub issues: Vec<String>,
pub warnings: Vec<String>,
}
pub mod integrations {
use super::*;
pub struct JenaIntegration;
impl JenaIntegration {
pub fn default_config() -> MigrationConfig {
MigrationConfig {
detection_strategy: DetectionStrategy::StandardReification,
auto_convert: true,
preserve_original: true, add_metadata: true,
confidence_threshold: 0.9,
max_depth: 15,
validate: true,
}
}
pub fn export_hints() -> HashMap<String, String> {
let mut hints = HashMap::new();
hints.insert(
"serialization".to_string(),
"Use Turtle-star or TriG-star for best compatibility".to_string(),
);
hints.insert(
"reification".to_string(),
"Jena expects standard RDF reification patterns".to_string(),
);
hints.insert(
"namespaces".to_string(),
"Include common Jena namespaces (rdf, rdfs, owl, xsd)".to_string(),
);
hints
}
pub fn validate_compatibility(graph: &StarGraph) -> Vec<String> {
let mut warnings = Vec::new();
if graph.len() > 100000 {
warnings.push(
"Large graph detected. Consider TDB2 storage backend in Jena".to_string(),
);
}
warnings
}
}
pub struct Rdf4jIntegration;
impl Rdf4jIntegration {
pub fn default_config() -> MigrationConfig {
MigrationConfig {
detection_strategy: DetectionStrategy::Automatic,
auto_convert: true,
preserve_original: false, add_metadata: true,
confidence_threshold: 0.85,
max_depth: 20,
validate: true,
}
}
pub fn export_hints() -> HashMap<String, String> {
let mut hints = HashMap::new();
hints.insert(
"serialization".to_string(),
"RDF4J natively supports RDF-star in all formats".to_string(),
);
hints.insert(
"storage".to_string(),
"Use Native or Memory store for RDF-star support".to_string(),
);
hints.insert(
"querying".to_string(),
"SPARQL-star fully supported in RDF4J 3.7+".to_string(),
);
hints
}
pub fn validate_compatibility(graph: &StarGraph) -> Vec<String> {
let mut warnings = Vec::new();
if graph.is_empty() {
warnings.push("Empty graph detected".to_string());
}
warnings
}
}
pub struct BlazegraphIntegration;
impl BlazegraphIntegration {
pub fn default_config() -> MigrationConfig {
MigrationConfig {
detection_strategy: DetectionStrategy::StandardReification,
auto_convert: true,
preserve_original: true, add_metadata: false, confidence_threshold: 0.9,
max_depth: 10,
validate: true,
}
}
pub fn export_hints() -> HashMap<String, String> {
let mut hints = HashMap::new();
hints.insert(
"note".to_string(),
"Blazegraph does not support RDF-star natively. Use reification.".to_string(),
);
hints.insert(
"format".to_string(),
"Use N-Triples or N-Quads for bulk loading".to_string(),
);
hints.insert(
"optimization".to_string(),
"Disable SPARQL-star features for Blazegraph compatibility".to_string(),
);
hints
}
pub fn convert_to_reification(graph: &StarGraph) -> Result<StarGraph, MigrationError> {
let mut config = Self::default_config();
config.auto_convert = true;
config.preserve_original = false;
let mut migrator = RdfStarMigrator::new(config);
migrator.detect_patterns(graph)?;
Ok(graph.clone())
}
}
pub struct StardogIntegration;
impl StardogIntegration {
pub fn default_config() -> MigrationConfig {
MigrationConfig {
detection_strategy: DetectionStrategy::Automatic,
auto_convert: true,
preserve_original: false, add_metadata: true,
confidence_threshold: 0.9,
max_depth: 25,
validate: true,
}
}
pub fn export_hints() -> HashMap<String, String> {
let mut hints = HashMap::new();
hints.insert(
"version".to_string(),
"Stardog 7.0+ required for RDF-star support".to_string(),
);
hints.insert(
"format".to_string(),
"Turtle-star and TriG-star fully supported".to_string(),
);
hints.insert(
"reasoning".to_string(),
"RDF-star triples participate in reasoning in Stardog".to_string(),
);
hints
}
pub fn validate_compatibility(graph: &StarGraph) -> Vec<String> {
let mut warnings = Vec::new();
if graph.len() > 1_000_000 {
warnings.push(
"Very large graph. Consider Stardog's bulk loading utilities".to_string(),
);
}
warnings
}
}
pub struct VirtuosoIntegration;
impl VirtuosoIntegration {
pub fn default_config() -> MigrationConfig {
MigrationConfig {
detection_strategy: DetectionStrategy::NamedGraphs,
auto_convert: true,
preserve_original: true, add_metadata: false,
confidence_threshold: 0.85,
max_depth: 10,
validate: true,
}
}
pub fn export_hints() -> HashMap<String, String> {
let mut hints = HashMap::new();
hints.insert(
"note".to_string(),
"Virtuoso does not support RDF-star syntax natively".to_string(),
);
hints.insert(
"approach".to_string(),
"Use named graphs to represent quoted triples".to_string(),
);
hints.insert(
"format".to_string(),
"Use N-Quads for named graph representation".to_string(),
);
hints
}
pub fn convert_to_named_graphs(graph: &StarGraph) -> Result<StarGraph, MigrationError> {
Ok(graph.clone())
}
}
pub struct GraphDbIntegration;
impl GraphDbIntegration {
pub fn default_config() -> MigrationConfig {
MigrationConfig {
detection_strategy: DetectionStrategy::StandardReification,
auto_convert: true,
preserve_original: true,
add_metadata: true,
confidence_threshold: 0.9,
max_depth: 15,
validate: true,
}
}
pub fn export_hints() -> HashMap<String, String> {
let mut hints = HashMap::new();
hints.insert(
"note".to_string(),
"GraphDB supports RDF-star in version 10+".to_string(),
);
hints.insert(
"format".to_string(),
"Turtle-star and TriG-star supported".to_string(),
);
hints.insert(
"inference".to_string(),
"RDF-star works with GraphDB reasoning".to_string(),
);
hints
}
}
pub struct AllegroGraphIntegration;
impl AllegroGraphIntegration {
pub fn default_config() -> MigrationConfig {
MigrationConfig {
detection_strategy: DetectionStrategy::StandardReification,
auto_convert: true,
preserve_original: true, add_metadata: true,
confidence_threshold: 0.9,
max_depth: 20,
validate: true,
}
}
pub fn export_hints() -> HashMap<String, String> {
let mut hints = HashMap::new();
hints.insert(
"note".to_string(),
"AllegroGraph 7.3+ has experimental RDF-star support".to_string(),
);
hints.insert(
"fallback".to_string(),
"Use standard reification for older versions".to_string(),
);
hints.insert(
"gruff".to_string(),
"Gruff visual tool can display reified triples".to_string(),
);
hints
}
}
pub struct NeptuneIntegration;
impl NeptuneIntegration {
pub fn default_config() -> MigrationConfig {
MigrationConfig {
detection_strategy: DetectionStrategy::StandardReification,
auto_convert: true,
preserve_original: true,
add_metadata: false, confidence_threshold: 0.9,
max_depth: 10,
validate: true,
}
}
pub fn export_hints() -> HashMap<String, String> {
let mut hints = HashMap::new();
hints.insert(
"note".to_string(),
"Neptune does not support RDF-star natively as of 2024".to_string(),
);
hints.insert(
"format".to_string(),
"Use N-Triples or N-Quads for bulk loading".to_string(),
);
hints.insert(
"approach".to_string(),
"Convert RDF-star to standard reification before import".to_string(),
);
hints.insert(
"performance".to_string(),
"Use Neptune bulk loader for large datasets".to_string(),
);
hints
}
pub fn validate_compatibility(graph: &StarGraph) -> Vec<String> {
let mut warnings = Vec::new();
if graph.len() > 10_000_000 {
warnings.push(
"Very large graph. Use Neptune bulk loader for optimal performance".to_string(),
);
}
warnings.push(
"Remember to convert RDF-star to reification before Neptune import".to_string(),
);
warnings
}
}
pub fn get_config_for_tool(tool_name: &str) -> Option<MigrationConfig> {
match tool_name.to_lowercase().as_str() {
"jena" | "apache-jena" => Some(JenaIntegration::default_config()),
"rdf4j" | "eclipse-rdf4j" => Some(Rdf4jIntegration::default_config()),
"blazegraph" => Some(BlazegraphIntegration::default_config()),
"stardog" => Some(StardogIntegration::default_config()),
"virtuoso" | "openlink-virtuoso" => Some(VirtuosoIntegration::default_config()),
"graphdb" | "ontotext-graphdb" => Some(GraphDbIntegration::default_config()),
"allegrograph" => Some(AllegroGraphIntegration::default_config()),
"neptune" | "aws-neptune" | "amazon-neptune" => {
Some(NeptuneIntegration::default_config())
}
_ => None,
}
}
pub fn get_export_hints(tool_name: &str) -> Option<HashMap<String, String>> {
match tool_name.to_lowercase().as_str() {
"jena" | "apache-jena" => Some(JenaIntegration::export_hints()),
"rdf4j" | "eclipse-rdf4j" => Some(Rdf4jIntegration::export_hints()),
"blazegraph" => Some(BlazegraphIntegration::export_hints()),
"stardog" => Some(StardogIntegration::export_hints()),
"virtuoso" | "openlink-virtuoso" => Some(VirtuosoIntegration::export_hints()),
"graphdb" | "ontotext-graphdb" => Some(GraphDbIntegration::export_hints()),
"allegrograph" => Some(AllegroGraphIntegration::export_hints()),
"neptune" | "aws-neptune" | "amazon-neptune" => {
Some(NeptuneIntegration::export_hints())
}
_ => None,
}
}
pub fn supported_tools() -> Vec<&'static str> {
vec![
"Apache Jena",
"Eclipse RDF4J",
"Blazegraph",
"Stardog",
"OpenLink Virtuoso",
"Ontotext GraphDB",
"AllegroGraph",
"Amazon Neptune",
]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_migrator_creation() {
let config = MigrationConfig::default();
let migrator = RdfStarMigrator::new(config);
assert_eq!(migrator.detected_patterns.len(), 0);
}
#[test]
fn test_standard_reification_detection() -> Result<(), Box<dyn std::error::Error>> {
let mut graph = StarGraph::new();
let reif_node = "_:reif1";
graph.insert(StarTriple::new(
StarTerm::blank_node(reif_node)?,
StarTerm::iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")?,
StarTerm::iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#Statement")?,
))?;
graph.insert(StarTriple::new(
StarTerm::blank_node(reif_node)?,
StarTerm::iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#subject")?,
StarTerm::iri("http://example.org/alice")?,
))?;
graph.insert(StarTriple::new(
StarTerm::blank_node(reif_node)?,
StarTerm::iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#predicate")?,
StarTerm::iri("http://example.org/age")?,
))?;
graph.insert(StarTriple::new(
StarTerm::blank_node(reif_node)?,
StarTerm::iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#object")?,
StarTerm::literal("30")?,
))?;
let mut migrator = RdfStarMigrator::new(MigrationConfig::default());
let count = migrator.detect_patterns(&graph)?;
assert!(count > 0);
Ok(())
}
#[test]
fn test_migration_statistics() {
let config = MigrationConfig::default();
let migrator = RdfStarMigrator::new(config);
let stats = migrator.generate_statistics();
assert_eq!(stats.standard_reifications, 0);
assert_eq!(stats.singleton_properties, 0);
}
}