Skip to content
Open
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
1 change: 1 addition & 0 deletions store/postgres/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub mod layout_for_tests {
Connection, EVENT_TAP, EVENT_TAP_ENABLED, Mirror, Namespace, make_dummy_site,
};
pub use crate::relational::*;
pub use crate::relational_queries::CopyEntityBatchQuery;
pub mod writable {
pub use crate::writable::test_support::allow_steps;
}
Expand Down
14 changes: 13 additions & 1 deletion store/postgres/src/relational.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1752,14 +1752,26 @@ impl Table {
}

/// Find the column `name` in this table. The name must be in snake case,
/// i.e., use SQL conventions
/// i.e., use SQL conventions.
///
/// Fulltext search columns (`ColumnType::TSVector`) are deliberately
/// skipped since they are not queryable entity attributes. Use
/// [`Table::column_including_fulltext`] when the fulltext columns are
/// needed, e.g. when copying a table.
pub fn column(&self, name: &SqlName) -> Option<&Column> {
self.columns
.iter()
.filter(|column| !matches!(column.column_type, ColumnType::TSVector(_)))
.find(|column| &column.name == name)
}

/// Find the column `name` in this table, including fulltext search
/// columns which [`Table::column`] hides. The name must be in snake
/// case, i.e., use SQL conventions.
pub fn column_including_fulltext(&self, name: &SqlName) -> Option<&Column> {
self.columns.iter().find(|column| &column.name == name)
}

/// Find the column for `field` in this table. The name must be the
/// GraphQL name of an entity field
pub fn column_for_field(&self, field: &str) -> Result<&Column, StoreError> {
Expand Down
8 changes: 7 additions & 1 deletion store/postgres/src/relational_queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5104,7 +5104,13 @@ impl<'a> CopyEntityBatchQuery<'a> {
) -> Result<Self, StoreError> {
let mut columns = Vec::new();
for dcol in &dst.columns {
if let Some(scol) = src.column(&dcol.name) {
// Match against all source columns, INCLUDING fulltext (tsvector)
// columns, which `Table::column` deliberately hides. The stored
// tsvector is copied verbatim; without this, fulltext columns
// would be silently dropped from the copy (they are nullable, so
// the error branch below is not taken) and the destination's
// search index would be left empty.
if let Some(scol) = src.column_including_fulltext(&dcol.name) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should can_copy_from also use column_including_fulltext it uses column() currently?

if let Some(msg) = dcol.is_assignable_from(scol, &src.object) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that claude flagged during the review:

is_assignable_from only compares FulltextConfig, which is language and algorithm, not fulltext_fields. So a graft that adds a field to include: copies the old tsvectors and searches on the new field quietly return nothing. Compare fulltext_fields here too?

return Err(anyhow!("{}", msg).into());
} else {
Expand Down
77 changes: 77 additions & 0 deletions store/test-store/tests/postgres/relational.rs
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,83 @@ async fn insert_null_fulltext_fields() {
.await;
}

/// Copying a table must also populate its fulltext (tsvector) search
/// columns. These columns are not returned by `Table::column`, so a naive
/// copy would silently drop them and leave the destination's search index
/// empty. This copies the `User` table into a fresh schema and checks that a
/// fulltext query against the copy still returns the expected entity.
#[graph::test]
async fn copy_populates_fulltext() {
use diesel_async::RunQueryDsl;
use graph_store_postgres::layout_for_tests::CopyEntityBatchQuery;

run_test(async |conn, layout| {
// Populate the source `User` table with searchable text
insert_users(conn, layout).await;

// Create a destination schema with the same layout in a separate
// namespace to copy into. Drop it first in case a previous run left
// it behind, since `remove_schema` only cleans up the source
// namespace.
let dst_namespace = Namespace::new("sgd0816".to_string()).unwrap();
conn.batch_execute(&format!(
"drop schema if exists {ns} cascade; create schema {ns}",
ns = dst_namespace.as_str()
))
.await
.unwrap();
let dst_site = make_dummy_site(
THINGS_SUBGRAPH_ID.clone(),
dst_namespace,
NETWORK_NAME.to_string(),
);
let dst_layout = Layout::create_relational_schema(
conn,
Arc::new(dst_site),
&THINGS_SCHEMA,
BTreeSet::new(),
)
.await
.expect("Failed to create destination relational schema");

// Copy the `User` table from the source into the destination
let src_table = layout.table_for_entity(&USER_TYPE).unwrap();
let dst_table = dst_layout.table_for_entity(&USER_TYPE).unwrap();
CopyEntityBatchQuery::new(
dst_table.as_ref(),
src_table.as_ref(),
0,
i64::MAX,
BLOCK_NUMBER_MAX,
)
.unwrap()
.count_current()
.get_result::<i64>(conn)
.await
.expect("Failed to copy User table");

// A fulltext query against the copy must find the matching user,
// which is only possible if the tsvector column was copied.
let mut query =
user_query().filter(EntityFilter::Fulltext("userSearch".into(), "Shaq:*".into()));
query.block = BLOCK_NUMBER_MAX;
let entities = dst_layout
.query::<Entity>(&LOGGER, conn, query)
.await
.expect("fulltext query on copy failed")
.0;
let ids: Vec<_> = entities
.iter()
.map(|entity| match entity.get("id") {
Some(Value::String(id)) => id.clone(),
_ => panic!("entity without string id"),
})
.collect();
assert_eq!(ids, vec!["3".to_string()]);
})
.await;
}

#[graph::test]
async fn update() {
run_test(async |conn, layout| {
Expand Down
Loading