Skip to content

Conversation

@DaxServer
Copy link
Owner

  • Checks if the column type is string-like (VARCHAR, TEXT, BLOB)
  • Converts the column to VARCHAR if it's not a string-like type
  • This ensures the replace operation works correctly for non-string columns

feat(project-store): Add pagination state and refresh current page

  • Adds current offset and limit to the project store
  • Adds a refreshCurrentPage action to fetch the current page again
  • This allows the UI to easily refresh the current page without losing pagination state

fix(replace-dialog): Handle replace operation errors properly

  • Emits the replace-completed event with the affected rows count
  • Displays a generic error message if the replace operation fails
  • Removes unnecessary check for affectedRows being undefined or null

- Checks if the column type is string-like (VARCHAR, TEXT, BLOB)
- Converts the column to VARCHAR if it's not a string-like type
- This ensures the replace operation works correctly for non-string columns

feat(project-store): Add pagination state and refresh current page

- Adds current offset and limit to the project store
- Adds a `refreshCurrentPage` action to fetch the current page again
- This allows the UI to easily refresh the current page without losing pagination state

fix(replace-dialog): Handle replace operation errors properly

- Emits the `replace-completed` event with the affected rows count
- Displays a generic error message if the replace operation fails
- Removes unnecessary check for `affectedRows` being undefined or null
@coderabbitai
Copy link

coderabbitai bot commented Sep 28, 2025

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Replace now supports non-text columns by auto-converting types when needed.
    • After a replace completes, the current data page refreshes automatically.
    • Added warning toasts; no-op replaces show “No rows were affected.”
  • Style
    • Improved Replace dialog label layout for clearer readability.
  • Tests
    • Added comprehensive tests covering replace behavior across multiple data types (e.g., number, boolean, date, JSON) and matching options.

Walkthrough

Backend now ensures target columns are string-like (converting and rolling back if needed) before running replace; new helpers and tests validate behavior across datatypes. Frontend emits a payload-less replaceCompleted event, shows a warning when zero rows affected, and refreshes the current page using preserved pagination state.

Changes

Cohort / File(s) Summary
Backend: Replace operation & helpers
backend/src/services/replace-operation.service.ts
performReplace captures original column type, ensures column is string-like (may ALTER to VARCHAR), runs update, and reverts schema if no rows changed. Adds helpers: getColumnType, isStringLikeType, ensureColumnIsStringType, changeColumnType.
Backend Tests: alter-table behavior
backend/tests/services/replace-operation.alter-table.service.test.ts
New tests validating replace on non-string columns (INTEGER, DOUBLE, BOOLEAN, DATE, VARCHAR, JSON), asserting affected rows, final values, and post-op column type (VARCHAR).
Frontend: Replace event emission & UX
frontend/src/features/data-processing/components/ColumnHeaderMenu.vue, frontend/src/features/data-processing/components/ReplaceDialog.vue
Declares emit('replaceCompleted'); ColumnHeaderMenu shows a warning when affectedRows === 0 and emits replaceCompleted without payload; ReplaceDialog always emits replaceCompleted with affectedRows on success and uses showError on failure; label markup adjusted.
Frontend: Page refresh wiring
frontend/src/features/data-processing/components/DataTabPanel.vue
Listens for @replace-completed and calls projectStore.refreshCurrentPage to reload the current page slice.
Frontend Store: pagination + refresh API
frontend/src/features/project-management/stores/project.store.ts
Adds currentOffset/currentLimit state, stores them in fetchProject(offset, limit), and exposes refreshCurrentPage(projectId) which refetches using stored pagination.
Frontend: error handling helper
frontend/src/shared/composables/useErrorHandling.ts
Adds showWarning(text) toast helper and exposes it in the composable API.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Service as ReplaceOperationService
  participant DB as Database

  Service->>DB: PRAGMA table_info(table)
  Service->>Service: getColumnType / isStringLikeType
  alt Column not string-like
    Service->>DB: ALTER TABLE ... ALTER COLUMN column TYPE VARCHAR
  end
  Service->>DB: UPDATE table SET column = REPLACE(column, find, replace)
  DB-->>Service: affectedRows
  alt affectedRows == 0 and conversion happened
    Service->>DB: ALTER TABLE ... ALTER COLUMN column TYPE <originalType>  %% rollback
  end
  Service-->>Caller: affectedRows
Loading
sequenceDiagram
  autonumber
  participant User as User
  participant Dialog as ReplaceDialog.vue
  participant Menu as ColumnHeaderMenu.vue
  participant Panel as DataTabPanel.vue
  participant Store as project.store
  participant API as Backend API

  User->>Dialog: Submit replace
  Dialog->>API: call replace endpoint
  API-->>Dialog: { affectedRows }
  Dialog->>Menu: emit('replaceCompleted')    %% no payload
  Menu->>Panel: @replace-completed
  Panel->>Store: refreshCurrentPage(projectId)
  Store->>API: fetchProject(projectId, currentOffset, currentLimit)
  API-->>Store: page data
  Store-->>Panel: updated page
  Panel-->>User: refreshed table view
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly follows conventional commit style and clearly summarizes the primary feature introduced—ensuring columns are string-like before performing replace operations—without unnecessary detail or noise.
Description Check ✅ Passed The description outlines each set of changes in the pull request, covering the replace-operation conversion logic, the project-store pagination enhancements, and the replace-dialog error handling fix, and is directly related to the changeset.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c10c1bd and 4d713ed.

📒 Files selected for processing (1)
  • backend/src/services/replace-operation.service.ts (2 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c5db975 and bd0cf96.

📒 Files selected for processing (6)
  • backend/src/services/replace-operation.service.ts (2 hunks)
  • backend/tests/services/replace-operation.alter-table.service.test.ts (1 hunks)
  • frontend/src/features/data-processing/components/ColumnHeaderMenu.vue (1 hunks)
  • frontend/src/features/data-processing/components/DataTabPanel.vue (2 hunks)
  • frontend/src/features/data-processing/components/ReplaceDialog.vue (5 hunks)
  • frontend/src/features/project-management/stores/project.store.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
backend/tests/services/replace-operation.alter-table.service.test.ts (2)
backend/src/plugins/database.ts (3)
  • initializeDb (8-48)
  • closeDb (57-63)
  • getDb (50-55)
backend/src/services/replace-operation.service.ts (1)
  • ReplaceOperationService (12-202)

Comment on lines 21 to 22
// Check if column is string-like, if not, convert it first
await this.ensureColumnIsStringType(table, column)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Avoid mutating schema on no-op replace
Calling ensureColumnIsStringType before we know whether anything matches permanently converts, say, an INTEGER column to VARCHAR even when affectedRows turns out to be 0. A “find” typo now silently changes the schema without touching data, which is a correctness regression. Please defer the ALTER until after you’ve confirmed there are matches (e.g., fetch the current type, count using CAST(column AS VARCHAR) when needed, bail out early on zero hits, then run the ALTER/UPDATE). That way we preserve the original type on no-ops and still satisfy the replace flow when a change is actually required.

-    // Check if column is string-like, if not, convert it first
-    await this.ensureColumnIsStringType(table, column)
-
-    // Count rows that will be affected before the update
-    const affectedRows = await this.countAffectedRows(table, column, find, caseSensitive, wholeWord)
-
-    // Only proceed if there are rows to update
-    if (affectedRows === 0) {
-      return 0
-    }
+    const columnType = await this.getColumnType(table, column)
+    const isStringColumn = this.isStringLikeType(columnType)
+
+    const affectedRows = await this.countAffectedRows(
+      table,
+      column,
+      find,
+      caseSensitive,
+      wholeWord,
+      isStringColumn,
+    )
+    if (affectedRows === 0) {
+      return 0
+    }
+    if (!isStringColumn) {
+      await this.ensureColumnIsStringType(table, column)
+    }

(Adjust countAffectedRows to accept the extra flag and CAST the column when it’s not already string-like.)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In backend/src/services/replace-operation.service.ts around lines 21-22, calling
ensureColumnIsStringType before checking for matches mutates the schema on no-op
replaces; change flow to first detect whether column is string-like, then call
countAffectedRows with a new flag (e.g., castToStringWhenNeeded) so the count
query uses CAST(column AS VARCHAR) only for counting when column is not
string-like; if count is zero bail out without altering schema, otherwise run
ensureColumnIsStringType then perform the UPDATE. Also update countAffectedRows
signature to accept the flag and perform the CAST in its SQL when flagged.

Comment on lines +66 to 67
@replace-completed="() => refreshCurrentPage(projectId)"
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Pass the project ID string, not the Ref, to refreshCurrentPage.
The current handler invokes refreshCurrentPage(projectId), but projectId here is a Ref<string>. When the event fires, the store receives [object Object] as the ID, so the subsequent API call uses an invalid project identifier. Forward the emitted payload (already a string) or use projectId.value.

Apply this diff:

-            @replace-completed="() => refreshCurrentPage(projectId)"
+            @replace-completed="(pid) => refreshCurrentPage(pid)"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@replace-completed="() => refreshCurrentPage(projectId)"
/>
@replace-completed="(pid) => refreshCurrentPage(pid)"
/>
🤖 Prompt for AI Agents
In frontend/src/features/data-processing/components/DataTabPanel.vue around
lines 66 to 67, the event handler is passing the Ref object `projectId` to
refreshCurrentPage which results in "[object Object]" being used as the project
ID; change the handler to pass a plain string by either forwarding the emitted
payload (e.g. use the event argument passed to the handler) or dereferencing the
Ref (use projectId.value) so refreshCurrentPage receives a string ID.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/src/services/replace-operation.service.ts (2)

116-123: Teach countAffectedRows to CAST when needed.

Add a flag to count via CAST(column AS VARCHAR) instead of altering the schema to count matches. Used by the refactored flow.

-  private async countAffectedRows(
+  private async countAffectedRows(
     table: string,
     column: string,
     find: string,
     caseSensitive: boolean,
     wholeWord: boolean,
-  ): Promise<number> {
+    castToVarchar: boolean = false,
+  ): Promise<number> {
@@
-    if (wholeWord) {
+    const colExpr = castToVarchar ? `CAST("${column}" AS VARCHAR)` : `"${column}"`
+    if (wholeWord) {
@@
-        WHERE "${column}" IS NOT NULL
-          AND regexp_matches("${column}", $1, $2)
+        WHERE ${colExpr} IS NOT NULL
+          AND regexp_matches(${colExpr}, $1, $2)
@@
-          WHERE "${column}" IS NOT NULL
-            AND position($1 in "${column}") > 0
+          WHERE ${colExpr} IS NOT NULL
+            AND position($1 in ${colExpr}) > 0
@@
-          WHERE "${column}" IS NOT NULL
-            AND regexp_matches("${column}", $1, 'i')
+          WHERE ${colExpr} IS NOT NULL
+            AND regexp_matches(${colExpr}, $1, 'i')

Also applies to: 126-153, 155-159


67-82: Inline regex flags as literals
DuckDB enforces that regex option flags be constant literals, so remove replaceFlags from the bind parameters and inline both replaceFlags and matchFlags in the SQL. For example:

const replaceFlags = caseSensitive ? 'g' : 'gi'
const matchFlags   = caseSensitive ? ''  : 'i'
const pattern      = `\\b${this.escapeRegex(find)}\\b`
const query = `
  UPDATE "${table}"
  SET "${column}" = regexp_replace("${column}", $1, $2, '${replaceFlags}')
  WHERE "${column}" IS NOT NULL
    AND regexp_matches("${column}", '${pattern}', '${matchFlags}')
`
return { query, params: [pattern, replace] }

Applies also to lines 126–136 and 145–152.

🧹 Nitpick comments (1)
backend/tests/services/replace-operation.alter-table.service.test.ts (1)

14-36: Strong coverage across types; add BLOB and unique test names.

  • Coverage is solid (INTEGER/DOUBLE/BOOLEAN/DATE/VARCHAR/JSON).
  • Two cases have the same name "JSON"; make them unique for clearer output.
  • Please add a BLOB case to catch the string-like misclassification and ensure conversion.

Minimal rename diff for the second JSON block:

-    {
-      name: 'JSON',
+    {
+      name: 'JSON (substring)',

Example BLOB test case snippet:

{
  name: 'BLOB',
  tableSql: `
    CREATE TABLE test (id INTEGER, payload BLOB);
  `,
  insertSql: [
    `INSERT INTO test (id, payload) VALUES (1, hex('48656c6c6f'))`, -- "Hello"
    `INSERT INTO test (id, payload) VALUES (2, hex('776f726c64'))`, -- "world"
  ],
  columnName: 'payload',
  initialType: 'BLOB',
  find: 'o',
  replace: '0',
  expectedAffectedRows: 2,
  expectedResults: ['Hell0', 'w0rld'],
}

Also applies to: 37-56, 58-77, 79-98, 100-118, 120-143, 145-168

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd0cf96 and c10c1bd.

📒 Files selected for processing (5)
  • backend/src/services/replace-operation.service.ts (2 hunks)
  • backend/tests/services/replace-operation.alter-table.service.test.ts (1 hunks)
  • frontend/src/features/data-processing/components/ColumnHeaderMenu.vue (1 hunks)
  • frontend/src/features/data-processing/components/ReplaceDialog.vue (5 hunks)
  • frontend/src/shared/composables/useErrorHandling.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
backend/tests/services/replace-operation.alter-table.service.test.ts (2)
backend/src/plugins/database.ts (3)
  • initializeDb (8-48)
  • closeDb (57-63)
  • getDb (50-55)
backend/src/services/replace-operation.service.ts (1)
  • ReplaceOperationService (12-221)
🔇 Additional comments (5)
frontend/src/features/data-processing/components/ReplaceDialog.vue (1)

46-47: Thanks for stringifying caught errors.

This keeps the toast readable by surfacing the actual error message instead of [object Object]. Nice cleanup.

backend/tests/services/replace-operation.alter-table.service.test.ts (3)

5-12: Good DB lifecycle management.

Per-test initialize/close guards state leakage across cases.


284-300: Type assertions look right; reverted vs. persisted conversions covered.

Asserting reverted type on 0 matches and VARCHAR on mutations matches the contract.

Consider adding an assertion that other columns’ types remain unchanged to catch unintended ripple effects.


301-305: Data validation is deterministic.

ORDER BY id ensures stable comparisons; good call.

backend/src/services/replace-operation.service.ts (1)

21-36: Defer ALTER; make the whole replace atomic.

Altering the column before counting mutates schema even for no-ops and, without a transaction, an exception between ALTER and UPDATE can leave the table in a wrong type. Count using CAST (no schema change), bail on 0, then ALTER+UPDATE within a transaction. This eliminates double-ALTER churn and closes the error-window.

Apply this diff:

@@
-    // Get the original column type before any modifications
-    const originalColumnType = await this.getColumnType(table, column)
-
-    // Check if column is string-like, if not, convert it first
-    const wasConverted = await this.ensureColumnIsStringType(table, column)
-
-    // Count rows that will be affected before the update
-    const affectedRows = await this.countAffectedRows(table, column, find, caseSensitive, wholeWord)
-
-    // Only proceed if there are rows to update
-    if (affectedRows === 0) {
-      // Revert column type if it was converted and no rows were affected
-      if (wasConverted) {
-        await this.changeColumnType(table, column, originalColumnType)
-      }
-      return 0
-    }
+    const originalColumnType = await this.getColumnType(table, column)
+    const isStringColumn = this.isStringLikeType(originalColumnType)
+    // Count without mutating schema; cast when needed
+    const affectedRows = await this.countAffectedRows(
+      table,
+      column,
+      find,
+      caseSensitive,
+      wholeWord,
+      /*castToVarchar*/ !isStringColumn,
+    )
+    if (affectedRows === 0) {
+      return 0
+    }
+    await this.db.run('BEGIN')
+    try {
+      if (!isStringColumn) {
+        await this.changeColumnType(table, column, 'VARCHAR')
+      }
@@
-    await this.db.run(query, queryParams)
-
-    return affectedRows
+      await this.db.run(query, queryParams)
+      await this.db.run('COMMIT')
+    } catch (err) {
+      await this.db.run('ROLLBACK')
+      throw err
+    }
+    return affectedRows

Also applies to: 39-51

Comment on lines +171 to +188
private async getColumnType(table: string, column: string): Promise<string> {
const result = await this.db.runAndReadAll(`PRAGMA table_info("${table}")`)
const columns = result.getRowObjectsJson() as Array<{
cid: number
name: string
type: string
pk: boolean
notnull: boolean
dflt_value: string | null
}>

const columnInfo = columns.find((col) => col.name === column)
if (!columnInfo) {
throw new Error(`Column '${column}' not found in table '${table}'`)
}

return columnInfo.type.toUpperCase()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Escape identifiers to prevent SQL injection via table/column names.

Directly interpolating table/column without escaping can break queries or allow injection if names contain quotes. Double-quote and escape inner quotes consistently.

Add helper and use everywhere identifiers are interpolated:

// helper
private q(id: string): string {
  return `"${id.replace(/"/g, '""')}"`
}

Then replace occurrences of "${table}"/"${column}" with ${this.q(table)} / ${this.q(column)} across queries.

Also applies to: 199-220

🤖 Prompt for AI Agents
In backend/src/services/replace-operation.service.ts around lines 171 to 188
(and also update usages at 199-220), identifiers are interpolated directly into
PRAGMA/SQL which can break queries or allow injection; add a private helper
q(id: string): string that returns the identifier wrapped in double-quotes with
inner double-quotes escaped by doubling, then replace occurrences like
`"${table}"` and `"${column}"` with `${this.q(table)}` / `${this.q(column)}` in
all SQL/PRAGMA string interpolations (including the areas flagged at 199-220) so
table and column names are safely escaped before use.

Comment on lines 193 to 197
private isStringLikeType(columnType: string): boolean {
const stringTypes = ['VARCHAR', 'TEXT', 'BLOB', 'CHAR', 'BPCHAR']

return stringTypes.some((type) => columnType.includes(type))
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

BLOB should not be treated as string-like.

Regex/replace on BLOB is invalid; letting it bypass conversion can error. Remove BLOB from string-like.

-    const stringTypes = ['VARCHAR', 'TEXT', 'BLOB', 'CHAR', 'BPCHAR']
+    const stringTypes = ['VARCHAR', 'TEXT', 'CHAR', 'BPCHAR']

Also add a BLOB test to ensure the column is converted before replace (see test suggestion below).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private isStringLikeType(columnType: string): boolean {
const stringTypes = ['VARCHAR', 'TEXT', 'BLOB', 'CHAR', 'BPCHAR']
return stringTypes.some((type) => columnType.includes(type))
}
private isStringLikeType(columnType: string): boolean {
const stringTypes = ['VARCHAR', 'TEXT', 'CHAR', 'BPCHAR']
return stringTypes.some((type) => columnType.includes(type))
}

@DaxServer DaxServer merged commit fb8eff6 into main Sep 28, 2025
4 of 5 checks passed
@DaxServer DaxServer deleted the replace-operation-ensure-column-is-string-like-before-replace branch September 28, 2025 18:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants