You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@texera.apache.org by "yangzhang75 (via GitHub)" <gi...@apache.org> on 2026/08/23 00:05:34 UTC

[PR] feat(workflow): freeze a pinned version as the public copy [texera]

yangzhang75 opened a new pull request, #7853:
URL: https://github.com/apache/texera/pull/7853

   Part of #7828. Stacked on #7851 — until that merges this PR shows its commit too; the review here is the second commit, `feat(workflow): freeze a pinned version as the public copy`.
   
   A public workflow follows the author's latest content, as publishing has always done. This adds the other state: the author pins the version they have now, and the public copy stops moving until they pin again.
   
   ## What lands
   
   `is_public` stays the on/off switch. `published_content` is the pin — NULL means following, non-NULL is the frozen copy.
   
   `WorkflowPublishService` owns the two states and the moves between them (`publish`, `pinLatest`, `unpin`, `unpublish`, `statusOf`), and three endpoints expose them:
   
   | Endpoint | Does |
   |---|---|
   | `POST /workflow/pin/{wid}` | freezes the author's current version as the public copy; called again, moves the pin forward |
   | `DELETE /workflow/pin/{wid}` | drops the pin, back to following the latest |
   | `GET /workflow/publish-status/{wid}` | published, pinned, and whether a pin is holding edits back |
   
   Publishing and unpublishing move through the same service, so unpublishing drops the pin rather than leaving a private workflow carrying one. Re-publishing starts in the following state: coming back should not silently put old public content on show again.
   
   ## Two paths narrowed so a pin can hold
   
   Saving a workflow read the whole row and wrote it all back. Two consequences, both fixed here by writing only name, description and content:
   
   - a publish landing while a save was in flight was silently rolled back to what the save had read;
   - a request body could set the publish columns itself.
   
   Creating a workflow clears those columns for the same reason.
   
   ## Nothing on screen changes
   
   No read path consults the pinned copy yet, and there is no UI. Every workflow is in the following state it is in today. The endpoints answer, and nothing calls them.
   
   ## Tests
   
   19 backend cases in `WorkflowPublishSpec`: the state machine (follow → pin → re-pin → unpin → unpublish → re-publish), the guards (no write access, not published, workflow missing), that a create cannot inject publish columns, and that a save, a collaborator's save and a rename all leave the publish state alone.
   
   `hasUnpublishedChanges` compares the two copies as parsed JSON rather than as strings — the same graph can come back with its keys in another order, and reporting that as an edit is an alarm the author cannot clear. One case covers it.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(workflow): freeze a pinned version as the public copy [texera]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #7853:
URL: https://github.com/apache/texera/pull/7853#issuecomment-5383280792

   <!-- texera-reviewer-suggestion -->
   ### Automated Reviewer Suggestions
   
   Based on the `git blame` history of the changed files, we recommend the following reviewers:
   
   - **Contributors with relevant context:** `@tanishqgandhi1908`, `@aglinxinyuan`, `@Ma77Ball`
     You can notify them by mentioning `@tanishqgandhi1908`, `@aglinxinyuan`, `@Ma77Ball` in a comment.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(workflow): freeze a pinned version as the public copy [texera]

Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #7853:
URL: https://github.com/apache/texera/pull/7853#discussion_r4119233786


##########
amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowPublishService.scala:
##########
@@ -0,0 +1,204 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.web.resource.dashboard.user.workflow
+
+import com.typesafe.scalalogging.LazyLogging
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.apache.texera.dao.SqlServer
+import org.apache.texera.dao.jooq.generated.Tables.WORKFLOW
+import org.apache.texera.dao.jooq.generated.enums.DefaultViewEnum
+import org.apache.texera.dao.jooq.generated.tables.daos.WorkflowDao
+import org.apache.texera.dao.jooq.generated.tables.pojos.Workflow
+import org.jooq.DSLContext
+
+import javax.ws.rs.{BadRequestException, NotFoundException}
+import scala.util.Try
+
+/**
+  * Version pinning for public workflows.
+  *
+  * A public workflow follows the author's latest, as publishing has always done, until the author
+  * pins the version they have now: the public then keeps seeing that frozen copy while the author's
+  * later edits stay in the workflow's own columns until they pin again.
+  *
+  * `is_public` stays the on/off switch; `published_content` is the pin, NULL while following. A pin
+  * freezes everything on public show -- the graph, the title, the description and the view it opens
+  * in -- because a copy that froze only its graph would still advertise a title nobody published.
+  *
+  * Not to be confused with sharing: a user granted access always tracks the author's latest, pin or
+  * no pin. Only viewers who arrive because the workflow is public are held at the frozen copy.
+  */
+object WorkflowPublishService extends LazyLogging {
+
+  private def context: DSLContext = SqlServer.getInstance().createDSLContext()
+
+  /**
+    * What the share dialog asks about: whether the workflow is public, whether a version is pinned,
+    * and whether that pin is holding edits back -- the last is true when pinning again would publish
+    * something, and always false while following.
+    */
+  case class PublishStatus(
+      isPublished: Boolean,
+      isPinned: Boolean,
+      hasUnpublishedChanges: Boolean
+  )
+
+  /**
+    * Whether two workflow contents describe the same graph. Compared as parsed trees, because the
+    * two blobs travel by different routes and the same graph can come back with its whitespace or
+    * key order rearranged -- reporting that as an edit the public cannot see would be an alarm the
+    * author cannot clear.
+    */
+  private def sameContent(a: String, b: String): Boolean =
+    a == b || Try(objectMapper.readTree(a) == objectMapper.readTree(b)).getOrElse(false)
+
+  /** The workflow, or a 404. */
+  private def requireWorkflow(wid: Integer): Workflow =
+    Option(new WorkflowDao(context.configuration).fetchOneByWid(wid))
+      .getOrElse(throw new NotFoundException(s"Workflow $wid not found"))
+
+  /**
+    * Turns publishing on, and touches nothing else. A workflow coming back from private is
+    * following the author's latest, because unpublishing always drops the pin: coming back should
+    * not silently put old public content back on show. Called on a workflow that is already public
+    * it changes nothing, pin included.
+    */
+  def publish(wid: Integer): PublishStatus = {
+    val updated = context
+      .update(WORKFLOW)
+      .set(WORKFLOW.IS_PUBLIC, java.lang.Boolean.TRUE)
+      .where(WORKFLOW.WID.eq(wid))
+      .execute()
+    if (updated == 0) {
+      throw new NotFoundException(s"Workflow $wid not found")
+    }
+    // Deliberately not "following latest": this turns publishing on and touches nothing else, so a
+    // workflow that somehow arrives here already pinned stays pinned.
+    logger.info(s"Workflow $wid published")
+    statusOf(wid)
+  }
+
+  /**
+    * Freezes the author's current copy as the public one. The title, the description and the
+    * default view freeze with the graph: they are as public as it is, and the database refuses a
+    * pinned copy that carries only part of itself. The view matters because a form's definition
+    * rides inside the content -- serving the live preference over a frozen graph would open a form
+    * on a copy that has none.
+    *
+    * Each column is copied from its own row rather than from a workflow read a moment earlier, so
+    * there is no window in which the author's next save lands and the pin freezes the version
+    * before it -- which would leave them looking at "you have unpublished changes" the instant
+    * after they pinned.
+    *
+    * Publishing is a condition, not something this writes: asking to be published and pinning are
+    * two statements, so a `/private` landing between them would otherwise be undone here, putting a
+    * workflow the author had just taken down back on show under a frozen copy.
+    *
+    * @return how many rows it matched -- zero for a workflow that is missing or not published.
+    */
+  private def writePin(wid: Integer): Int =
+    context
+      .update(WORKFLOW)
+      .set(WORKFLOW.PUBLISHED_CONTENT, WORKFLOW.CONTENT)
+      .set(WORKFLOW.PUBLISHED_NAME, WORKFLOW.NAME)
+      .set(WORKFLOW.PUBLISHED_DESCRIPTION, WORKFLOW.DESCRIPTION)
+      .set(WORKFLOW.PUBLISHED_DEFAULT_VIEW, WORKFLOW.DEFAULT_VIEW)
+      .where(WORKFLOW.WID.eq(wid).and(WORKFLOW.IS_PUBLIC.isTrue))
+      .execute()
+
+  /**
+    * Clears the pinned copy in one statement, optionally unpublishing too: the constraint accepts a
+    * row only with every frozen column set on a public workflow, or with every one of them NULL, so
+    * clearing them one at a time -- or clearing them after `is_public` -- would be rejected.
+    *
+    * `published_version_id` is named by that constraint as well but is not touched here, for the
+    * same reason [[writePin]] does not set it: nothing writes it yet, so it is NULL on every row.
+    *
+    * @return how many rows it matched, so a missing workflow is distinguishable from a done one.
+    */
+  private def clearPin(wid: Integer, alsoUnpublish: Boolean = false): Int = {
+    val cleared = context
+      .update(WORKFLOW)
+      .set(WORKFLOW.PUBLISHED_CONTENT, null.asInstanceOf[String])
+      .set(WORKFLOW.PUBLISHED_NAME, null.asInstanceOf[String])
+      .set(WORKFLOW.PUBLISHED_DESCRIPTION, null.asInstanceOf[String])
+      .set(WORKFLOW.PUBLISHED_DEFAULT_VIEW, null.asInstanceOf[DefaultViewEnum])
+    val statement =
+      if (alsoUnpublish) cleared.set(WORKFLOW.IS_PUBLIC, java.lang.Boolean.FALSE) else cleared
+    statement.where(WORKFLOW.WID.eq(wid)).execute()

Review Comment:
   `clearPin` also matches private rows. Because `requirePublishable` and this update are separate statements, `/private` can commit between them; `unpin` then reports success (and logs “following latest”) with `isPublished = false`, contrary to the endpoint's public-only contract. Make the unpin update conditional on `IS_PUBLIC = true`, and when no row matches distinguish an existing private workflow from a missing one as `pinLatest` does.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(workflow): freeze a pinned version as the public copy [texera]

Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #7853:
URL: https://github.com/apache/texera/pull/7853#discussion_r3936808013


##########
amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowPublishService.scala:
##########
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.web.resource.dashboard.user.workflow
+
+import com.typesafe.scalalogging.LazyLogging
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.apache.texera.dao.SqlServer
+import org.apache.texera.dao.jooq.generated.Tables.WORKFLOW
+import org.apache.texera.dao.jooq.generated.enums.DefaultViewEnum
+import org.apache.texera.dao.jooq.generated.tables.daos.WorkflowDao
+import org.apache.texera.dao.jooq.generated.tables.pojos.Workflow
+import org.jooq.DSLContext
+
+import javax.ws.rs.NotFoundException
+import scala.util.Try
+
+/**
+  * Version pinning for public workflows.
+  *
+  * A public workflow follows the author's latest, as publishing has always done, until the author
+  * pins the version they have now: the public then keeps seeing that frozen copy while the author's
+  * later edits stay in the workflow's own columns until they pin again.
+  *
+  * `is_public` stays the on/off switch; `published_content` is the pin, NULL while following. A pin
+  * freezes everything on public show -- the graph, the title, the description and the view it opens
+  * in -- because a copy that froze only its graph would still advertise a title nobody published.
+  *
+  * Not to be confused with sharing: a user granted access always tracks the author's latest, pin or
+  * no pin. Only viewers who arrive because the workflow is public are held at the frozen copy.
+  */
+object WorkflowPublishService extends LazyLogging {
+
+  private def context: DSLContext = SqlServer.getInstance().createDSLContext()
+
+  /**
+    * What the share dialog asks about: whether the workflow is public, whether a version is pinned,
+    * and whether that pin is holding edits back -- the last is true when pinning again would publish
+    * something, and always false while following.
+    */
+  case class PublishStatus(
+      isPublished: Boolean,
+      isPinned: Boolean,
+      hasUnpublishedChanges: Boolean
+  )
+
+  /**
+    * Whether two workflow contents describe the same graph. Compared as parsed trees, because the
+    * two blobs travel by different routes and the same graph can come back with its whitespace or
+    * key order rearranged -- reporting that as an edit the public cannot see would be an alarm the
+    * author cannot clear.
+    */
+  private def sameContent(a: String, b: String): Boolean =
+    a == b || Try(objectMapper.readTree(a) == objectMapper.readTree(b)).getOrElse(false)
+
+  /** The workflow, or a 404. */
+  private def requireWorkflow(wid: Integer): Workflow =
+    Option(new WorkflowDao(context.configuration).fetchOneByWid(wid))
+      .getOrElse(throw new NotFoundException(s"Workflow $wid not found"))
+
+  /**
+    * Turns publishing on, and touches nothing else. A workflow coming back from private is
+    * following the author's latest, because unpublishing always drops the pin: coming back should
+    * not silently put old public content back on show. Called on a workflow that is already public
+    * it changes nothing, pin included.
+    */
+  def publish(wid: Integer): PublishStatus = {
+    val updated = context
+      .update(WORKFLOW)
+      .set(WORKFLOW.IS_PUBLIC, java.lang.Boolean.TRUE)
+      .where(WORKFLOW.WID.eq(wid))
+      .execute()
+    if (updated == 0) {
+      throw new NotFoundException(s"Workflow $wid not found")
+    }
+    logger.info(s"Workflow $wid published, following latest")
+    statusOf(wid)

Review Comment:
   The log message claims the workflow is now 'following latest', but `publish` intentionally does not drop an existing pin (and can be called on an already-public, pinned workflow). Consider logging the actual resulting state (e.g., include whether it is pinned via `statusOf`) or use a neutral message like 'published' to avoid misleading operational diagnostics.



##########
amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowPublishSpec.scala:
##########
@@ -0,0 +1,564 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.web.resource.dashboard.user.workflow
+
+import org.apache.texera.auth.SessionUser
+import org.apache.texera.dao.MockTexeraDB
+import org.apache.texera.dao.jooq.generated.Tables.{WORKFLOW, WORKFLOW_USER_ACCESS}
+import org.apache.texera.dao.jooq.generated.enums.{DefaultViewEnum, PrivilegeEnum, UserRoleEnum}
+import org.apache.texera.dao.jooq.generated.tables.daos.{
+  UserDao,
+  WorkflowDao,
+  WorkflowUserAccessDao
+}
+import org.apache.texera.dao.jooq.generated.tables.pojos.{User, Workflow, WorkflowUserAccess}
+import org.scalatest.BeforeAndAfterAll
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+import org.jooq.{ExecuteContext, ExecuteListener}
+import org.jooq.impl.{DefaultConfiguration, DefaultExecuteListenerProvider}
+
+import java.time.OffsetDateTime
+import javax.ws.rs.{BadRequestException, ForbiddenException, NotFoundException}
+
+/**
+  * Covers the publish state a workflow can be in: following the author's latest content, as
+  * publishing has always done, or holding a pinned copy of the version the author froze.
+  *
+  * Only the state itself is covered here: nothing serves the pinned copy to a reader yet, so the
+  * assertions are about which copy each operation leaves stored.
+  */
+class WorkflowPublishSpec
+    extends AnyFlatSpec
+    with BeforeAndAfterAll
+    with Matchers
+    with MockTexeraDB {
+
+  private val exampleCreationTime = OffsetDateTime.parse("2025-01-01T00:00:00Z")
+
+  private def makeUser(uid: Int, name: String): User = {
+    val user = new User
+    user.setUid(Integer.valueOf(uid))
+    user.setName(name)
+    user.setEmail(s"$name@example.com")
+    user.setRole(UserRoleEnum.ADMIN)
+    user.setComment("test")
+    user.setAccountCreationTime(exampleCreationTime)
+    user
+  }
+
+  /** The author. */
+  private val owner = makeUser(1, "publish_owner")
+
+  /** A stranger: no access of their own, so nothing about this workflow is theirs to change. */
+  private val stranger = makeUser(2, "publish_stranger")
+
+  private val ownerSession = new SessionUser(owner)
+  private val strangerSession = new SessionUser(stranger)
+
+  private val workflowResource = new WorkflowResource()
+
+  private val publishedContent = """{"operators":[],"note":"content_as_published"}"""
+  private val editedContent = """{"operators":[],"note":"content_only_a_draft"}"""
+
+  private def workflowDao = new WorkflowDao(getDSLContext.configuration())
+
+  override protected def beforeAll(): Unit = {
+    initializeDBAndReplaceDSLContext()
+    val userDao = new UserDao(getDSLContext.configuration())
+    userDao.insert(owner)
+    userDao.insert(stranger)
+  }
+
+  override protected def afterAll(): Unit = shutdownDB()
+
+  /** Creates a workflow owned by `owner` holding [[publishedContent]]. */
+  private def createWorkflow(name: String): Integer = {
+    val workflow = new Workflow()
+    workflow.setName(name)
+    workflow.setDescription("a workflow")
+    workflow.setContent(publishedContent)
+    workflowResource.createWorkflow(workflow, ownerSession).workflow.getWid
+  }
+
+  /**
+    * Publishes and pins in one step, which is the state most of these tests are about. Publishing on
+    * its own leaves the workflow following the author's latest; pinning is what freezes a copy.
+    */
+  private def publishPinned(wid: Integer): WorkflowPublishService.PublishStatus = {
+    workflowResource.makePublic(wid, ownerSession)
+    workflowResource.pinLatest(wid, ownerSession)
+  }
+
+  /** Saves `content` as the author's working copy, the way an autosave would. */
+  private def edit(wid: Integer, content: String): Unit = {
+    val workflow = workflowDao.fetchOneByWid(wid)
+    workflow.setContent(content)
+    workflowResource.persistWorkflow(workflow, ownerSession)
+  }
+
+  /** Renames and re-describes the author's working copy, the way the dashboard does. */
+  private def relabel(wid: Integer, name: String, description: String): Unit = {
+    val workflow = workflowDao.fetchOneByWid(wid)
+    workflow.setName(name)
+    workflow.setDescription(description)
+    workflowResource.persistWorkflow(workflow, ownerSession)
+  }
+
+  /**
+    * Runs `interleaved` in the last moment before `act` sends its own write, which is where a second
+    * request slips in unnoticed. Driven off the statement itself rather than off a thread, so the
+    * ordering is the same on every run.
+    */
+  private def interleaving(interleaved: () => Unit)(act: => Unit): Unit = {
+    var pending = true
+    val configuration = getDSLContext.configuration().asInstanceOf[DefaultConfiguration]
+    val previousListeners = configuration.executeListenerProviders()
+    configuration.set(new DefaultExecuteListenerProvider(new ExecuteListener {
+      override def executeStart(ctx: ExecuteContext): Unit = {
+        // The workflow table itself, not workflow_version or the access tables: matching those too
+        // would let a later change to one of these paths interleave at the wrong moment and leave
+        // the test passing for the wrong reason.
+        val sql = Option(ctx.sql()).getOrElse("").toLowerCase
+        if (pending && sql.startsWith("update") && sql.contains("\"workflow\" set")) {
+          pending = false
+          interleaved()
+        }

Review Comment:
   This interleaving detector relies on the exact SQL string shape (quoting and formatting) produced by jOOQ, which can change across dialects/jOOQ versions and make these tests brittle. To make the trigger more robust, consider matching more flexibly (e.g., tolerate different quoting/backticks and whitespace via a regex) or use `ctx.query()`/query metadata where available rather than the rendered SQL string.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(workflow): freeze a pinned version as the public copy [texera]

Posted by "yangzhang75 (via GitHub)" <gi...@apache.org>.
yangzhang75 commented on PR #7853:
URL: https://github.com/apache/texera/pull/7853#issuecomment-5544404092

   /request-review: @mengw15 


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(workflow): freeze a pinned version as the public copy [texera]

Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #7853:
URL: https://github.com/apache/texera/pull/7853#issuecomment-5383308237

   <!-- texera-benchmarks-comment -->
   ## ⚠️ Benchmark changes need a look
   
   🟢 0 better · 🔴 10 worse · ⚪ 5 noise (<±5%) · 0 without baseline
   
   > CI benchmark results are noisy; treat <±5% as noise unless repeated.
   
   [Dashboard](https://apache.github.io/texera/dev/bench/) · [Run](https://github.com/apache/texera/actions/runs/32606962327)
   
   | | config | throughput | MB/s | latency | max Δ latest / 7d |
   |---|---|---:|---:|---:|---:|
   | 🔴 | bs=10 sw=10 sl=64 | 504 | 0.308 | 19,291/26,321/26,321 us | 🔴 +114.6% / 🔴 +83.0% |
   | 🔴 | bs=100 sw=10 sl=64 | 1,232 | 0.752 | 82,153/90,442/90,442 us | 🔴 +20.4% / 🟢 -16.7% |
   | 🔴 | bs=1000 sw=10 sl=64 | 1,433 | 0.874 | 694,496/774,324/774,324 us | 🔴 +6.4% / 🟢 +28.1% |
   
   <details><summary>Baseline details</summary>
   
   Latest main `b7c33b0` from 2026-08-22T12:41:18.215Z
   
   | config | metric | PR | latest main | 7d avg | Δ latest | Δ 7d |
   |---|---|---:|---:|---:|---:|---:|
   | bs=10 sw=10 sl=64 | throughput | 504 tuples/sec | 1,070 tuples/sec | 833.79 tuples/sec | -52.9% | -39.6% |
   | bs=10 sw=10 sl=64 | MB/s | 0.308 MB/s | 0.653 MB/s | 0.509 MB/s | -52.8% | -39.5% |
   | bs=10 sw=10 sl=64 | p50 | 19,291 us | 8,990 us | 11,864 us | +114.6% | +62.6% |
   | bs=10 sw=10 sl=64 | p95 | 26,321 us | 12,633 us | 14,381 us | +108.4% | +83.0% |
   | bs=10 sw=10 sl=64 | p99 | 26,321 us | 13,861 us | 18,035 us | +89.9% | +45.9% |
   | bs=100 sw=10 sl=64 | throughput | 1,232 tuples/sec | 1,420 tuples/sec | 1,083 tuples/sec | -13.2% | +13.7% |
   | bs=100 sw=10 sl=64 | MB/s | 0.752 MB/s | 0.867 MB/s | 0.661 MB/s | -13.2% | +13.7% |
   | bs=100 sw=10 sl=64 | p50 | 82,153 us | 69,686 us | 93,077 us | +17.9% | -11.7% |
   | bs=100 sw=10 sl=64 | p95 | 90,442 us | 75,137 us | 99,553 us | +20.4% | -9.2% |
   | bs=100 sw=10 sl=64 | p99 | 90,442 us | 89,601 us | 108,604 us | +0.9% | -16.7% |
   | bs=1000 sw=10 sl=64 | throughput | 1,433 tuples/sec | 1,455 tuples/sec | 1,119 tuples/sec | -1.5% | +28.1% |
   | bs=1000 sw=10 sl=64 | MB/s | 0.874 MB/s | 0.888 MB/s | 0.683 MB/s | -1.6% | +28.0% |
   | bs=1000 sw=10 sl=64 | p50 | 694,496 us | 685,976 us | 909,247 us | +1.2% | -23.6% |
   | bs=1000 sw=10 sl=64 | p95 | 774,324 us | 727,444 us | 952,561 us | +6.4% | -18.7% |
   | bs=1000 sw=10 sl=64 | p99 | 774,324 us | 767,738 us | 985,186 us | +0.9% | -21.4% |
   
   </details>
   
   <details><summary>Raw CSV</summary>
   
   ```csv
   config_idx,batch_size,schema_width,string_len,num_batches,total_ms,total_tuples,total_bytes,tuples_per_sec,mb_per_sec,lat_p50_us,lat_p95_us,lat_p99_us
   0,10,10,64,20,396.97,200,128000,504,0.308,19291.45,26320.69,26320.69
   1,100,10,64,20,1623.23,2000,1280000,1232,0.752,82152.55,90442.03,90442.03
   2,1000,10,64,20,13959.17,20000,12800000,1433,0.874,694495.56,774324.26,774324.26
   ```
   
   </details>
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(workflow): freeze a pinned version as the public copy [texera]

Posted by "yangzhang75 (via GitHub)" <gi...@apache.org>.
yangzhang75 commented on code in PR #7853:
URL: https://github.com/apache/texera/pull/7853#discussion_r4127090895


##########
amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowPublishService.scala:
##########
@@ -0,0 +1,204 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.web.resource.dashboard.user.workflow
+
+import com.typesafe.scalalogging.LazyLogging
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.apache.texera.dao.SqlServer
+import org.apache.texera.dao.jooq.generated.Tables.WORKFLOW
+import org.apache.texera.dao.jooq.generated.enums.DefaultViewEnum
+import org.apache.texera.dao.jooq.generated.tables.daos.WorkflowDao
+import org.apache.texera.dao.jooq.generated.tables.pojos.Workflow
+import org.jooq.DSLContext
+
+import javax.ws.rs.{BadRequestException, NotFoundException}
+import scala.util.Try
+
+/**
+  * Version pinning for public workflows.
+  *
+  * A public workflow follows the author's latest, as publishing has always done, until the author
+  * pins the version they have now: the public then keeps seeing that frozen copy while the author's
+  * later edits stay in the workflow's own columns until they pin again.
+  *
+  * `is_public` stays the on/off switch; `published_content` is the pin, NULL while following. A pin
+  * freezes everything on public show -- the graph, the title, the description and the view it opens
+  * in -- because a copy that froze only its graph would still advertise a title nobody published.
+  *
+  * Not to be confused with sharing: a user granted access always tracks the author's latest, pin or
+  * no pin. Only viewers who arrive because the workflow is public are held at the frozen copy.
+  */
+object WorkflowPublishService extends LazyLogging {
+
+  private def context: DSLContext = SqlServer.getInstance().createDSLContext()
+
+  /**
+    * What the share dialog asks about: whether the workflow is public, whether a version is pinned,
+    * and whether that pin is holding edits back -- the last is true when pinning again would publish
+    * something, and always false while following.
+    */
+  case class PublishStatus(
+      isPublished: Boolean,
+      isPinned: Boolean,
+      hasUnpublishedChanges: Boolean
+  )
+
+  /**
+    * Whether two workflow contents describe the same graph. Compared as parsed trees, because the
+    * two blobs travel by different routes and the same graph can come back with its whitespace or
+    * key order rearranged -- reporting that as an edit the public cannot see would be an alarm the
+    * author cannot clear.
+    */
+  private def sameContent(a: String, b: String): Boolean =
+    a == b || Try(objectMapper.readTree(a) == objectMapper.readTree(b)).getOrElse(false)
+
+  /** The workflow, or a 404. */
+  private def requireWorkflow(wid: Integer): Workflow =
+    Option(new WorkflowDao(context.configuration).fetchOneByWid(wid))
+      .getOrElse(throw new NotFoundException(s"Workflow $wid not found"))
+
+  /**
+    * Turns publishing on, and touches nothing else. A workflow coming back from private is
+    * following the author's latest, because unpublishing always drops the pin: coming back should
+    * not silently put old public content back on show. Called on a workflow that is already public
+    * it changes nothing, pin included.
+    */
+  def publish(wid: Integer): PublishStatus = {
+    val updated = context
+      .update(WORKFLOW)
+      .set(WORKFLOW.IS_PUBLIC, java.lang.Boolean.TRUE)
+      .where(WORKFLOW.WID.eq(wid))
+      .execute()
+    if (updated == 0) {
+      throw new NotFoundException(s"Workflow $wid not found")
+    }
+    // Deliberately not "following latest": this turns publishing on and touches nothing else, so a
+    // workflow that somehow arrives here already pinned stays pinned.
+    logger.info(s"Workflow $wid published")
+    statusOf(wid)
+  }
+
+  /**
+    * Freezes the author's current copy as the public one. The title, the description and the
+    * default view freeze with the graph: they are as public as it is, and the database refuses a
+    * pinned copy that carries only part of itself. The view matters because a form's definition
+    * rides inside the content -- serving the live preference over a frozen graph would open a form
+    * on a copy that has none.
+    *
+    * Each column is copied from its own row rather than from a workflow read a moment earlier, so
+    * there is no window in which the author's next save lands and the pin freezes the version
+    * before it -- which would leave them looking at "you have unpublished changes" the instant
+    * after they pinned.
+    *
+    * Publishing is a condition, not something this writes: asking to be published and pinning are
+    * two statements, so a `/private` landing between them would otherwise be undone here, putting a
+    * workflow the author had just taken down back on show under a frozen copy.
+    *
+    * @return how many rows it matched -- zero for a workflow that is missing or not published.
+    */
+  private def writePin(wid: Integer): Int =
+    context
+      .update(WORKFLOW)
+      .set(WORKFLOW.PUBLISHED_CONTENT, WORKFLOW.CONTENT)
+      .set(WORKFLOW.PUBLISHED_NAME, WORKFLOW.NAME)
+      .set(WORKFLOW.PUBLISHED_DESCRIPTION, WORKFLOW.DESCRIPTION)
+      .set(WORKFLOW.PUBLISHED_DEFAULT_VIEW, WORKFLOW.DEFAULT_VIEW)
+      .where(WORKFLOW.WID.eq(wid).and(WORKFLOW.IS_PUBLIC.isTrue))
+      .execute()
+
+  /**
+    * Clears the pinned copy in one statement, optionally unpublishing too: the constraint accepts a
+    * row only with every frozen column set on a public workflow, or with every one of them NULL, so
+    * clearing them one at a time -- or clearing them after `is_public` -- would be rejected.
+    *
+    * `published_version_id` is named by that constraint as well but is not touched here, for the
+    * same reason [[writePin]] does not set it: nothing writes it yet, so it is NULL on every row.
+    *
+    * @return how many rows it matched, so a missing workflow is distinguishable from a done one.
+    */
+  private def clearPin(wid: Integer, alsoUnpublish: Boolean = false): Int = {
+    val cleared = context
+      .update(WORKFLOW)
+      .set(WORKFLOW.PUBLISHED_CONTENT, null.asInstanceOf[String])
+      .set(WORKFLOW.PUBLISHED_NAME, null.asInstanceOf[String])
+      .set(WORKFLOW.PUBLISHED_DESCRIPTION, null.asInstanceOf[String])
+      .set(WORKFLOW.PUBLISHED_DEFAULT_VIEW, null.asInstanceOf[DefaultViewEnum])
+    val statement =
+      if (alsoUnpublish) cleared.set(WORKFLOW.IS_PUBLIC, java.lang.Boolean.FALSE) else cleared
+    statement.where(WORKFLOW.WID.eq(wid)).execute()

Review Comment:
   Also fixed, and it is the mirror of the previous one — thanks.
   
   `clearPin` now carries the condition in its own statement for the unpin path (`WHERE wid = ? AND is_public`), and `unpin` tells a missing workflow from a private one the same way `pinLatest` does. Unpublishing deliberately keeps no such condition: taking down what is already down is a no-op, not an error.
   
   Covered by an interleaving test that mirrors the pin one — a `/private` committed between the guard and the update now answers 400 and leaves the workflow down, instead of reporting "unpinned, following latest" about something no longer on show.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(workflow): freeze a pinned version as the public copy [texera]

Posted by "codecov-commenter (via GitHub)" <gi...@apache.org>.
codecov-commenter commented on PR #7853:
URL: https://github.com/apache/texera/pull/7853#issuecomment-5383294438

   ## [Codecov](https://app.codecov.io/gh/apache/texera/pull/7853?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) Report
   :white_check_mark: All modified and coverable lines are covered by tests.
   :white_check_mark: Project coverage is 91.86%. Comparing base ([`b7c33b0`](https://app.codecov.io/gh/apache/texera/commit/b7c33b0892db49a94a3b15c81eed4ebcbc12882d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)) to head ([`8f56d11`](https://app.codecov.io/gh/apache/texera/commit/8f56d11635f4ba6a43b8bb20a8c25f140e690fec?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache)).
   :warning: Report is 3 commits behind head on main.
   
   <details><summary>Additional details and impacted files</summary>
   
   
   
   ```diff
   @@             Coverage Diff              @@
   ##               main    #7853      +/-   ##
   ============================================
   - Coverage     91.87%   91.86%   -0.02%     
   + Complexity     4510     4507       -3     
   ============================================
     Files          1173     1173              
     Lines         47350    47339      -11     
     Branches       5306     5306              
   ============================================
   - Hits          43502    43486      -16     
   - Misses         2204     2206       +2     
   - Partials       1644     1647       +3     
   ```
   
   | [Flag](https://app.codecov.io/gh/apache/texera/pull/7853/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | Coverage Δ | | *Carryforward flag |
   |---|---|---|---|
   | [access-control-service](https://app.codecov.io/gh/apache/texera/pull/7853/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `81.00% <ø> (ø)` | | |
   | [agent-service](https://app.codecov.io/gh/apache/texera/pull/7853/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `98.62% <ø> (ø)` | | Carriedforward from [f8b0291](https://app.codecov.io/gh/apache/texera/commit/f8b02912489645ec493a1811ca0350a34ed69bfd?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [amber](https://app.codecov.io/gh/apache/texera/pull/7853/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `88.34% <100.00%> (-0.04%)` | :arrow_down: | Carriedforward from [f8b0291](https://app.codecov.io/gh/apache/texera/commit/f8b02912489645ec493a1811ca0350a34ed69bfd?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [computing-unit-managing-service](https://app.codecov.io/gh/apache/texera/pull/7853/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `73.67% <ø> (ø)` | | Carriedforward from [f8b0291](https://app.codecov.io/gh/apache/texera/commit/f8b02912489645ec493a1811ca0350a34ed69bfd?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [config-service](https://app.codecov.io/gh/apache/texera/pull/7853/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `86.73% <ø> (ø)` | | |
   | [file-service](https://app.codecov.io/gh/apache/texera/pull/7853/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `75.74% <ø> (ø)` | | Carriedforward from [f8b0291](https://app.codecov.io/gh/apache/texera/commit/f8b02912489645ec493a1811ca0350a34ed69bfd?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [frontend](https://app.codecov.io/gh/apache/texera/pull/7853/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `93.79% <ø> (ø)` | | Carriedforward from [f8b0291](https://app.codecov.io/gh/apache/texera/commit/f8b02912489645ec493a1811ca0350a34ed69bfd?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [notebook-migration-service](https://app.codecov.io/gh/apache/texera/pull/7853/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `79.13% <ø> (ø)` | | |
   | [pyamber](https://app.codecov.io/gh/apache/texera/pull/7853/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `97.57% <ø> (ø)` | | Carriedforward from [f8b0291](https://app.codecov.io/gh/apache/texera/commit/f8b02912489645ec493a1811ca0350a34ed69bfd?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   | [workflow-compiling-service](https://app.codecov.io/gh/apache/texera/pull/7853/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) | `77.19% <ø> (ø)` | | Carriedforward from [f8b0291](https://app.codecov.io/gh/apache/texera/commit/f8b02912489645ec493a1811ca0350a34ed69bfd?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) |
   
   *This pull request uses carry forward flags. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache) to find out more.
   </details>
   
   [:umbrella: View full report in Codecov by Harness](https://app.codecov.io/gh/apache/texera/pull/7853?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).   
   :loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=apache).
   <details><summary> :rocket: New features to boost your workflow: </summary>
   
   - :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
   </details>


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(workflow): freeze a pinned version as the public copy [texera]

Posted by "yangzhang75 (via GitHub)" <gi...@apache.org>.
yangzhang75 commented on code in PR #7853:
URL: https://github.com/apache/texera/pull/7853#discussion_r4029975895


##########
amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowPublishService.scala:
##########
@@ -0,0 +1,193 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.web.resource.dashboard.user.workflow
+
+import com.typesafe.scalalogging.LazyLogging
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.apache.texera.dao.SqlServer
+import org.apache.texera.dao.jooq.generated.Tables.WORKFLOW
+import org.apache.texera.dao.jooq.generated.enums.DefaultViewEnum
+import org.apache.texera.dao.jooq.generated.tables.daos.WorkflowDao
+import org.apache.texera.dao.jooq.generated.tables.pojos.Workflow
+import org.jooq.DSLContext
+
+import javax.ws.rs.NotFoundException
+import scala.util.Try
+
+/**
+  * Version pinning for public workflows.
+  *
+  * A public workflow follows the author's latest, as publishing has always done, until the author
+  * pins the version they have now: the public then keeps seeing that frozen copy while the author's
+  * later edits stay in the workflow's own columns until they pin again.
+  *
+  * `is_public` stays the on/off switch; `published_content` is the pin, NULL while following. A pin
+  * freezes everything on public show -- the graph, the title, the description and the view it opens
+  * in -- because a copy that froze only its graph would still advertise a title nobody published.
+  *
+  * Not to be confused with sharing: a user granted access always tracks the author's latest, pin or
+  * no pin. Only viewers who arrive because the workflow is public are held at the frozen copy.
+  */
+object WorkflowPublishService extends LazyLogging {
+
+  private def context: DSLContext = SqlServer.getInstance().createDSLContext()
+
+  /**
+    * What the share dialog asks about: whether the workflow is public, whether a version is pinned,
+    * and whether that pin is holding edits back -- the last is true when pinning again would publish
+    * something, and always false while following.
+    */
+  case class PublishStatus(
+      isPublished: Boolean,
+      isPinned: Boolean,
+      hasUnpublishedChanges: Boolean
+  )
+
+  /**
+    * Whether two workflow contents describe the same graph. Compared as parsed trees, because the
+    * two blobs travel by different routes and the same graph can come back with its whitespace or
+    * key order rearranged -- reporting that as an edit the public cannot see would be an alarm the
+    * author cannot clear.
+    */
+  private def sameContent(a: String, b: String): Boolean =
+    a == b || Try(objectMapper.readTree(a) == objectMapper.readTree(b)).getOrElse(false)
+
+  /** The workflow, or a 404. */
+  private def requireWorkflow(wid: Integer): Workflow =
+    Option(new WorkflowDao(context.configuration).fetchOneByWid(wid))
+      .getOrElse(throw new NotFoundException(s"Workflow $wid not found"))
+
+  /**
+    * Turns publishing on, and touches nothing else. A workflow coming back from private is
+    * following the author's latest, because unpublishing always drops the pin: coming back should
+    * not silently put old public content back on show. Called on a workflow that is already public
+    * it changes nothing, pin included.
+    */
+  def publish(wid: Integer): PublishStatus = {
+    val updated = context
+      .update(WORKFLOW)
+      .set(WORKFLOW.IS_PUBLIC, java.lang.Boolean.TRUE)
+      .where(WORKFLOW.WID.eq(wid))
+      .execute()
+    if (updated == 0) {
+      throw new NotFoundException(s"Workflow $wid not found")
+    }
+    logger.info(s"Workflow $wid published, following latest")
+    statusOf(wid)

Review Comment:
   Good catch, and it is the same overclaim I had already fixed in the scaladoc above without fixing the log line. Now logs just `Workflow $wid published`, with a note saying why it deliberately does not say "following latest".



##########
amber/src/test/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowPublishSpec.scala:
##########
@@ -0,0 +1,564 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.web.resource.dashboard.user.workflow
+
+import org.apache.texera.auth.SessionUser
+import org.apache.texera.dao.MockTexeraDB
+import org.apache.texera.dao.jooq.generated.Tables.{WORKFLOW, WORKFLOW_USER_ACCESS}
+import org.apache.texera.dao.jooq.generated.enums.{DefaultViewEnum, PrivilegeEnum, UserRoleEnum}
+import org.apache.texera.dao.jooq.generated.tables.daos.{
+  UserDao,
+  WorkflowDao,
+  WorkflowUserAccessDao
+}
+import org.apache.texera.dao.jooq.generated.tables.pojos.{User, Workflow, WorkflowUserAccess}
+import org.scalatest.BeforeAndAfterAll
+import org.scalatest.flatspec.AnyFlatSpec
+import org.scalatest.matchers.should.Matchers
+
+import org.jooq.{ExecuteContext, ExecuteListener}
+import org.jooq.impl.{DefaultConfiguration, DefaultExecuteListenerProvider}
+
+import java.time.OffsetDateTime
+import javax.ws.rs.{BadRequestException, ForbiddenException, NotFoundException}
+
+/**
+  * Covers the publish state a workflow can be in: following the author's latest content, as
+  * publishing has always done, or holding a pinned copy of the version the author froze.
+  *
+  * Only the state itself is covered here: nothing serves the pinned copy to a reader yet, so the
+  * assertions are about which copy each operation leaves stored.
+  */
+class WorkflowPublishSpec
+    extends AnyFlatSpec
+    with BeforeAndAfterAll
+    with Matchers
+    with MockTexeraDB {
+
+  private val exampleCreationTime = OffsetDateTime.parse("2025-01-01T00:00:00Z")
+
+  private def makeUser(uid: Int, name: String): User = {
+    val user = new User
+    user.setUid(Integer.valueOf(uid))
+    user.setName(name)
+    user.setEmail(s"$name@example.com")
+    user.setRole(UserRoleEnum.ADMIN)
+    user.setComment("test")
+    user.setAccountCreationTime(exampleCreationTime)
+    user
+  }
+
+  /** The author. */
+  private val owner = makeUser(1, "publish_owner")
+
+  /** A stranger: no access of their own, so nothing about this workflow is theirs to change. */
+  private val stranger = makeUser(2, "publish_stranger")
+
+  private val ownerSession = new SessionUser(owner)
+  private val strangerSession = new SessionUser(stranger)
+
+  private val workflowResource = new WorkflowResource()
+
+  private val publishedContent = """{"operators":[],"note":"content_as_published"}"""
+  private val editedContent = """{"operators":[],"note":"content_only_a_draft"}"""
+
+  private def workflowDao = new WorkflowDao(getDSLContext.configuration())
+
+  override protected def beforeAll(): Unit = {
+    initializeDBAndReplaceDSLContext()
+    val userDao = new UserDao(getDSLContext.configuration())
+    userDao.insert(owner)
+    userDao.insert(stranger)
+  }
+
+  override protected def afterAll(): Unit = shutdownDB()
+
+  /** Creates a workflow owned by `owner` holding [[publishedContent]]. */
+  private def createWorkflow(name: String): Integer = {
+    val workflow = new Workflow()
+    workflow.setName(name)
+    workflow.setDescription("a workflow")
+    workflow.setContent(publishedContent)
+    workflowResource.createWorkflow(workflow, ownerSession).workflow.getWid
+  }
+
+  /**
+    * Publishes and pins in one step, which is the state most of these tests are about. Publishing on
+    * its own leaves the workflow following the author's latest; pinning is what freezes a copy.
+    */
+  private def publishPinned(wid: Integer): WorkflowPublishService.PublishStatus = {
+    workflowResource.makePublic(wid, ownerSession)
+    workflowResource.pinLatest(wid, ownerSession)
+  }
+
+  /** Saves `content` as the author's working copy, the way an autosave would. */
+  private def edit(wid: Integer, content: String): Unit = {
+    val workflow = workflowDao.fetchOneByWid(wid)
+    workflow.setContent(content)
+    workflowResource.persistWorkflow(workflow, ownerSession)
+  }
+
+  /** Renames and re-describes the author's working copy, the way the dashboard does. */
+  private def relabel(wid: Integer, name: String, description: String): Unit = {
+    val workflow = workflowDao.fetchOneByWid(wid)
+    workflow.setName(name)
+    workflow.setDescription(description)
+    workflowResource.persistWorkflow(workflow, ownerSession)
+  }
+
+  /**
+    * Runs `interleaved` in the last moment before `act` sends its own write, which is where a second
+    * request slips in unnoticed. Driven off the statement itself rather than off a thread, so the
+    * ordering is the same on every run.
+    */
+  private def interleaving(interleaved: () => Unit)(act: => Unit): Unit = {
+    var pending = true
+    val configuration = getDSLContext.configuration().asInstanceOf[DefaultConfiguration]
+    val previousListeners = configuration.executeListenerProviders()
+    configuration.set(new DefaultExecuteListenerProvider(new ExecuteListener {
+      override def executeStart(ctx: ExecuteContext): Unit = {
+        // The workflow table itself, not workflow_version or the access tables: matching those too
+        // would let a later change to one of these paths interleave at the wrong moment and leave
+        // the test passing for the wrong reason.
+        val sql = Option(ctx.sql()).getOrElse("").toLowerCase
+        if (pending && sql.startsWith("update") && sql.contains("\"workflow\" set")) {
+          pending = false
+          interleaved()
+        }

Review Comment:
   Fair. Loosened to a regex that tolerates jOOQ's quoting and spacing (`"workflow"`, backticks, brackets, optional schema prefix). Worth noting the failure mode was never a silent pass: the helper asserts that something *was* interleaved, so a rendering it fails to recognise turns the test red rather than skipping it.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(workflow): freeze a pinned version as the public copy [texera]

Posted by "Copilot (via GitHub)" <gi...@apache.org>.
Copilot commented on code in PR #7853:
URL: https://github.com/apache/texera/pull/7853#discussion_r4118032126


##########
amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowPublishService.scala:
##########
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.web.resource.dashboard.user.workflow
+
+import com.typesafe.scalalogging.LazyLogging
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.apache.texera.dao.SqlServer
+import org.apache.texera.dao.jooq.generated.Tables.WORKFLOW
+import org.apache.texera.dao.jooq.generated.enums.DefaultViewEnum
+import org.apache.texera.dao.jooq.generated.tables.daos.WorkflowDao
+import org.apache.texera.dao.jooq.generated.tables.pojos.Workflow
+import org.jooq.DSLContext
+
+import javax.ws.rs.NotFoundException
+import scala.util.Try
+
+/**
+  * Version pinning for public workflows.
+  *
+  * A public workflow follows the author's latest, as publishing has always done, until the author
+  * pins the version they have now: the public then keeps seeing that frozen copy while the author's
+  * later edits stay in the workflow's own columns until they pin again.
+  *
+  * `is_public` stays the on/off switch; `published_content` is the pin, NULL while following. A pin
+  * freezes everything on public show -- the graph, the title, the description and the view it opens
+  * in -- because a copy that froze only its graph would still advertise a title nobody published.
+  *
+  * Not to be confused with sharing: a user granted access always tracks the author's latest, pin or
+  * no pin. Only viewers who arrive because the workflow is public are held at the frozen copy.
+  */
+object WorkflowPublishService extends LazyLogging {
+
+  private def context: DSLContext = SqlServer.getInstance().createDSLContext()
+
+  /**
+    * What the share dialog asks about: whether the workflow is public, whether a version is pinned,
+    * and whether that pin is holding edits back -- the last is true when pinning again would publish
+    * something, and always false while following.
+    */
+  case class PublishStatus(
+      isPublished: Boolean,
+      isPinned: Boolean,
+      hasUnpublishedChanges: Boolean
+  )
+
+  /**
+    * Whether two workflow contents describe the same graph. Compared as parsed trees, because the
+    * two blobs travel by different routes and the same graph can come back with its whitespace or
+    * key order rearranged -- reporting that as an edit the public cannot see would be an alarm the
+    * author cannot clear.
+    */
+  private def sameContent(a: String, b: String): Boolean =
+    a == b || Try(objectMapper.readTree(a) == objectMapper.readTree(b)).getOrElse(false)
+
+  /** The workflow, or a 404. */
+  private def requireWorkflow(wid: Integer): Workflow =
+    Option(new WorkflowDao(context.configuration).fetchOneByWid(wid))
+      .getOrElse(throw new NotFoundException(s"Workflow $wid not found"))
+
+  /**
+    * Turns publishing on, and touches nothing else. A workflow coming back from private is
+    * following the author's latest, because unpublishing always drops the pin: coming back should
+    * not silently put old public content back on show. Called on a workflow that is already public
+    * it changes nothing, pin included.
+    */
+  def publish(wid: Integer): PublishStatus = {
+    val updated = context
+      .update(WORKFLOW)
+      .set(WORKFLOW.IS_PUBLIC, java.lang.Boolean.TRUE)
+      .where(WORKFLOW.WID.eq(wid))
+      .execute()
+    if (updated == 0) {
+      throw new NotFoundException(s"Workflow $wid not found")
+    }
+    // Deliberately not "following latest": this turns publishing on and touches nothing else, so a
+    // workflow that somehow arrives here already pinned stays pinned.
+    logger.info(s"Workflow $wid published")
+    statusOf(wid)
+  }
+
+  /**
+    * Freezes the author's current copy as the public one, and turns publishing on. The title, the
+    * description and the default view freeze with the graph: they are as public as it is, and the
+    * database refuses a pinned copy that carries only part of itself. The view matters because a
+    * form's definition rides inside the content -- serving the live preference over a frozen graph
+    * would open a form on a copy that has none.
+    *
+    * Each column is copied from its own row rather than from a workflow read a moment earlier, so
+    * there is no window in which the author's next save lands and the pin freezes the version
+    * before it -- which would leave them looking at "you have unpublished changes" the instant
+    * after they pinned.
+    *
+    * @return how many rows it matched, so a missing workflow is distinguishable from a done one.
+    */
+  private def writePin(wid: Integer): Int =
+    context
+      .update(WORKFLOW)
+      .set(WORKFLOW.IS_PUBLIC, java.lang.Boolean.TRUE)

Review Comment:
   The publication check and this update are separate statements. If `/private` clears the pin after `requirePublishable` succeeds but before this statement executes, setting `IS_PUBLIC` back to true republishes the workflow, contrary to the pin endpoint's “already published” guard. Make the pin update conditional on `IS_PUBLIC = true` (without writing that flag), and distinguish an unpublished row from a missing one when zero rows match.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(workflow): freeze a pinned version as the public copy [texera]

Posted by "yangzhang75 (via GitHub)" <gi...@apache.org>.
yangzhang75 commented on PR #7853:
URL: https://github.com/apache/texera/pull/7853#issuecomment-6028986473

   /request-review: @aglinxinyuan 


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(workflow): freeze a pinned version as the public copy [texera]

Posted by "yangzhang75 (via GitHub)" <gi...@apache.org>.
yangzhang75 commented on code in PR #7853:
URL: https://github.com/apache/texera/pull/7853#discussion_r4119212614


##########
amber/src/main/scala/org/apache/texera/web/resource/dashboard/user/workflow/WorkflowPublishService.scala:
##########
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.texera.web.resource.dashboard.user.workflow
+
+import com.typesafe.scalalogging.LazyLogging
+import org.apache.texera.amber.util.JSONUtils.objectMapper
+import org.apache.texera.dao.SqlServer
+import org.apache.texera.dao.jooq.generated.Tables.WORKFLOW
+import org.apache.texera.dao.jooq.generated.enums.DefaultViewEnum
+import org.apache.texera.dao.jooq.generated.tables.daos.WorkflowDao
+import org.apache.texera.dao.jooq.generated.tables.pojos.Workflow
+import org.jooq.DSLContext
+
+import javax.ws.rs.NotFoundException
+import scala.util.Try
+
+/**
+  * Version pinning for public workflows.
+  *
+  * A public workflow follows the author's latest, as publishing has always done, until the author
+  * pins the version they have now: the public then keeps seeing that frozen copy while the author's
+  * later edits stay in the workflow's own columns until they pin again.
+  *
+  * `is_public` stays the on/off switch; `published_content` is the pin, NULL while following. A pin
+  * freezes everything on public show -- the graph, the title, the description and the view it opens
+  * in -- because a copy that froze only its graph would still advertise a title nobody published.
+  *
+  * Not to be confused with sharing: a user granted access always tracks the author's latest, pin or
+  * no pin. Only viewers who arrive because the workflow is public are held at the frozen copy.
+  */
+object WorkflowPublishService extends LazyLogging {
+
+  private def context: DSLContext = SqlServer.getInstance().createDSLContext()
+
+  /**
+    * What the share dialog asks about: whether the workflow is public, whether a version is pinned,
+    * and whether that pin is holding edits back -- the last is true when pinning again would publish
+    * something, and always false while following.
+    */
+  case class PublishStatus(
+      isPublished: Boolean,
+      isPinned: Boolean,
+      hasUnpublishedChanges: Boolean
+  )
+
+  /**
+    * Whether two workflow contents describe the same graph. Compared as parsed trees, because the
+    * two blobs travel by different routes and the same graph can come back with its whitespace or
+    * key order rearranged -- reporting that as an edit the public cannot see would be an alarm the
+    * author cannot clear.
+    */
+  private def sameContent(a: String, b: String): Boolean =
+    a == b || Try(objectMapper.readTree(a) == objectMapper.readTree(b)).getOrElse(false)
+
+  /** The workflow, or a 404. */
+  private def requireWorkflow(wid: Integer): Workflow =
+    Option(new WorkflowDao(context.configuration).fetchOneByWid(wid))
+      .getOrElse(throw new NotFoundException(s"Workflow $wid not found"))
+
+  /**
+    * Turns publishing on, and touches nothing else. A workflow coming back from private is
+    * following the author's latest, because unpublishing always drops the pin: coming back should
+    * not silently put old public content back on show. Called on a workflow that is already public
+    * it changes nothing, pin included.
+    */
+  def publish(wid: Integer): PublishStatus = {
+    val updated = context
+      .update(WORKFLOW)
+      .set(WORKFLOW.IS_PUBLIC, java.lang.Boolean.TRUE)
+      .where(WORKFLOW.WID.eq(wid))
+      .execute()
+    if (updated == 0) {
+      throw new NotFoundException(s"Workflow $wid not found")
+    }
+    // Deliberately not "following latest": this turns publishing on and touches nothing else, so a
+    // workflow that somehow arrives here already pinned stays pinned.
+    logger.info(s"Workflow $wid published")
+    statusOf(wid)
+  }
+
+  /**
+    * Freezes the author's current copy as the public one, and turns publishing on. The title, the
+    * description and the default view freeze with the graph: they are as public as it is, and the
+    * database refuses a pinned copy that carries only part of itself. The view matters because a
+    * form's definition rides inside the content -- serving the live preference over a frozen graph
+    * would open a form on a copy that has none.
+    *
+    * Each column is copied from its own row rather than from a workflow read a moment earlier, so
+    * there is no window in which the author's next save lands and the pin freezes the version
+    * before it -- which would leave them looking at "you have unpublished changes" the instant
+    * after they pinned.
+    *
+    * @return how many rows it matched, so a missing workflow is distinguishable from a done one.
+    */
+  private def writePin(wid: Integer): Int =
+    context
+      .update(WORKFLOW)
+      .set(WORKFLOW.IS_PUBLIC, java.lang.Boolean.TRUE)

Review Comment:
   Confirmed, and reproduced before fixing: with an unpublish interleaved between the guard and the update, the workflow came back public *and* pinned — the copy the author had just taken down is what the public would read.
   
   Fixed as you suggest. `writePin` no longer writes `is_public`; it requires it in the WHERE, so being published and being pinned are decided by one statement. Zero rows now means missing or not published, and `pinLatest` tells those apart (404 vs 400).
   
   The later PR in this series makes pinning transactional and takes the row's lock first; there the same check moved under the lock, deliberately *before* the anchor version row is inserted, so a pin that cannot land leaves nothing behind.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(workflow): freeze a pinned version as the public copy [texera]

Posted by "yangzhang75 (via GitHub)" <gi...@apache.org>.
yangzhang75 commented on PR #7853:
URL: https://github.com/apache/texera/pull/7853#issuecomment-6087148180

   @aglinxinyuan could you merge this when you find some time?


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] feat(workflow): freeze a pinned version as the public copy [texera]

Posted by "aglinxinyuan (via GitHub)" <gi...@apache.org>.
aglinxinyuan merged PR #7853:
URL: https://github.com/apache/texera/pull/7853


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@texera.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org