From b071e1f8f4c8eaaf46aa360ad096611bd2b143bf Mon Sep 17 00:00:00 2001 From: Mia Date: Sun, 12 Oct 2025 11:07:21 +0100 Subject: [PATCH] feat(lexica): getPostThreadV2 lexicon Change-Id: snkqxsmqlxnurxsotuowrkystrspyyop --- lexica/src/app_bsky/mod.rs | 1 + lexica/src/app_bsky/unspecced.rs | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 lexica/src/app_bsky/unspecced.rs diff --git a/lexica/src/app_bsky/mod.rs b/lexica/src/app_bsky/mod.rs index 4505c3d..6296e53 100644 --- a/lexica/src/app_bsky/mod.rs +++ b/lexica/src/app_bsky/mod.rs @@ -7,6 +7,7 @@ pub mod feed; pub mod graph; pub mod labeler; pub mod richtext; +pub mod unspecced; #[derive(Clone, Default, Debug, Serialize)] #[serde(rename_all = "camelCase")] diff --git a/lexica/src/app_bsky/unspecced.rs b/lexica/src/app_bsky/unspecced.rs new file mode 100644 index 0000000..e4b906a --- /dev/null +++ b/lexica/src/app_bsky/unspecced.rs @@ -0,0 +1,33 @@ +use crate::app_bsky::feed::{BlockedAuthor, PostView}; +use serde::Serialize; + +#[derive(Clone, Debug, Serialize)] +pub struct ThreadV2Item { + pub uri: String, + pub depth: i32, + pub value: ThreadV2ItemType, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(tag = "$type")] +pub enum ThreadV2ItemType { + #[serde(rename = "app.bsky.unspecced.defs#threadItemPost")] + Post(ThreadItemPost), + #[serde(rename = "app.bsky.unspecced.defs#threadItemNoUnauthenticated")] + NoUnauthenticated {}, + #[serde(rename = "app.bsky.unspecced.defs#threadItemNotFound")] + NotFound {}, + #[serde(rename = "app.bsky.unspecced.defs#threadItemBlocked")] + Blocked { author: BlockedAuthor }, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ThreadItemPost { + pub post: PostView, + pub more_parents: bool, + pub more_replies: i32, + pub op_thread: bool, + pub hidden_by_threadgate: bool, + pub muted_by_viewer: bool, +} -- 2.43.0 From e38a2ff5dc681377ca660b1b6811a753cea8a65a Mon Sep 17 00:00:00 2001 From: Mia Date: Sun, 12 Oct 2025 13:40:30 +0100 Subject: [PATCH] refactor(parakeet): move thread getters out for reuse Change-Id: wvnysqtqkkzwqzmuyrmzwkzsotqspqxp --- parakeet/src/db.rs | 40 +++++++++++++++++++++++- parakeet/src/xrpc/app_bsky/feed/posts.rs | 26 ++------------- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/parakeet/src/db.rs b/parakeet/src/db.rs index 6f708d7..5343eb8 100644 --- a/parakeet/src/db.rs +++ b/parakeet/src/db.rs @@ -1,5 +1,5 @@ use diesel::prelude::*; -use diesel::sql_types::{Array, Bool, Nullable, Text}; +use diesel::sql_types::{Array, Bool, Integer, Nullable, Text}; use diesel_async::{AsyncPgConnection, RunQueryDsl}; use parakeet_db::{schema, types}; @@ -196,3 +196,41 @@ pub async fn get_pinned_post_uri( .await .optional() } + +#[derive(Debug, QueryableByName)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[allow(unused)] +pub struct ThreadItem { + #[diesel(sql_type = Text)] + pub at_uri: String, + #[diesel(sql_type = Nullable)] + pub parent_uri: Option, + #[diesel(sql_type = Nullable)] + pub root_uri: Option, + #[diesel(sql_type = Integer)] + pub depth: i32, +} + +pub async fn get_thread_children( + conn: &mut AsyncPgConnection, + uri: &str, + depth: i32, +) -> QueryResult> { + diesel::sql_query(include_str!("sql/thread.sql")) + .bind::(uri) + .bind::(depth) + .load(conn) + .await +} + +pub async fn get_thread_parents( + conn: &mut AsyncPgConnection, + uri: &str, + height: i32, +) -> QueryResult> { + diesel::sql_query(include_str!("sql/thread_parent.sql")) + .bind::(uri) + .bind::(height) + .load(conn) + .await +} diff --git a/parakeet/src/xrpc/app_bsky/feed/posts.rs b/parakeet/src/xrpc/app_bsky/feed/posts.rs index f27bf44..13e79fa 100644 --- a/parakeet/src/xrpc/app_bsky/feed/posts.rs +++ b/parakeet/src/xrpc/app_bsky/feed/posts.rs @@ -361,19 +361,6 @@ pub struct GetPostThreadRes { pub threadgate: Option, } -#[derive(Debug, QueryableByName)] -#[diesel(check_for_backend(diesel::pg::Pg))] -struct ThreadItem { - #[diesel(sql_type = diesel::sql_types::Text)] - at_uri: String, - #[diesel(sql_type = diesel::sql_types::Nullable)] - parent_uri: Option, - // #[diesel(sql_type = diesel::sql_types::Nullable)] - // root_uri: Option, - #[diesel(sql_type = diesel::sql_types::Integer)] - depth: i32, -} - pub async fn get_post_thread( State(state): State, AtpAcceptLabelers(labelers): AtpAcceptLabelers, @@ -409,17 +396,8 @@ pub async fn get_post_thread( } } - let replies = diesel::sql_query(include_str!("../../../sql/thread.sql")) - .bind::(&uri) - .bind::(depth as i32) - .load::(&mut conn) - .await?; - - let parents = diesel::sql_query(include_str!("../../../sql/thread_parent.sql")) - .bind::(&uri) - .bind::(parent_height as i32) - .load::(&mut conn) - .await?; + let replies = crate::db::get_thread_children(&mut conn, &uri, depth as i32).await?; + let parents = crate::db::get_thread_parents(&mut conn, &uri, parent_height as i32).await?; let reply_uris = replies.iter().map(|item| item.at_uri.clone()).collect(); let parent_uris = parents.iter().map(|item| item.at_uri.clone()).collect(); -- 2.43.0 From 7949cd60118120939586b85cf1dc81645480a5bf Mon Sep 17 00:00:00 2001 From: Mia Date: Sun, 12 Oct 2025 13:05:14 +0100 Subject: [PATCH] feat(parakeet): getPostThreadV2 (and ...Other) Change-Id: rrkymxnzoyyklsynoykpnzsoxrukutwr --- parakeet-db/src/models.rs | 1 + parakeet/src/db.rs | 56 +++ parakeet/src/sql/thread_branching.sql | 13 + .../src/sql/thread_v2_hidden_children.sql | 6 + parakeet/src/xrpc/app_bsky/mod.rs | 3 + parakeet/src/xrpc/app_bsky/unspecced/mod.rs | 1 + .../src/xrpc/app_bsky/unspecced/thread_v2.rs | 382 ++++++++++++++++++ 7 files changed, 462 insertions(+) create mode 100644 parakeet/src/sql/thread_branching.sql create mode 100644 parakeet/src/sql/thread_v2_hidden_children.sql create mode 100644 parakeet/src/xrpc/app_bsky/unspecced/mod.rs create mode 100644 parakeet/src/xrpc/app_bsky/unspecced/thread_v2.rs diff --git a/parakeet-db/src/models.rs b/parakeet-db/src/models.rs index 69c78c4..dd32417 100644 --- a/parakeet-db/src/models.rs +++ b/parakeet-db/src/models.rs @@ -431,6 +431,7 @@ pub struct AuthorFeedItem { pub sort_at: DateTime, } +pub use not_null_vec::TextArray; mod not_null_vec { use diesel::deserialize::FromSql; use diesel::pg::Pg; diff --git a/parakeet/src/db.rs b/parakeet/src/db.rs index 5343eb8..222d8f7 100644 --- a/parakeet/src/db.rs +++ b/parakeet/src/db.rs @@ -2,6 +2,7 @@ use diesel::prelude::*; use diesel::sql_types::{Array, Bool, Integer, Nullable, Text}; use diesel_async::{AsyncPgConnection, RunQueryDsl}; use parakeet_db::{schema, types}; +use parakeet_db::models::TextArray; pub async fn get_actor_status( conn: &mut AsyncPgConnection, @@ -223,6 +224,39 @@ pub async fn get_thread_children( .await } +pub async fn get_thread_children_branching( + conn: &mut AsyncPgConnection, + uri: &str, + depth: i32, + branching_factor: i32, +) -> QueryResult> { + diesel::sql_query(include_str!("sql/thread_branching.sql")) + .bind::(uri) + .bind::(depth) + .bind::(branching_factor) + .load(conn) + .await +} + +#[derive(Debug, QueryableByName)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct HiddenThreadChildItem { + #[diesel(sql_type = Text)] + pub at_uri: String, +} + +pub async fn get_thread_children_hidden( + conn: &mut AsyncPgConnection, + uri: &str, + root: &str, +) -> QueryResult> { + diesel::sql_query(include_str!("sql/thread_v2_hidden_children.sql")) + .bind::(uri) + .bind::(root) + .load(conn) + .await +} + pub async fn get_thread_parents( conn: &mut AsyncPgConnection, uri: &str, @@ -234,3 +268,25 @@ pub async fn get_thread_parents( .load(conn) .await } + +pub async fn get_root_post(conn: &mut AsyncPgConnection, uri: &str) -> QueryResult> { + schema::posts::table + .select(schema::posts::root_uri) + .find(&uri) + .get_result(conn) + .await + .optional() + .map(|v| v.flatten()) +} + +pub async fn get_threadgate_hiddens( + conn: &mut AsyncPgConnection, + uri: &str, +) -> QueryResult> { + schema::threadgates::table + .select(schema::threadgates::hidden_replies) + .find(&uri) + .get_result(conn) + .await + .optional() +} diff --git a/parakeet/src/sql/thread_branching.sql b/parakeet/src/sql/thread_branching.sql new file mode 100644 index 0000000..1229d4e --- /dev/null +++ b/parakeet/src/sql/thread_branching.sql @@ -0,0 +1,13 @@ +with recursive thread as (select at_uri, parent_uri, root_uri, 0 as depth + from posts + where parent_uri = $1 + and violates_threadgate = FALSE + union all + (select p.at_uri, p.parent_uri, p.root_uri, thread.depth + 1 + from posts p + join thread on p.parent_uri = thread.at_uri + where thread.depth <= $2 + and violates_threadgate = FALSE + LIMIT $3)) +select * +from thread; diff --git a/parakeet/src/sql/thread_v2_hidden_children.sql b/parakeet/src/sql/thread_v2_hidden_children.sql new file mode 100644 index 0000000..3271092 --- /dev/null +++ b/parakeet/src/sql/thread_v2_hidden_children.sql @@ -0,0 +1,6 @@ +select at_uri +from posts +where parent_uri = $1 + and at_uri = any (select unnest(hidden_replies) + from threadgates + where post_uri = $2) \ No newline at end of file diff --git a/parakeet/src/xrpc/app_bsky/mod.rs b/parakeet/src/xrpc/app_bsky/mod.rs index ca6a74d..93ce228 100644 --- a/parakeet/src/xrpc/app_bsky/mod.rs +++ b/parakeet/src/xrpc/app_bsky/mod.rs @@ -6,6 +6,7 @@ mod bookmark; mod feed; mod graph; mod labeler; +mod unspecced; #[rustfmt::skip] pub fn routes() -> Router { @@ -64,6 +65,8 @@ pub fn routes() -> Router { // TODO: app.bsky.notification.putActivitySubscriptions // TODO: app.bsky.notification.putPreferences // TODO: app.bsky.notification.putPreferencesV2 + .route("/app.bsky.unspecced.getPostThreadV2", get(unspecced::thread_v2::get_post_thread_v2)) + .route("/app.bsky.unspecced.getPostThreadOtherV2", get(unspecced::thread_v2::get_post_thread_other_v2)) } async fn not_implemented() -> axum::http::StatusCode { diff --git a/parakeet/src/xrpc/app_bsky/unspecced/mod.rs b/parakeet/src/xrpc/app_bsky/unspecced/mod.rs new file mode 100644 index 0000000..712eabb --- /dev/null +++ b/parakeet/src/xrpc/app_bsky/unspecced/mod.rs @@ -0,0 +1 @@ +pub mod thread_v2; diff --git a/parakeet/src/xrpc/app_bsky/unspecced/thread_v2.rs b/parakeet/src/xrpc/app_bsky/unspecced/thread_v2.rs new file mode 100644 index 0000000..20c5e6b --- /dev/null +++ b/parakeet/src/xrpc/app_bsky/unspecced/thread_v2.rs @@ -0,0 +1,382 @@ +use crate::db::ThreadItem; +use crate::hydration::StatefulHydrator; +use crate::xrpc::error::{Error, XrpcResult}; +use crate::xrpc::extract::{AtpAcceptLabelers, AtpAuth}; +use crate::xrpc::normalise_at_uri; +use crate::GlobalState; +use axum::extract::{Query, State}; +use axum::Json; +use itertools::Itertools; +use lexica::app_bsky::feed::{BlockedAuthor, PostView, ThreadgateView}; +use lexica::app_bsky::unspecced::{ThreadItemPost, ThreadV2Item, ThreadV2ItemType}; +use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; +use std::collections::{HashMap, HashSet}; + +const THREAD_PARENTS: usize = 50; +const DEFAULT_BRANCHING: u32 = 10; +const DEFAULT_DEPTH: u32 = 6; + +#[derive(Copy, Clone, Debug, Default, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum PostThreadSort { + Newest, + #[default] + Oldest, + Top, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetPostThreadV2Req { + pub anchor: String, + pub above: Option, + pub below: Option, + pub branching_factor: Option, + #[serde(default)] + pub sort: PostThreadSort, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetPostThreadV2Res { + pub thread: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub threadgate: Option, + pub has_other_replies: bool, +} + +pub async fn get_post_thread_v2( + State(state): State, + AtpAcceptLabelers(labelers): AtpAcceptLabelers, + maybe_auth: Option, + Query(query): Query, +) -> XrpcResult> { + let mut conn = state.pool.get().await?; + let maybe_did = maybe_auth.clone().map(|v| v.0); + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); + + let uri = normalise_at_uri(&state.dataloaders, &query.anchor).await?; + let depth = query.below.unwrap_or(DEFAULT_DEPTH).clamp(0, 20) as i32; + let branching_factor = query + .branching_factor + .unwrap_or(DEFAULT_BRANCHING) + .clamp(0, 100) as i32; + + let anchor = hyd + .hydrate_post(uri.clone()) + .await + .ok_or(Error::not_found())?; + + if let Some(v) = &anchor.author.viewer { + if v.blocked_by || v.blocking.is_some() { + let block = ThreadV2ItemType::Blocked { + author: BlockedAuthor { + did: anchor.author.did, + viewer: anchor.author.viewer, + }, + }; + + return Ok(Json(GetPostThreadV2Res { + thread: vec![ThreadV2Item { + uri, + depth: 0, + value: block, + }], + threadgate: anchor.threadgate, + has_other_replies: false, + })); + } + } + + // get the root post URI (if there is one) and return its author's DID. + let root_uri = crate::db::get_root_post(&mut conn, &uri) + .await? + .unwrap_or(uri.clone()); + let root_did = root_uri[5..].split('/').collect::>()[0]; + + let replies = + crate::db::get_thread_children_branching(&mut conn, &uri, depth, branching_factor + 1) + .await?; + let reply_uris = replies + .iter() + .map(|item| item.at_uri.clone()) + .collect::>(); + + // bluesky seems to use -50 atm. we get 1 extra to know if to set more_parents. + let parents = match query.above.unwrap_or(true) { + true => crate::db::get_thread_parents(&mut conn, &uri, THREAD_PARENTS as i32 + 1).await?, + false => vec![], + }; + let parent_uris = parents + .iter() + .map(|item| item.at_uri.clone()) + .collect::>(); + + let (mut replies_hyd, mut parents_hyd) = tokio::join!( + hyd.hydrate_posts(reply_uris), + hyd.hydrate_posts(parent_uris), + ); + + let threadgate = anchor.threadgate.clone(); + let hidden: HashSet<_, std::hash::RandomState> = match &threadgate { + Some(tg) => crate::db::get_threadgate_hiddens(&mut conn, &tg.uri).await?, + None => None, + } + .map(|hiddens| HashSet::from_iter(Vec::from(hiddens))) + .unwrap_or_default(); + + let root_has_more = parents.len() > THREAD_PARENTS; + let mut is_op_thread = true; + + let mut thread = Vec::with_capacity(1 + replies.len() + parents.len()); + + thread.extend( + parents + .into_iter() + .tail(THREAD_PARENTS) + .enumerate() + .map(|(idx, item)| { + let value = parents_hyd + .remove(&item.at_uri) + .map(|post| { + if let Some(v) = &post.author.viewer { + if v.blocked_by || v.blocking.is_some() { + return ThreadV2ItemType::Blocked { + author: BlockedAuthor { + did: post.author.did, + viewer: post.author.viewer, + }, + }; + } + } + + let op_thread = (is_op_thread + || item.root_uri.is_none() && item.parent_uri.is_none()) + && post.author.did == root_did; + + ThreadV2ItemType::Post(ThreadItemPost { + post, + more_parents: idx == 0 && root_has_more, + more_replies: 0, + op_thread, + hidden_by_threadgate: false, + muted_by_viewer: false, + }) + }) + .unwrap_or(ThreadV2ItemType::NotFound {}); + + ThreadV2Item { + uri: item.at_uri, + depth: -item.depth - 1, + value, + } + }), + ); + + is_op_thread = is_op_thread && anchor.author.did == root_did; + thread.push(ThreadV2Item { + uri: uri.clone(), + depth: 0, + value: ThreadV2ItemType::Post(ThreadItemPost { + post: anchor, + more_parents: false, + more_replies: 0, + op_thread: is_op_thread, + hidden_by_threadgate: false, + muted_by_viewer: false, + }), + }); + + let mut replies_grouped = replies + .into_iter() + .into_group_map_by(|item| item.parent_uri.clone().unwrap_or_default()); + + // start with the anchor + let (children, has_other_replies) = build_thread_children( + &mut replies_grouped, + &mut replies_hyd, + &hidden, + &uri, + is_op_thread, + 1, + &BuildThreadChildrenOpts { + root_did, + sort: query.sort, + maybe_did: &maybe_did, + max_depth: depth, + }, + ); + thread.extend(children); + + Ok(Json(GetPostThreadV2Res { + thread, + threadgate, + has_other_replies, + })) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetPostThreadOtherV2Req { + pub anchor: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetPostThreadOtherV2Res { + pub thread: Vec, +} + +pub async fn get_post_thread_other_v2( + State(state): State, + AtpAcceptLabelers(labelers): AtpAcceptLabelers, + maybe_auth: Option, + Query(query): Query, +) -> XrpcResult> { + let mut conn = state.pool.get().await?; + let hyd = StatefulHydrator::new(&state.dataloaders, &state.cdn, &labelers, maybe_auth); + + let uri = normalise_at_uri(&state.dataloaders, &query.anchor).await?; + + let root = crate::db::get_root_post(&mut conn, &uri) + .await? + .unwrap_or(uri.clone()); + + // this only returns immediate children (depth==1) where hiddenByThreadgate=TRUE + let replies = crate::db::get_thread_children_hidden(&mut conn, &uri, &root).await?; + let reply_uris = replies + .into_iter() + .map(|item| item.at_uri) + .collect::>(); + let thread = hyd + .hydrate_posts(reply_uris) + .await + .into_iter() + .filter(|(_, post)| match &post.author.viewer { + Some(viewer) if viewer.blocked_by || viewer.blocking.is_some() => false, + _ => true, + }) + .map(|(uri, post)| { + let post = ThreadItemPost { + post, + more_parents: false, + more_replies: 0, + op_thread: false, + hidden_by_threadgate: true, + muted_by_viewer: false, + }; + + ThreadV2Item { + uri, + depth: 1, + value: ThreadV2ItemType::Post(post), + } + }) + .collect(); + + Ok(Json(GetPostThreadOtherV2Res { thread })) +} + +#[derive(Debug)] +struct BuildThreadChildrenOpts<'a> { + root_did: &'a str, + sort: PostThreadSort, + maybe_did: &'a Option, + max_depth: i32, +} + +fn build_thread_children( + grouped_replies: &mut HashMap>, + replies_hyd: &mut HashMap, + hidden: &HashSet, + parent: &str, + is_op_thread: bool, + depth: i32, + opts: &BuildThreadChildrenOpts, +) -> (Vec, bool) { + let mut has_other_replies = false; + + let Some(replies) = grouped_replies.remove(parent) else { + return (Vec::default(), has_other_replies); + }; + + let replies = replies + .into_iter() + .filter_map(|item| replies_hyd.remove(&item.at_uri)) + .sorted_by(sort_replies(&opts.sort)); + + let mut out = Vec::new(); + + for post in replies { + let reply_count = grouped_replies + .get(&post.uri) + .map(|v| v.len()) + .unwrap_or_default(); + let at_max = depth == opts.max_depth; + let more_replies = if at_max { reply_count } else { 0 }; + let op_thread = is_op_thread && post.author.did == opts.root_did; + + // shouldn't push to the thread if there's a block relation. Bsky doesn't push a type of Blocked for replies... + if let Some(v) = &post.author.viewer { + if v.blocked_by || v.blocking.is_some() { + continue; + } + } + + // check if the post is hidden AND we're NOT the author (hidden posts still show for their author) + if hidden.contains(&post.uri) && !did_is_cur(opts.maybe_did, &post.author.did) { + // post is hidden - do not ~pass go~ push to the thread. + if depth == 1 { + has_other_replies = true; + } + continue; + } + + let uri = post.uri.clone(); + out.push(ThreadV2Item { + uri: post.uri.clone(), + depth, + value: ThreadV2ItemType::Post(ThreadItemPost { + post, + more_parents: false, + more_replies: more_replies as i32, + op_thread, + hidden_by_threadgate: false, + muted_by_viewer: false, + }), + }); + + if !at_max { + // we don't care about has_other_replies when recursing + let (children, _) = build_thread_children( + grouped_replies, + replies_hyd, + hidden, + &uri, + op_thread, + depth + 1, + opts, + ); + + out.extend(children); + } + } + + (out, has_other_replies) +} + +fn sort_replies(sort: &PostThreadSort) -> impl Fn(&PostView, &PostView) -> Ordering + use<'_> { + move |a: &PostView, b: &PostView| match sort { + PostThreadSort::Newest => b.indexed_at.cmp(&a.indexed_at), + PostThreadSort::Oldest => a.indexed_at.cmp(&b.indexed_at), + PostThreadSort::Top => b.stats.like_count.cmp(&a.stats.like_count), + } +} + +fn did_is_cur(cur: &Option, did: &String) -> bool { + match cur { + Some(cur) => did == cur, + None => false, + } +} -- 2.43.0 From db50a40521ea2616b16cb834e9abbd60b050c6bd Mon Sep 17 00:00:00 2001 From: Mia Date: Sun, 12 Oct 2025 19:24:34 +0100 Subject: [PATCH] fix(parakeet): post thread depth off by one Change-Id: toprokrqrnrkqmxwronrlunsyxnkpwln --- parakeet/src/sql/thread.sql | 2 +- parakeet/src/sql/thread_branching.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/parakeet/src/sql/thread.sql b/parakeet/src/sql/thread.sql index 41ea953..87f5dc3 100644 --- a/parakeet/src/sql/thread.sql +++ b/parakeet/src/sql/thread.sql @@ -1,4 +1,4 @@ -with recursive thread as (select at_uri, parent_uri, root_uri, 0 as depth +with recursive thread as (select at_uri, parent_uri, root_uri, 1 as depth from posts where parent_uri = $1 and violates_threadgate=FALSE union all diff --git a/parakeet/src/sql/thread_branching.sql b/parakeet/src/sql/thread_branching.sql index 1229d4e..a52f7da 100644 --- a/parakeet/src/sql/thread_branching.sql +++ b/parakeet/src/sql/thread_branching.sql @@ -1,4 +1,4 @@ -with recursive thread as (select at_uri, parent_uri, root_uri, 0 as depth +with recursive thread as (select at_uri, parent_uri, root_uri, 1 as depth from posts where parent_uri = $1 and violates_threadgate = FALSE -- 2.43.0