Skip to content

feat: Add filter option for pg_dump#168

Open
gautamsi wants to merge 10 commits intoeduardolat:developfrom
OEMUAP:feature/add-filter-option
Open

feat: Add filter option for pg_dump#168
gautamsi wants to merge 10 commits intoeduardolat:developfrom
OEMUAP:feature/add-filter-option

Conversation

@gautamsi
Copy link

@gautamsi gautamsi commented Jan 25, 2026

I needed a way to exclude table data for several large tables to make use of nightly export for developer use.

decided to add that feature, then I saw --filter option in latest postgres which makes use of a filter file containing multiple include exclude things

This PR adds support for --filter parameter in postgres 17+

Later I updated this to translate the value from this config to legacy --table, --exclude-table etc (only for table schema and table_data) for lower postgres version.

this is how it looks on backup config

Screenshot 2026-01-25 031038

there is another modal for help on this filter option
Screenshot 2026-01-25 031720

There is also a guided mode on this which allows you to select specific options and minimize human errors
image

I have tested that manually in local dev container with postgres 15 and 17

Summary by CodeRabbit

  • New Features

    • Added backup content filtering to include/exclude databases, schemas, tables with support for modern and legacy PostgreSQL.
    • Backup storage now preserves filter definitions.
    • Dual-mode filter editor (Text and Guided) in create/edit backup flows.
  • Documentation

    • New in-app help with filter syntax, examples, and version notes.
  • Chores

    • Added prebuild scripts to consolidate SQL files for build-time generation.

✏️ Tip: You can customize this high-level summary in your review settings.

eduardolat and others added 9 commits October 5, 2025 23:14
Merge develop into main for v0.5.1-rc.1
🥇 Thanks FetchGoat for upgrading to Gold Sponsor
Co-authored-by: gautamsi <5769869+gautamsi@users.noreply.github.com>
Co-authored-by: gautamsi <5769869+gautamsi@users.noreply.github.com>
Co-authored-by: gautamsi <5769869+gautamsi@users.noreply.github.com>
- For PG 17+: use --filter parameter with temp file
- For PG 13-16: convert filters to legacy arguments (--exclude-table, --exclude-table-data, --exclude-schema)
- Added version compatibility notice in UI help text
- Only exclude filters supported in legacy mode (include is default behavior)
- Filter types without legacy equivalents are not supported in PG < 17

Co-authored-by: gautamsi <5769869+gautamsi@users.noreply.github.com>
- Added support for include table, schema, and extension filters using --table, --schema, and --extension arguments
- Updated help text to reflect full legacy mode support
- Both include and exclude filters now work in PostgreSQL 13-16
- Only foreign_data, table_and_children, and table_data_and_children remain unsupported in legacy mode

Co-authored-by: gautamsi <5769869+gautamsi@users.noreply.github.com>
@coderabbitai
Copy link

coderabbitai bot commented Jan 25, 2026

📝 Walkthrough

Walkthrough

This PR adds a filter_content column to backups, persists and surfaces it in create/edit flows, implements version-aware pg_dump handling (Postgres 17+ uses --filter with a temp file; older versions convert filters to legacy flags), and adds dual-mode web UI editors for filter authoring.

Changes

Cohort / File(s) Summary
Database Migration
internal/database/migrations/20251216081402_add_filter_content_to_backups.sql
Adds filter_content TEXT to backups in Up; drops it in Down.
Database Schema Updates
internal/service/backups/create_backup.sql, internal/service/backups/update_backup.sql
Include filter_content in INSERT and UPDATE (uses COALESCE for updates).
Query Execution
internal/service/executions/run_execution.go, internal/service/executions/run_execution.sql
Retrieve filter_content from backups and pass it into DumpParams (new FilterContent).
PostgreSQL Integration
internal/integration/postgres/postgres.go
Adds FilterContent handling: for Postgres ≥17 write temp file and pass --filter; for older versions convert filter lines to legacy --table/--schema/--exclude args; manages temp file lifecycle and error cleanup.
Web UI Help
internal/view/web/dashboard/backups/common.go
Adds filterHelp() providing UI docs, format spec, examples, and version notes.
Backup Creation UI
internal/view/web/dashboard/backups/create_backup.go
Adds FilterContent form field and Alpine.js dual-mode (Text/Guided) filter editor; syncs to hidden filter_content input.
Backup Edit UI
internal/view/web/dashboard/backups/edit_backup.go
Adds FilterContent form field, JSON-encodes initial content for client init, and injects dual-mode filter editor with live sync.
Build Scripts
prebuild-simple.mjs, prebuild.mjs
New Node scripts to aggregate internal/service/**/*.sql files into internal/database/dbgen/queries.gen.sql, prefixing -- name: lines with -- file: tags and adding a non-editable header.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant WebUI as Web UI
    participant Server as Go Server
    participant Database as Database
    participant PGDump as pg_dump
    participant Temp as Temp File

    User->>WebUI: Create/Edit backup with filter
    WebUI->>Server: POST backup (includes filter_content)
    Server->>Database: INSERT/UPDATE backup (filter_content stored)
    Database-->>Server: ACK

    Note over Server,PGDump: Execution time
    Server->>Server: Check PostgreSQL version
    alt PostgreSQL ≥ 17
        Server->>Temp: Write filter_content to temp file
        Server->>PGDump: Run pg_dump --filter <temp-file>
        PGDump-->>Temp: Read filter rules
    else PostgreSQL < 17
        Server->>Server: Convert filter_content to legacy args
        Server->>PGDump: Run pg_dump with legacy --table/--schema/--exclude flags
    end
    PGDump-->>Server: Return backup data
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I nibble lines of filter lore,

text and guided, tucked in store,
Temp files whisper to pg_dump's ear,
Old flags hum when versions veer,
Backups sing—safe evermore.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Add filter option for pg_dump' directly and clearly summarizes the main change: adding a filter feature for pg_dump across the codebase.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

🤖 Fix all issues with AI agents
In `@internal/view/web/dashboard/backups/create_backup.go`:
- Around line 328-381: The hidden field can get out of sync because
convertToText, convertToGuided, and removeRow modify state but never update the
hidden input; after each state-mutating method (convertToText, convertToGuided,
removeRow—and also addRow if you want immediate sync when adding a blank row)
call this.syncToHidden() so the DOM-hidden value [name=filter_content] is
updated whenever filterRows or textFilter or filterMode change; update the end
of those functions to invoke syncToHidden() (keeping the existing logic intact).

In `@internal/view/web/dashboard/backups/edit_backup.go`:
- Around line 66-68: The FilterContent field is being constructed as
sql.NullString{String: formData.FilterContent, Valid: formData.FilterContent !=
""} which sets Valid=false for an empty string and lets the DB COALESCE keep the
old value, preventing users from clearing the filter; change construction in the
struct (where FilterContent is set) to always set Valid:true (e.g.,
sql.NullString{String: formData.FilterContent, Valid: true}) so an empty string
overwrites the existing value and users can clear their filter, leaving
OptNoComments handling unchanged.
- Around line 430-435: The hidden input named "filter_content" is not
initialized so existing filters can be lost on submit; fix by setting its
initial value from the current filter state (e.g., supply the existing filter
JSON/string to nodx.Input as the value attribute for the input named
"filter_content") or alternatively ensure syncToHidden() runs on component
initialization (add an x-init that calls syncToHidden() on the parent element
that defines the filter UI). Update the nodx.Input call for the "filter_content"
hidden field or add x-init to the filter container so the hidden value is
populated before any user interaction.
🧹 Nitpick comments (3)
prebuild-simple.mjs (1)

8-48: Make output deterministic by sorting SQL files.
Filesystem traversal order can vary, which makes queries.gen.sql unstable across runs. A simple sort keeps builds reproducible.

♻️ Proposed fix
-  const files = findSqlFiles(serviceDir);
+  const files = findSqlFiles(serviceDir);
+  files.sort();
prebuild.mjs (1)

27-39: Sort the SQL file list for deterministic output.
Without an explicit sort, file order can differ between environments and change the generated file content.

♻️ Proposed fix
   for (const sourceGlob of sourceGlobs) {
     const foundFiles = await glob(path.join(rootDir, sourceGlob));
     files.push(...foundFiles);
   }
+  files.sort();
internal/integration/postgres/postgres.go (1)

237-264: String-based version comparison is fragile.

Line 239 uses string comparison (version.Value.Version >= "17"), which works for current versions ("13"-"18") but could break with future double-digit versions or different version formats.

Consider using a numeric comparison or comparing against the enum directly:

♻️ Suggested approach
-		if version.Value.Version >= "17" {
+		// PG17+ supports --filter parameter
+		if version == PG17 || version == PG18 {

Or add a helper method to check version capability.

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: 1

🤖 Fix all issues with AI agents
In `@internal/view/web/dashboard/backups/edit_backup.go`:
- Around line 328-340: The syncToHidden function and the component x-init use
document.querySelector('[name=filter_content]') which always selects the first
matching element and can overwrite other modals; update syncToHidden (and any
x-init lookups) to use the component-scoped DOM via Alpine's $el (e.g., use
$el.querySelector('[name=filter_content]') or this.$el.querySelector when inside
the Alpine component) so each modal updates its own hidden input, and ensure you
null-check the result before setting .value to avoid runtime errors.
🧹 Nitpick comments (1)
internal/view/web/dashboard/backups/create_backup.go (1)

373-385: Scope the hidden-input lookup to the component or use Alpine's x-ref pattern.
document.querySelector grabs the first matching element globally. If multiple filter editors coexist, updates can target the wrong hidden field. You can scope the query using this.$el.querySelector('[name=filter_content]'), but Alpine's recommended approach is to use x-ref with $refs:

<input x-ref="filterContent" name="filter_content" type="hidden">

Then update the content via this.$refs.filterContent.value = content; instead.

🔧 Option 1: Scoped querySelector
-					document.querySelector('[name=filter_content]').value = content;
+					this.$el.querySelector('[name=filter_content]').value = content;

Comment on lines +328 to +340
syncToHidden() {
let content = '';
if (this.filterMode === 'text') {
content = this.textFilter;
} else {
this.filterRows.forEach(row => {
if (row.pattern) {
content += row.action + ' ' + row.type + ' ' + row.pattern + '\n';
}
});
}
document.querySelector('[name=filter_content]').value = content;
}
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

🧩 Analysis chain

🌐 Web query:

Alpine.js this.$el x-data methods documentation

💡 Result:

In Alpine, $el (not this.$el) is the documented way to access the current DOM element from within Alpine expressions and component methods.

  • x-data defines an Alpine component and provides its reactive state (and methods) to the element and its children. You can define methods directly inside the object. [1]
  • $el is a magic property that returns the current DOM node (the element whose directive/expression is running). Example: @click="$el.innerHTML = 'Hello World!'". [2]
  • To reuse an x-data component with methods, define it via Alpine.data(name, factory), then use it like x-data="dropdown"; methods like toggle() can use this.open. [3]
  • If you need the whole current Alpine scope as an object (for passing into external functions), use $data. [4]

Docs (official):

  • x-data: [1]
  • $el magic: [2]
  • Alpine.data(...): [3]
  • $data magic: [4]

Sources: [1] (alpinejs.dev) [2] (alpinejs.dev) [3] (alpinejs.dev) [4] (alpinejs.dev)

Citations:


🏁 Script executed:

# Find Alpine.js files and patterns in the codebase
fd -e '.js' -e '.html' -e '.go' | head -30
# Search for Alpine.js x-data usage patterns
rg "x-data" --type html --type go -A 5 | head -50

Repository: eduardolat/pgbackweb

Length of output: 1021


🏁 Script executed:

# Look at the specific file mentioned in the review
find . -name "edit_backup.go" -o -name "*edit_backup*" 2>/dev/null

Repository: eduardolat/pgbackweb

Length of output: 116


🏁 Script executed:

# Read the edit_backup.go file to see the Alpine.js code context
cat -n internal/view/web/dashboard/backups/edit_backup.go | sed -n '310,360p'

Repository: eduardolat/pgbackweb

Length of output: 1952


🏁 Script executed:

# Get more context around the file - what's the overall structure?
wc -l internal/view/web/dashboard/backups/edit_backup.go

Repository: eduardolat/pgbackweb

Length of output: 118


🏁 Script executed:

# Look for other instances of document.querySelector and syncToHidden patterns
rg "syncToHidden|document\.querySelector" --type go -B 3 -A 3

Repository: eduardolat/pgbackweb

Length of output: 11419


🏁 Script executed:

# Search for x-data usage and modal patterns in the codebase
rg "x-data|x-init" --type go -B 2 -A 5 | head -80

Repository: eduardolat/pgbackweb

Length of output: 46


Hidden input sync can target the wrong modal.
document.querySelector('[name=filter_content]') will always return the first match in the DOM. With multiple edit modals present, syncToHidden() (and x-init) can overwrite another backup's hidden input, causing silent data loss. Use a component-scoped lookup with the Alpine.js magic property $el instead.

🐛 Proposed fix
-							document.querySelector('[name=filter_content]').value = content;
+							$el.querySelector('[name=filter_content]').value = content;
📝 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
syncToHidden() {
let content = '';
if (this.filterMode === 'text') {
content = this.textFilter;
} else {
this.filterRows.forEach(row => {
if (row.pattern) {
content += row.action + ' ' + row.type + ' ' + row.pattern + '\n';
}
});
}
document.querySelector('[name=filter_content]').value = content;
}
syncToHidden() {
let content = '';
if (this.filterMode === 'text') {
content = this.textFilter;
} else {
this.filterRows.forEach(row => {
if (row.pattern) {
content += row.action + ' ' + row.type + ' ' + row.pattern + '\n';
}
});
}
$el.querySelector('[name=filter_content]').value = content;
}
🤖 Prompt for AI Agents
In `@internal/view/web/dashboard/backups/edit_backup.go` around lines 328 - 340,
The syncToHidden function and the component x-init use
document.querySelector('[name=filter_content]') which always selects the first
matching element and can overwrite other modals; update syncToHidden (and any
x-init lookups) to use the component-scoped DOM via Alpine's $el (e.g., use
$el.querySelector('[name=filter_content]') or this.$el.querySelector when inside
the Alpine component) so each modal updates its own hidden input, and ensure you
null-check the result before setting .value to avoid runtime errors.

@gautamsi gautamsi changed the base branch from main to develop January 25, 2026 18:22
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.

3 participants