@@ -95,7 +95,8 @@ func (db *DB) LoadProjectIdentityPublicationDelta(
9595 }
9696
9797 where , args := projectIdentityPublicationChangeWhere (
98- "c" , afterRevision , throughRevision , projects , excludeProjects ,
98+ "c" , "c.project" , afterRevision , throughRevision ,
99+ projects , excludeProjects ,
99100 )
100101 rows , err := db .getReader ().QueryContext (ctx , `
101102 SELECT o.source_archive_id, o.source_archive_salt,
@@ -168,6 +169,10 @@ func (db *DB) LoadProjectIdentityPublicationDelta(
168169 return delta , fmt .Errorf ("closing project identity observation tombstones: %w" , err )
169170 }
170171
172+ snapshotWhere , snapshotArgs := projectIdentityPublicationChangeWhere (
173+ "c" , "owner.project" , afterRevision , throughRevision ,
174+ projects , excludeProjects ,
175+ )
171176 rows , err = db .getReader ().QueryContext (ctx , `
172177 SELECT s.session_id, s.project, s.machine, s.root_path, s.git_remote,
173178 s.git_remote_name, s.repository_path, s.worktree_name,
@@ -177,8 +182,11 @@ func (db *DB) LoadProjectIdentityPublicationDelta(
177182 FROM session_project_identity_snapshot_changes c
178183 JOIN session_project_identity_snapshots s
179184 ON s.session_id = c.session_id AND s.project = c.project
180- ` + where + ` AND c.deleted = 0
181- ORDER BY c.session_id, c.project` , args ... )
185+ JOIN sessions owner
186+ ON owner.id = s.session_id AND owner.deleted_at IS NULL
187+ ` + snapshotWhere + ` AND c.deleted = 0
188+ AND (TRIM(s.key_source) != '' OR TRIM(s.worktree_root_path) != '')
189+ ORDER BY c.session_id, c.project` , snapshotArgs ... )
182190 if err != nil {
183191 return delta , fmt .Errorf ("listing changed session project identity snapshots: %w" , err )
184192 }
@@ -213,11 +221,15 @@ func (db *DB) LoadProjectIdentityPublicationDelta(
213221 return delta , fmt .Errorf ("closing changed session project identity snapshots: %w" , err )
214222 }
215223
224+ tombstoneWhere , tombstoneArgs := projectIdentityPublicationChangeWhere (
225+ "c" , "c.project" , afterRevision , throughRevision ,
226+ projects , excludeProjects ,
227+ )
216228 rows , err = db .getReader ().QueryContext (ctx , `
217229 SELECT c.session_id, c.project
218230 FROM session_project_identity_snapshot_changes c
219- ` + where + ` AND c.deleted = 1
220- ORDER BY c.session_id, c.project` , args ... )
231+ ` + tombstoneWhere + ` AND c.deleted = 1
232+ ORDER BY c.session_id, c.project` , tombstoneArgs ... )
221233 if err != nil {
222234 return delta , fmt .Errorf ("listing session project identity snapshot tombstones: %w" , err )
223235 }
@@ -236,11 +248,13 @@ func (db *DB) LoadProjectIdentityPublicationDelta(
236248}
237249
238250func projectIdentityPublicationChangeWhere (
239- alias string ,
251+ revisionAlias string ,
252+ projectExpression string ,
240253 afterRevision , throughRevision int64 ,
241254 projects , excludeProjects []string ,
242255) (string , []any ) {
243- where := "WHERE " + alias + ".revision > ? AND " + alias + ".revision <= ?"
256+ where := "WHERE " + revisionAlias + ".revision > ? AND " +
257+ revisionAlias + ".revision <= ?"
244258 args := []any {afterRevision , throughRevision }
245259 appendProjects := func (values []string , negate bool ) {
246260 if len (values ) == 0 {
@@ -255,7 +269,7 @@ func projectIdentityPublicationChangeWhere(
255269 if negate {
256270 op = " NOT IN "
257271 }
258- where += " AND " + alias + ".project" + op +
272+ where += " AND " + projectExpression + op +
259273 "(" + strings .Join (placeholders , "," ) + ")"
260274 }
261275 appendProjects (projects , false )
@@ -1735,6 +1749,123 @@ func (db *DB) ListSessionProjectIdentitySnapshots(
17351749 return out , nil
17361750}
17371751
1752+ // ListPublishableSessionProjectIdentitySnapshots returns authoritative source
1753+ // snapshots owned by sessions in the current project scope. sessionIDs limits
1754+ // the result when non-nil; an empty non-nil slice returns no rows. Placeholder
1755+ // snapshots created before a session is inspected are deliberately excluded.
1756+ func (db * DB ) ListPublishableSessionProjectIdentitySnapshots (
1757+ ctx context.Context ,
1758+ sessionIDs , projects , excludeProjects []string ,
1759+ ) ([]export.ProjectIdentityObservation , error ) {
1760+ if ctx == nil {
1761+ ctx = context .Background ()
1762+ }
1763+ if sessionIDs != nil && len (sessionIDs ) == 0 {
1764+ return nil , nil
1765+ }
1766+
1767+ query := func (ids []string ) ([]export.ProjectIdentityObservation , error ) {
1768+ predicates := []string {
1769+ "owner.deleted_at IS NULL" ,
1770+ "(TRIM(snap.key_source) != '' OR TRIM(snap.worktree_root_path) != '')" ,
1771+ }
1772+ var args []any
1773+ appendSet := func (expression string , values []string , negate bool ) {
1774+ if len (values ) == 0 {
1775+ return
1776+ }
1777+ placeholders := make ([]string , len (values ))
1778+ for i , value := range values {
1779+ placeholders [i ] = "?"
1780+ args = append (args , value )
1781+ }
1782+ op := " IN "
1783+ if negate {
1784+ op = " NOT IN "
1785+ }
1786+ predicates = append (
1787+ predicates ,
1788+ expression + op + "(" + strings .Join (placeholders , "," )+ ")" ,
1789+ )
1790+ }
1791+ appendSet ("owner.project" , projects , false )
1792+ appendSet ("owner.project" , excludeProjects , true )
1793+ appendSet ("snap.session_id" , ids , false )
1794+
1795+ rows , err := db .getReader ().QueryContext (ctx , `
1796+ SELECT snap.session_id, snap.project, snap.machine, snap.root_path,
1797+ snap.git_remote, snap.git_remote_name, snap.repository_path,
1798+ snap.worktree_name, snap.worktree_root_path,
1799+ snap.worktree_relationship, snap.checkout_state,
1800+ snap.git_branch, snap.remote_resolution,
1801+ snap.remote_candidate_count, snap.observed_at,
1802+ snap.normalized_remote, snap.key_source, snap.key
1803+ FROM session_project_identity_snapshots snap
1804+ JOIN sessions owner ON owner.id = snap.session_id
1805+ WHERE ` + strings .Join (predicates , " AND " )+ `
1806+ ORDER BY snap.session_id` , args ... )
1807+ if err != nil {
1808+ return nil , fmt .Errorf (
1809+ "listing publishable session project identity snapshots: %w" ,
1810+ err ,
1811+ )
1812+ }
1813+ defer rows .Close ()
1814+
1815+ var out []export.ProjectIdentityObservation
1816+ for rows .Next () {
1817+ var obs export.ProjectIdentityObservation
1818+ var observedAt string
1819+ if err := rows .Scan (
1820+ & obs .SessionID , & obs .Project , & obs .Machine , & obs .RootPath ,
1821+ & obs .GitRemote , & obs .GitRemoteName , & obs .RepositoryPath ,
1822+ & obs .WorktreeName , & obs .WorktreeRootPath ,
1823+ & obs .WorktreeRelationship , & obs .CheckoutState ,
1824+ & obs .GitBranch , & obs .RemoteResolution ,
1825+ & obs .RemoteCandidateCount , & observedAt ,
1826+ & obs .NormalizedRemote , & obs .KeySource , & obs .Key ,
1827+ ); err != nil {
1828+ return nil , fmt .Errorf (
1829+ "scanning publishable session project identity snapshot: %w" ,
1830+ err ,
1831+ )
1832+ }
1833+ obs .ObservedAt , err = time .Parse (time .RFC3339Nano , observedAt )
1834+ if err != nil {
1835+ return nil , fmt .Errorf (
1836+ "parsing publishable session identity timestamp: %w" , err ,
1837+ )
1838+ }
1839+ out = append (out , obs )
1840+ }
1841+ if err := rows .Err (); err != nil {
1842+ return nil , fmt .Errorf (
1843+ "iterating publishable session project identity snapshots: %w" ,
1844+ err ,
1845+ )
1846+ }
1847+ return out , nil
1848+ }
1849+
1850+ if sessionIDs == nil {
1851+ return query (nil )
1852+ }
1853+ chunkSize := maxSQLVars - len (projects ) - len (excludeProjects )
1854+ if chunkSize < 1 {
1855+ chunkSize = 1
1856+ }
1857+ var out []export.ProjectIdentityObservation
1858+ err := queryChunkedSize (sessionIDs , chunkSize , func (ids []string ) error {
1859+ rows , err := query (ids )
1860+ if err != nil {
1861+ return err
1862+ }
1863+ out = append (out , rows ... )
1864+ return nil
1865+ })
1866+ return out , err
1867+ }
1868+
17381869func (db * DB ) BuildProjectIdentityMap (
17391870 ctx context.Context ,
17401871 labels []string ,
0 commit comments