--- name: add-query-to-table description: Adds a new DB query to a page and wires it to a Table widget; documents how parameter/search syntax works (widget bindings, optional date range). Use when the user asks to add a query, connect a query to a table, create a new query, or how parameters/filters work in queries. --- # Add Query to Table & Parameter Search Syntax Use this skill when adding a new query to a page, wiring a query to a table, or when you need the correct syntax for parameterized filters (e.g. date range from widgets). ## 1. Query structure (single source of truth) Each query lives under the page in a folder named after the query: - **Path**: `pages//queries//` - **Files**: - **`.txt`** -- Raw SQL. This is the **source of truth** for the query body. Keep it readable (real newlines, no escaping). - **`metadata.json`** -- Appsmith action config. The `body` inside `unpublishedAction.actionConfiguration` must match the SQL in the `.txt` file, stored as a single-line string with `\n` for newlines and `\"` for double quotes. **Naming**: Use lowercase with underscores (e.g. `units_ordered_by_series`, `pending_pos_slx_pending`). The query name is the identifier used in bindings (e.g. `{{query_name.data}}`). ## 2. Metadata.json required fields - **`gitSyncId`** (CRITICAL): Must be the **first field** in the root object. Format: `"<24-char-hex>_"`. Without this, Appsmith will auto-remove the query on the next pull/sync. Generate with: ```bash python3 -c "import uuid; print(uuid.uuid4().hex[:24] + '_' + str(uuid.uuid4()))" ``` Use the same 24-char hex prefix as the page's `gitSyncId` but a unique UUID suffix. - **`id`**: `"_"` (e.g. `"Pending POs - SLx Pending_pending_pos_slx_pending"`). - **`name`**: `""` (must match the folder and the name used in bindings). - **`unpublishedAction.actionConfiguration.body`**: The exact SQL from `.txt`, escaped for JSON (newlines -> `\n`, `"` -> `\"`). - **`unpublishedAction.datasource`**: Use the same as other queries on the page (e.g. `xTuple_Sandbox`). - **`unpublishedAction.pageId`**: `""`. - **`unpublishedAction.dynamicBindingPathList`**: `[{"key":"body"}]` when the body contains `{{...}}` widget references. - Keep **`pluginId`**: `"postgres-plugin"`, **`pluginType`**: `"DB"`, and existing flags like **`encodeParamsToggle`**, **`paginationType`**, **`pluginSpecifiedTemplates`**, **`timeoutInMillisecond`** consistent with other DB queries in the app. When adding a new query, copy an existing query's `metadata.json` from the same page and change `gitSyncId`, `id`, `name`, and `body` (and ensure `body` stays in sync with the new `.txt` file). ## 3. Wiring the query to a Table widget - In the Table widget JSON (e.g. `widgets/.../Table1.json`), set: - **`tableData`**: `"{{.data}}"` - Table columns read from the query result by **key**. The key is the **SELECT alias** from the query (e.g. `"Product"`, `"Qty Ordered"`). In `primaryColumns`, the column **id** can be a safe identifier (e.g. `Qty_Ordered`) while **originalId** / **alias** / **label** match the display name; **computedValue** must use the same key as in the query result, e.g. `currentRow["Qty Ordered"]`. So: **query SELECT aliases = keys in the table's row object**. Keep column keys and any `currentRow["..."]` references in the table in sync with those aliases. ## 4. Parameter search syntax (widgets in SQL) ### 4.1 Referencing a widget value - **Syntax**: `{{WidgetName.property}}` - **Examples**: - DatePicker date: `{{SeriesDateFrom.selectedDate}}`, `{{PowerDateTo.selectedDate}}` - Other widgets: use the widget's value property (e.g. `selectedOptionValue`, `text`) as per Appsmith docs. Values are injected as strings. For PostgreSQL dates you typically cast in SQL, e.g. `'{{DatePicker1.selectedDate}}'::date`. ### 4.2 Optional date range (no filter when either date is empty) Use this pattern so that: - If **either** date widget is empty -> the date condition is **not** applied (all dates allowed). - If **both** are set -> filter by `date_column BETWEEN from ::date AND to ::date`. **SQL pattern** (replace widget names and column as needed): ```sql AND ( NULLIF('{{DateFromWidget.selectedDate}}','') IS NULL OR NULLIF('{{DateToWidget.selectedDate}}','') IS NULL OR date_column BETWEEN NULLIF('{{DateFromWidget.selectedDate}}','')::date AND NULLIF('{{DateToWidget.selectedDate}}','')::date ) ``` - **Logic**: `NULLIF('{{...}}','')` turns an empty string into SQL `NULL`. If either widget is empty, one of the first two conditions is true, so the whole `AND (...)` is true and the BETWEEN is not applied. When both are non-empty, the third branch applies the range. - **Widget names**: Use the actual widget names (e.g. `SeriesDateFrom` / `SeriesDateTo` for one tab, `PowerDateFrom` / `PowerDateTo` for another). Ensure those widgets exist on the same page and (if in a tab) the same tab so the query runs with the right context. ### 4.3 Required parameters (always filter) If the filter must always be applied (no "show all" when empty): - Use the binding directly and ensure the widget always has a value, or use a default in the widget. - Example: `AND cohead_orderdate BETWEEN '{{DateFrom.selectedDate}}'::date AND '{{DateTo.selectedDate}}'::date` -- then empty dates may produce invalid SQL or empty results, so prefer the optional pattern above unless the UI guarantees non-empty values. ## 5. Checklist when adding a new query 1. Create `pages//queries//`. 2. Add `.txt` with the full SQL (source of truth). 3. Add `metadata.json` with correct `gitSyncId`, `id`, `name`, `body` (body = SQL from .txt, JSON-escaped), and same datasource/pageId as other page queries. 4. If the SQL uses `{{...}}`, set `dynamicBindingPathList` to `[{"key":"body"}]`. 5. In the Table widget that should show the data, set `tableData` to `{{.data}}`. 6. Ensure table column keys / `currentRow["..."]` match the query's SELECT aliases. ## 6. Renaming or replacing a query - To **rename** (e.g. `units_shipped_by_series` -> `units_ordered_by_series`): - Create the new folder and files under the new name (with a new `gitSyncId`). - Update every reference to the old query (e.g. `tableData`: `{{old_name.data}}` -> `{{new_name.data}}`). - Remove the old query folder (both `.txt` and `metadata.json`). - When **replacing** the SQL but keeping the same name, update the `.txt` first, then update the `body` in `metadata.json` to match (same content, JSON-escaped). ## Reference - **gitSyncId**: `<24-char-hex>_`. Without this, Appsmith auto-removes the query on sync. - **Optional date range**: NULLIF + IS NULL + OR + BETWEEN as above. - **Table data binding**: `{{.data}}`. - **Column keys**: Must match query SELECT aliases (e.g. `"Product"`, `"Qty Ordered"`).