Skip to main content
Glama

Convex MCP server

Official
by get-convex
tests.rs31.1 kB
use std::{ str::FromStr, sync::Arc, }; use common::{ bootstrap_model::index::{ database_index::{ DatabaseIndexSpec, DatabaseIndexState, IndexedFields, }, IndexConfig, IndexMetadata, TabletIndexMetadata, INDEX_TABLE, }, document::{ CreationTime, PackedDocument, ParseDocument as _, ResolvedDocument, }, index::IndexKey, persistence::{ ConflictStrategy, DocumentLogEntry, NoopRetentionValidator, Persistence, PersistenceIndexEntry, RepeatablePersistence, }, testing::{ TestIdGenerator, TestPersistence, }, types::{ unchecked_repeatable_ts, DatabaseIndexUpdate, DatabaseIndexValue, GenericIndexName, IndexDescriptor, PersistenceVersion, TableName, Timestamp, }, }; use itertools::Itertools; use must_let::must_let; use runtime::testing::TestRuntime; use value::{ assert_obj, FieldPath, ResolvedDocumentId, TabletId, }; trait IdGenerator { fn system_generate(&mut self, table_name: &TableName) -> ResolvedDocumentId; } impl IdGenerator for TestIdGenerator { fn system_generate(&mut self, table_name: &TableName) -> ResolvedDocumentId { TestIdGenerator::system_generate(self, table_name) } } struct ConstantId(ResolvedDocumentId); impl IdGenerator for ConstantId { fn system_generate(&mut self, _table_name: &TableName) -> ResolvedDocumentId { self.0 } } use crate::{ backend_in_memory_indexes::BackendInMemoryIndexes, index_registry::IndexRegistry, }; fn next_document_id( id_generator: &mut TestIdGenerator, table_name: &str, ) -> anyhow::Result<ResolvedDocumentId> { Ok(id_generator.user_generate(&TableName::from_str(table_name)?)) } fn gen_index_document( id_generator: &mut dyn IdGenerator, metadata: TabletIndexMetadata, ) -> anyhow::Result<ResolvedDocument> { let index_id = id_generator.system_generate(&INDEX_TABLE); ResolvedDocument::new(index_id, CreationTime::ONE, metadata.try_into()?) } fn index_documents( id_generator: &mut TestIdGenerator, mut indexes: Vec<TabletIndexMetadata>, ) -> anyhow::Result<Vec<(Timestamp, PackedDocument)>> { let mut index_documents = Vec::new(); let index_table = id_generator.system_table_id(&INDEX_TABLE); // Add the _index.by_id index. indexes.push(IndexMetadata::new_enabled( GenericIndexName::by_id(index_table.tablet_id), IndexedFields::by_id(), )); let ts = Timestamp::must(0); for metadata in indexes { let doc = gen_index_document(id_generator, metadata.clone())?; index_documents.push((ts, PackedDocument::pack(&doc))); } Ok(index_documents) } #[test] fn test_metadata_add_and_drop_index() -> anyhow::Result<()> { let mut id_generator = TestIdGenerator::new(); let index_documents = index_documents(&mut id_generator, vec![])?; let mut index_registry = IndexRegistry::bootstrap( &id_generator, index_documents.iter().map(|(_, d)| d.clone()), PersistenceVersion::default(), )?; assert_eq!(index_registry.all_enabled_indexes().len(), 1); let table_id = id_generator.user_table_id(&"messages".parse()?); let by_id = GenericIndexName::by_id(table_id.tablet_id); let by_name = GenericIndexName::new(table_id.tablet_id, IndexDescriptor::new("by_name")?)?; // Add `messages.by_id`. let by_id = gen_index_document( &mut id_generator, IndexMetadata::new_enabled(by_id, IndexedFields::by_id()), )?; let result = index_registry.update(None, Some(&by_id)); assert!(result.is_ok()); assert_eq!(index_registry.all_enabled_indexes().len(), 2); // Add messages by name. let by_name = gen_index_document( &mut id_generator, IndexMetadata::new_enabled(by_name, vec!["name".parse()?].try_into()?), )?; let result = index_registry.update(None, Some(&by_name)); assert!(result.is_ok()); assert_eq!(index_registry.all_enabled_indexes().len(), 3); // Try to drop it. Should succeed. let result = index_registry.update(Some(&by_name), None); assert!(result.is_ok()); assert_eq!(index_registry.all_enabled_indexes().len(), 2); Ok(()) } #[test] fn test_metadata_rename_index() -> anyhow::Result<()> { let mut id_generator = TestIdGenerator::new(); let index_documents = index_documents(&mut id_generator, vec![])?; let mut index_registry = IndexRegistry::bootstrap( &id_generator, index_documents.iter().map(|(_, d)| d.clone()), PersistenceVersion::default(), )?; let table = id_generator.user_table_id(&"messages".parse()?); let by_id = GenericIndexName::by_id(table.tablet_id); let by_first_id = GenericIndexName::new(table.tablet_id, IndexDescriptor::new("by_first_id")?)?; let by_name = GenericIndexName::new(table.tablet_id, IndexDescriptor::new("by_name")?)?; let by_first_name = GenericIndexName::new(table.tablet_id, IndexDescriptor::new("by_first_name")?)?; // Add messages table_scan index. let original = gen_index_document( &mut id_generator, IndexMetadata::new_enabled(by_id.clone(), vec!["name".parse()?].try_into()?), )?; let result = index_registry.update(None, Some(&original)); assert!(result.is_ok()); assert_eq!(index_registry.all_enabled_indexes().len(), 2); assert!(index_registry.enabled_index_metadata(&by_id).is_some()); // Renaming of table scan index is not allowed. let rename = ResolvedDocument::new( original.id(), CreationTime::ONE, IndexMetadata::new_enabled(by_first_id.clone(), vec!["name".parse()?].try_into()?) .try_into()?, )?; let result = index_registry.update(Some(&original), Some(&rename)); let err = result.unwrap_err(); assert!( format!("{err:?}").contains(&format!( "Can't rename system defined index {}.by_id", table.tablet_id )), "{err}" ); assert_eq!(index_registry.all_enabled_indexes().len(), 2); assert!(index_registry.enabled_index_metadata(&by_id).is_some()); assert!(index_registry .enabled_index_metadata(&by_first_id) .is_none()); // Add `by_name` let original = gen_index_document( &mut id_generator, IndexMetadata::new_enabled(by_name.clone(), vec!["name".parse()?].try_into()?), )?; let result = index_registry.update(None, Some(&original)); assert!(result.is_ok()); assert_eq!(index_registry.all_enabled_indexes().len(), 3); assert!(index_registry.enabled_index_metadata(&by_name).is_some()); // Rename `by_name` to `by_first_name`. let rename = ResolvedDocument::new( original.id(), CreationTime::ONE, IndexMetadata::new_enabled(by_first_name.clone(), vec!["name".parse()?].try_into()?) .try_into()?, )?; let result = index_registry.update(Some(&original), Some(&rename)); assert!(result.is_ok()); assert_eq!(index_registry.all_enabled_indexes().len(), 3); assert!(index_registry.enabled_index_metadata(&by_name).is_none()); assert!(index_registry .enabled_index_metadata(&by_first_name) .is_some()); Ok(()) } #[test] fn test_metadata_change_index() -> anyhow::Result<()> { let mut id_generator = TestIdGenerator::new(); let table = id_generator.user_table_id(&"messages".parse()?); let by_id = GenericIndexName::by_id(table.tablet_id); let by_name = GenericIndexName::new(table.tablet_id, IndexDescriptor::new("by_name")?)?; let authors_table = id_generator.user_table_id(&"authors".parse()?); let authors_by_name = GenericIndexName::new(authors_table.tablet_id, IndexDescriptor::new("by_name")?)?; let indexes = vec![IndexMetadata::new_enabled(by_id, IndexedFields::by_id())]; let index_documents = index_documents(&mut id_generator, indexes)?; let mut index_registry = IndexRegistry::bootstrap( &id_generator, index_documents.iter().map(|(_, d)| d.clone()), PersistenceVersion::default(), )?; let original = gen_index_document( &mut id_generator, IndexMetadata::new_enabled(by_name.clone(), vec!["name".parse()?].try_into()?), )?; let result = index_registry.update(None, Some(&original)); assert!(result.is_ok()); // Changing fields is not allowed. let changed_fields = ResolvedDocument::new( original.id(), CreationTime::ONE, IndexMetadata::new_enabled(by_name.clone(), vec!["first_name".parse()?].try_into()?) .try_into()?, )?; let result = index_registry.update(Some(&original), Some(&changed_fields)); assert!(result.is_err()); assert!(format!("{:?}", result.unwrap_err()) .contains("Can't modify index spec for existing indexes")); let current_metadata = index_registry.enabled_index_metadata(&by_name).unwrap(); must_let!(let IndexConfig::Database { spec, .. } = &current_metadata.config); must_let!(let DatabaseIndexSpec { fields } = spec); assert_eq!(*fields, vec!["name".parse()?].try_into()?,); // Changing which table the index is indexing is not allowed. let changed_table = ResolvedDocument::new( original.id(), CreationTime::ONE, IndexMetadata::new_enabled(authors_by_name.clone(), vec!["name".parse()?].try_into()?) .try_into()?, )?; let result = index_registry.update(Some(&original), Some(&changed_table)); assert!(result.is_err()); assert!(format!("{:?}", result.unwrap_err()).contains("Can't change indexed table")); assert!(index_registry.enabled_index_metadata(&by_name).is_some()); assert!(index_registry .enabled_index_metadata(&authors_by_name) .is_none()); // Creating a new index with the same name and state is not allowed. let name_collision = ResolvedDocument::new( id_generator.system_generate(&INDEX_TABLE), CreationTime::ONE, IndexMetadata::new_enabled(by_name.clone(), vec!["other_field".parse()?].try_into()?) .try_into()?, )?; let result = index_registry.update(None, Some(&name_collision)); assert!(result.is_err()); assert!( format!("{}", result.unwrap_err().root_cause()).contains(&format!( "Cannot create a second enabled index with name {}.by_name", table.tablet_id )) ); let current_metadata = index_registry.enabled_index_metadata(&by_name).unwrap(); must_let!( let IndexConfig::Database { spec: DatabaseIndexSpec { fields }, .. } = &current_metadata.config ); assert_eq!(*fields, vec!["name".parse()?].try_into()?,); Ok(()) } #[test] fn test_second_pending_index_for_name_fails() -> anyhow::Result<()> { let mut id_generator = TestIdGenerator::new(); let indexes = vec![]; let index_documents = index_documents(&mut id_generator, indexes)?; let mut index_registry = IndexRegistry::bootstrap( &id_generator, index_documents.iter().map(|(_, d)| d.clone()), PersistenceVersion::default(), )?; let table = id_generator.user_table_id(&"messages".parse()?); // Creating a new index with the same name and state is not allowed. let by_name = GenericIndexName::new(table.tablet_id, IndexDescriptor::new("by_name")?)?; let pending = gen_index_document( &mut id_generator, IndexMetadata::new_backfilling( Timestamp::MIN, by_name.clone(), vec!["name".parse()?].try_into()?, ), )?; let result = index_registry.update(None, Some(&pending)); assert!(result.is_ok()); let name_collision = ResolvedDocument::new( id_generator.system_generate(&INDEX_TABLE), CreationTime::ONE, IndexMetadata::new_backfilling( Timestamp::MIN, by_name.clone(), vec!["other_field".parse()?].try_into()?, ) .try_into()?, )?; let result = index_registry.update(None, Some(&name_collision)); assert!(result.is_err()); assert!( format!("{}", result.unwrap_err().root_cause()).contains(&format!( "Cannot create a second pending index with name {}.by_name", table.tablet_id )) ); let current_index = index_registry.get_pending(&by_name).unwrap(); must_let!(let IndexConfig::Database { spec, .. } = &current_index.metadata.config); must_let!(let DatabaseIndexSpec { fields } = spec); assert_eq!(*fields, vec!["name".parse()?].try_into()?,); Ok(()) } #[test] fn test_metadata_index_updates() -> anyhow::Result<()> { let mut id_generator = TestIdGenerator::new(); let table = id_generator.user_table_id(&"messages".parse()?); let by_id = GenericIndexName::by_id(table.tablet_id); let by_author = GenericIndexName::new(table.tablet_id, IndexDescriptor::new("by_author")?)?; let by_content = GenericIndexName::new(table.tablet_id, IndexDescriptor::new("by_content")?)?; let indexes = vec![ IndexMetadata::new_enabled(by_id.clone(), IndexedFields::by_id()), IndexMetadata::new_enabled(by_author.clone(), vec!["author".parse()?].try_into()?), IndexMetadata::new_enabled(by_content.clone(), vec!["content".parse()?].try_into()?), ]; let index_documents = index_documents(&mut id_generator, indexes)?; let mut index_registry = IndexRegistry::bootstrap( &id_generator, index_documents.iter().map(|(_, d)| d.clone()), PersistenceVersion::default(), )?; let mut in_memory_indexes = BackendInMemoryIndexes::bootstrap(&index_registry, index_documents, Timestamp::MIN)?; let by_id = index_registry.get_enabled(&by_id).unwrap().id(); let by_content = index_registry.get_enabled(&by_content).unwrap().id(); let by_author = index_registry.get_enabled(&by_author).unwrap().id(); let doc_id = next_document_id(&mut id_generator, "messages")?; let v1 = ResolvedDocument::new( doc_id, CreationTime::ONE, assert_obj!( "content" => "hllo", "author" => 33, ), )?; index_registry.update(None, Some(&v1))?; assert_eq!( in_memory_indexes.update(&index_registry, Timestamp::must(1), None, Some(v1.clone())), vec![ DatabaseIndexUpdate { index_id: by_id, key: IndexKey::new(vec![], doc_id.into()), value: DatabaseIndexValue::NonClustered(doc_id), is_system_index: true, }, DatabaseIndexUpdate { index_id: by_author, key: IndexKey::new(vec![33.into()], doc_id.into()), value: DatabaseIndexValue::NonClustered(doc_id), is_system_index: false, }, DatabaseIndexUpdate { index_id: by_content, key: IndexKey::new(vec!["hllo".try_into()?], doc_id.into()), value: DatabaseIndexValue::NonClustered(doc_id), is_system_index: false, }, ] ); let v2 = ResolvedDocument::new( doc_id, CreationTime::ONE, assert_obj!( "content" => "hello (edited)", "author" => 33, ), )?; index_registry.update(Some(&v1), Some(&v2))?; assert_eq!( in_memory_indexes.update( &index_registry, Timestamp::must(2), Some(v1), Some(v2.clone()) ), vec![ DatabaseIndexUpdate { index_id: by_id, key: IndexKey::new(vec![], doc_id.into()), value: DatabaseIndexValue::NonClustered(doc_id), is_system_index: true, }, // We generate an updated index entry even if the field has not // changed. Otherwise consistency checking and vacuuming old revisions // will become quite complicated. We can reconsider in the long run. DatabaseIndexUpdate { index_id: by_author, key: IndexKey::new(vec![33.into()], doc_id.into()), value: DatabaseIndexValue::NonClustered(doc_id), is_system_index: false, }, DatabaseIndexUpdate { index_id: by_content, key: IndexKey::new(vec!["hello (edited)".try_into()?], doc_id.into()), value: DatabaseIndexValue::NonClustered(doc_id), is_system_index: false, }, DatabaseIndexUpdate { index_id: by_content, key: IndexKey::new(vec!["hllo".try_into()?], doc_id.into()), value: DatabaseIndexValue::Deleted, is_system_index: false, }, ] ); let v3 = ResolvedDocument::new( doc_id, CreationTime::ONE, assert_obj!( "author" => 33, ), )?; index_registry.update(Some(&v2), Some(&v3))?; assert_eq!( in_memory_indexes.update( &index_registry, Timestamp::must(3), Some(v2), Some(v3.clone()) ), vec![ DatabaseIndexUpdate { index_id: by_id, key: IndexKey::new(vec![], doc_id.into()), value: DatabaseIndexValue::NonClustered(doc_id), is_system_index: true, }, DatabaseIndexUpdate { index_id: by_author, key: IndexKey::new(vec![33.into()], doc_id.into()), value: DatabaseIndexValue::NonClustered(doc_id), is_system_index: false, }, DatabaseIndexUpdate { index_id: by_content, key: IndexKey::new_allow_missing(vec![None], doc_id.into()), value: DatabaseIndexValue::NonClustered(doc_id), is_system_index: false, }, DatabaseIndexUpdate { index_id: by_content, key: IndexKey::new(vec!["hello (edited)".try_into()?], doc_id.into()), value: DatabaseIndexValue::Deleted, is_system_index: false, }, ] ); index_registry.update(Some(&v3), None)?; assert_eq!( in_memory_indexes.update(&index_registry, Timestamp::must(4), Some(v3), None), vec![ DatabaseIndexUpdate { index_id: by_id, key: IndexKey::new(vec![], doc_id.into()), value: DatabaseIndexValue::Deleted, is_system_index: true, }, DatabaseIndexUpdate { index_id: by_author, key: IndexKey::new(vec![33.into()], doc_id.into()), value: DatabaseIndexValue::Deleted, is_system_index: false, }, DatabaseIndexUpdate { index_id: by_content, key: IndexKey::new_allow_missing(vec![None], doc_id.into()), value: DatabaseIndexValue::Deleted, is_system_index: false, }, ] ); Ok(()) } #[convex_macro::test_runtime] async fn test_load_into_memory(_rt: TestRuntime) -> anyhow::Result<()> { let mut id_generator = TestIdGenerator::new(); let table = id_generator.user_table_id(&"messages".parse()?); let by_id = GenericIndexName::by_id(table.tablet_id); let by_author = GenericIndexName::new(table.tablet_id, IndexDescriptor::new("by_author")?)?; let indexes = vec![IndexMetadata::new_enabled( by_id.clone(), IndexedFields::by_id(), )]; let index_documents = index_documents(&mut id_generator, indexes)?; let mut index_registry = IndexRegistry::bootstrap( &id_generator, index_documents.iter().map(|(_, d)| d.clone()), PersistenceVersion::default(), )?; let mut in_memory_indexes = BackendInMemoryIndexes::bootstrap(&index_registry, index_documents, Timestamp::MIN)?; let mut index_metadata = IndexMetadata::new_enabled(by_author.clone(), vec!["author".parse()?].try_into()?); let index_doc = gen_index_document(&mut id_generator, index_metadata.clone())?; index_registry.update(None, Some(&index_doc))?; in_memory_indexes.update( &index_registry, Timestamp::must(1), None, Some(index_doc.clone()), ); // Add a document to persistence. let ps = Arc::new(TestPersistence::new()); let doc1 = ResolvedDocument::new( next_document_id(&mut id_generator, "messages")?, CreationTime::ONE, assert_obj!( "content" => "hello there!", "author" => "alice", ), )?; index_registry.update(None, Some(&doc1))?; let index_updates = in_memory_indexes.update( &index_registry, Timestamp::must(2), None, Some(doc1.clone()), ); ps.write( &[(DocumentLogEntry { ts: Timestamp::must(2), id: doc1.id_with_table_id(), value: Some(doc1), prev_ts: None, })], &index_updates .into_iter() .map(|u| PersistenceIndexEntry::from_index_update(Timestamp::must(2), &u)) .collect_vec(), ConflictStrategy::Error, ) .await?; id_generator.write_tables(ps.clone()).await?; let retention_validator = Arc::new(NoopRetentionValidator {}); // Load the index. in_memory_indexes .load_enabled( table.tablet_id, vec![(&index_doc).parse()?], &RepeatablePersistence::new( ps.clone(), unchecked_repeatable_ts(Timestamp::must(2)), retention_validator, ) .read_snapshot(unchecked_repeatable_ts(Timestamp::must(2)))?, ) .await?; assert_eq!( in_memory_indexes .in_memory_indexes() .get(&index_doc.id().internal_id()) .unwrap() .len(), 1 ); // Add another document. let doc2 = ResolvedDocument::new( next_document_id(&mut id_generator, "messages")?, CreationTime::ONE, assert_obj!( "content" => "hello to you too!", "author" => "bob", ), )?; index_registry.update(None, Some(&doc2))?; in_memory_indexes.update(&index_registry, Timestamp::must(3), None, Some(doc2)); // Make sure both documents are loaded in the in_memory index. assert_eq!( in_memory_indexes .in_memory_indexes() .get(&index_doc.id().internal_id()) .unwrap() .len(), 2 ); // Change the index state. It should still be loaded into memory. must_let!(let IndexConfig::Database { ref mut on_disk_state, ..} = index_metadata.config); *on_disk_state = DatabaseIndexState::Enabled; let updated_index_doc = ResolvedDocument::new( index_doc.id(), CreationTime::ONE, index_metadata.try_into()?, )?; index_registry.update(Some(&index_doc), Some(&updated_index_doc))?; in_memory_indexes.update( &index_registry, Timestamp::must(4), Some(index_doc.clone()), Some(updated_index_doc.clone()), ); // The documents should still be in-memory. assert_eq!( in_memory_indexes .in_memory_indexes() .get(&index_doc.id().internal_id()) .unwrap() .len(), 2 ); // Drop the index. It should no longer be in memory. index_registry.update(Some(&updated_index_doc), None)?; in_memory_indexes.update( &index_registry, Timestamp::must(4), Some(updated_index_doc), None, ); assert!(in_memory_indexes .in_memory_indexes() .get(&index_doc.id().internal_id()) .is_none()); Ok(()) } fn default_registry(id_generator: &mut TestIdGenerator) -> anyhow::Result<IndexRegistry> { let index_documents = index_documents(id_generator, vec![])?; IndexRegistry::bootstrap( id_generator, index_documents.iter().map(|(_, d)| d.clone()), PersistenceVersion::default(), ) } #[test] pub fn same_indexes_empty_registry_are_identical() -> anyhow::Result<()> { let mut id_generator = TestIdGenerator::new(); let first = default_registry(&mut id_generator)?; let second = first.clone(); assert!(first.same_indexes(&second)); assert!(second.same_indexes(&first)); Ok(()) } fn new_enabled_doc( id_generator: &mut dyn IdGenerator, tablet_id: TabletId, name: &str, fields: Vec<&str>, ) -> anyhow::Result<ResolvedDocument> { let index_name = GenericIndexName::new(tablet_id, IndexDescriptor::new(name.to_string())?)?; let field_paths = fields .into_iter() .map(|field| field.parse()) .collect::<anyhow::Result<Vec<FieldPath>>>()?; let metadata = IndexMetadata::new_enabled(index_name, field_paths.try_into()?); gen_index_document(id_generator, metadata) } fn new_pending_doc( id_generator: &mut dyn IdGenerator, tablet_id: TabletId, name: &str, fields: Vec<&str>, ) -> anyhow::Result<ResolvedDocument> { let index_name = GenericIndexName::new(tablet_id, IndexDescriptor::new(name.to_string())?)?; let field_paths = fields .into_iter() .map(|field| field.parse()) .collect::<anyhow::Result<Vec<FieldPath>>>()?; let metadata = IndexMetadata::new_backfilling(Timestamp::MIN, index_name, field_paths.try_into()?); gen_index_document(id_generator, metadata) } fn tablet_id(id_generator: &mut TestIdGenerator) -> anyhow::Result<TabletId> { Ok(id_generator.user_table_id(&"table".parse()?).tablet_id) } #[test] pub fn same_indexes_one_enabled_one_empty_are_not_same() -> anyhow::Result<()> { let mut id_generator = TestIdGenerator::new(); let mut first = default_registry(&mut id_generator)?; let second = first.clone(); let tablet_id = tablet_id(&mut id_generator)?; let index_doc = new_enabled_doc(&mut id_generator, tablet_id, "by_author", vec!["author"])?; first.update(None, Some(&index_doc))?; assert!(!first.same_indexes(&second)); assert!(!second.same_indexes(&first)); Ok(()) } #[test] pub fn same_indexes_identical_enabled_doc_are_same() -> anyhow::Result<()> { let mut id_generator = TestIdGenerator::new(); let mut first = default_registry(&mut id_generator)?; let mut second = first.clone(); let tablet_id = tablet_id(&mut id_generator)?; let index_doc = new_enabled_doc(&mut id_generator, tablet_id, "by_author", vec!["author"])?; first.update(None, Some(&index_doc))?; second.update(None, Some(&index_doc))?; assert!(first.same_indexes(&second)); assert!(second.same_indexes(&first)); Ok(()) } #[test] pub fn same_indexes_different_enabled_doc_id_are_not_same() -> anyhow::Result<()> { let mut id_generator = TestIdGenerator::new(); let mut first = default_registry(&mut id_generator)?; let mut second = first.clone(); let tablet_id = tablet_id(&mut id_generator)?; let first_doc = new_enabled_doc(&mut id_generator, tablet_id, "by_author", vec!["author"])?; let second_doc = new_enabled_doc(&mut id_generator, tablet_id, "by_author", vec!["author"])?; first.update(None, Some(&first_doc))?; second.update(None, Some(&second_doc))?; assert!(!first.same_indexes(&second)); assert!(!second.same_indexes(&first)); Ok(()) } #[test] pub fn same_indexes_identical_pending_doc_are_same() -> anyhow::Result<()> { let mut id_generator = TestIdGenerator::new(); let mut first = default_registry(&mut id_generator)?; let mut second = first.clone(); let tablet_id = tablet_id(&mut id_generator)?; let index_doc = new_pending_doc(&mut id_generator, tablet_id, "by_author", vec!["author"])?; first.update(None, Some(&index_doc))?; second.update(None, Some(&index_doc))?; assert!(first.same_indexes(&second)); assert!(second.same_indexes(&first)); Ok(()) } #[test] pub fn same_indexes_different_pending_docs_are_not_same() -> anyhow::Result<()> { let mut id_generator = TestIdGenerator::new(); let mut first = default_registry(&mut id_generator)?; let mut second = first.clone(); let tablet_id = tablet_id(&mut id_generator)?; let first_doc = new_pending_doc(&mut id_generator, tablet_id, "by_author", vec!["author"])?; let second_doc = new_pending_doc(&mut id_generator, tablet_id, "by_author", vec!["author"])?; first.update(None, Some(&first_doc))?; second.update(None, Some(&second_doc))?; assert!(!first.same_indexes(&second)); assert!(!second.same_indexes(&first)); Ok(()) } #[test] pub fn same_indexes_same_docs_different_states_are_same() -> anyhow::Result<()> { let mut id_generator = TestIdGenerator::new(); let mut first = default_registry(&mut id_generator)?; let mut second = first.clone(); let tablet_id = tablet_id(&mut id_generator)?; let first_doc = new_pending_doc(&mut id_generator, tablet_id, "by_author", vec!["author"])?; let second_doc = new_enabled_doc( &mut ConstantId(first_doc.id()), tablet_id, "by_author", vec!["author"], )?; first.update(None, Some(&first_doc))?; second.update(None, Some(&second_doc))?; assert!(first.same_indexes(&second)); assert!(second.same_indexes(&first)); Ok(()) } #[test] pub fn same_indexes_same_docs_different_update_order_are_same() -> anyhow::Result<()> { let mut id_generator = TestIdGenerator::new(); let mut first = default_registry(&mut id_generator)?; let mut second = first.clone(); let tablet_id = tablet_id(&mut id_generator)?; let first_enabled = new_enabled_doc(&mut id_generator, tablet_id, "by_author", vec!["author"])?; let second_enabled = new_enabled_doc(&mut id_generator, tablet_id, "by_title", vec!["title"])?; let first_pending = new_pending_doc( &mut id_generator, tablet_id, "by_publisher", vec!["publisher"], )?; let second_pending = new_pending_doc( &mut id_generator, tablet_id, "by_subtitle", vec!["subtitle"], )?; first.update(None, Some(&first_enabled))?; first.update(None, Some(&second_enabled))?; first.update(None, Some(&first_pending))?; first.update(None, Some(&second_pending))?; second.update(None, Some(&second_pending))?; second.update(None, Some(&first_pending))?; second.update(None, Some(&second_enabled))?; second.update(None, Some(&first_enabled))?; assert!(first.same_indexes(&second)); assert!(second.same_indexes(&first)); Ok(()) }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/get-convex/convex-backend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server