# Query Builder

> Build optimized and safe SQL queries for Protheus tables. Automatically includes mandatory filters (D_E_L_E_T_, branch), suggests appropriate indexes from SIX patterns, generates both Embedded SQL (preferring FWExecStatement) and Workarea (DBSelectArea/DBSeek) versions, and warns about common Protheus SQL pitfalls. Use when user says 'build query', 'SQL for Protheus table', 'FWExecStatement', 'TCQuery', 'Workarea vs SQL'.

- Skill: `tbc-servicos/query-builder` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add tbc-servicos/query-builder`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tbc-servicos/query-builder/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Data & Analytics
- License: MIT
- Author: tbc-servicos (https://skillmd.com/u/tbc-servicos)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tbc-servicos/query-builder

---


# Protheus Query Builder

## Overview

Build correct, safe, and optimized SQL queries for Protheus ERP tables. Protheus has unique database conventions — mandatory soft-delete filters, multi-branch filtering, Hungarian notation for fields, data dictionary-driven schemas, and specific index patterns — that every query must respect. This skill generates queries that follow these conventions and helps choose between Workarea access and Embedded SQL.

## When to Use

Use this skill when:

- Writing SQL queries against Protheus tables (SA1, SD1, SF2, etc.)
- Building parameterized SQL queries (`FWExecStatement`, `TCSqlExec`) in AdvPL/TLPP
- Optimizing existing queries for Protheus-specific patterns
- Deciding between Workarea access and Embedded SQL
- Ensuring mandatory filters are not missing
- Generating safe queries that prevent SQL injection

---

## Bundled Reference Files

This skill uses progressive disclosure. The SKILL.md body covers conventions, decision logic, the checklist, and anti-patterns. Detailed code templates and cross-database reference tables are in the `references/` directory — read them on demand based on the scenario:

| Reference File | When to Read | Content |
| --- | --- | --- |
| [references/query-patterns-and-examples.md](references/query-patterns-and-examples.md) | Generating **query code** — Workarea, Embedded SQL, multi-table JOINs, TCSqlExec updates, counting, or reviewing SQL injection prevention examples | Full code templates for all 5 query patterns, safe/unsafe FWExecStatement examples, LIKE clause parameterization |
| [references/cross-database-compatibility.md](references/cross-database-compatibility.md) | Handling **cross-database concerns** — ChangeQuery(), TCGetDB(), DBAccess macros, or translating functions between MSSQL / PostgreSQL / Oracle | ChangeQuery and TCGetDB code examples, DBAccess macros table, cross-database function equivalents (9 operations × 4 dialects) |

> Also refer to [references/sonarqube-rules-reference.md](../references/sonarqube-rules-reference.md) for the complete SonarQube rules reference shared across skills.

---

## Protheus Database Conventions

### Table Naming

Protheus table aliases follow the pattern `XXN` where `XX` is the module prefix and `N` is a sequence:

| Prefix | Module            | Example Tables                                                                                                            |
| ------ | ----------------- | ------------------------------------------------------------------------------------------------------------------------- |
| SA     | Customers/Vendors | SA1 (Customers), SA2 (Vendors), SA3 (Salespeople)                                                                         |
| SB     | Products          | SB1 (Products), SB2 (Stock Balances), SB5 (Supplements)                                                                   |
| SC     | Purchasing        | SC1 (Purchase Requests), SC5 (Sales Orders Header), SC6 (Sales Orders Items), SC7 (Purchase Orders)                       |
| SD     | Documents         | SD1 (Incoming Invoices Items), SD2 (Outgoing Invoices Items), SD3 (Internal Movements)                                    |
| SE     | Financial         | SE1 (Accounts Receivable), SE2 (Accounts Payable), SE5 (Cash Movements)                                                   |
| SF     | Invoices          | SF1 (Incoming Invoices Header), SF2 (Outgoing Invoices Header)                                                            |
| SX     | Data Dictionary   | SX1 (Parameters), SX2 (Tables), SX3 (Fields), SX5 (Lookup Tables), SX6 (System Parameters), SX7 (Triggers), SIX (Indexes) |
| ZZ     | Custom            | ZZ1-ZZZ (Custom tables created by customer, also Z01-Z99, ZA0-ZAZ, etc.)                                                  |

### Physical Table Names

The physical table name in the database appends the company code:

| Alias | Physical Name | Rule                                          |
| ----- | ------------- | --------------------------------------------- |
| SA1   | SA1010        | Alias + Company ("01") + Branch padding ("0") |
| SD1   | SD1010        | Same pattern                                  |

Use `RetSQLName("SA1")` to get the correct physical table name dynamically.

### Field Naming

Fields follow the pattern `XX_FIELD` where `XX` matches the table alias prefix:

| Table | Field    | Meaning                 |
| ----- | -------- | ----------------------- |
| SA1   | A1_COD   | Customer code           |
| SA1   | A1_NOME  | Customer name           |
| SD1   | D1_DOC   | Invoice document number |
| SD1   | D1_TOTAL | Invoice item total      |
| SF2   | F2_DOC   | Outgoing invoice number |

### Mandatory System Fields

**Every query against Protheus tables MUST include these filters:**

| Field        | Filter                              | Purpose                                                    |
| ------------ | ----------------------------------- | ---------------------------------------------------------- |
| `D_E_L_E_T_` | `= ' '` (single space)              | Soft-delete flag. Records with `'*'` are logically deleted |
| `XX_FILIAL`  | `= cFilAnt` or `= FWxFilial("XXX")` | Multi-branch filter                                        |

```sql
-- ALWAYS include both filters
SELECT A1_COD, A1_NOME
FROM SA1010 SA1
WHERE SA1.D_E_L_E_T_ = ' '
  AND SA1.A1_FILIAL = '01'
```

> **Warning:** Omitting `D_E_L_E_T_` will return deleted records. Omitting the branch filter will return records from all branches, which is usually incorrect and a security risk.

---

## Query Patterns

This skill supports five query patterns. Read [references/query-patterns-and-examples.md](references/query-patterns-and-examples.md) for the full code templates.

| Pattern | Approach | When to Use |
| --- | --- | --- |
| 1 — Simple Select (Workarea) | `DbSelectArea` + `DbSeek` | Single record lookup by existing index, record-by-record operations |
| 2 — Simple Select (Embedded SQL) | `FWExecStatement` + `OpenAlias` / `ExecScalar` | Aggregations, complex filters, no suitable index for Workarea |
| 3 — Multi-Table Join | Embedded SQL with `INNER JOIN` | Cross-table reporting, invoice details with product names |
| 4 — INSERT/UPDATE (TCSqlExec) | `FWExecStatement` + `TCSqlExec` | Direct SQL writes (bypasses data dictionary triggers) |
| 5 — Counting Records | `FWExecStatement:ExecScalar()` on `SELECT COUNT(*)` | Record counts with filters |

> **Important:** Pattern 4 (TCSqlExec) bypasses data dictionary validations and triggers. Prefer Workarea `RecLock`/`MsUnlock` for standard CRUD operations.

---

## Workarea vs. Embedded SQL Decision Matrix

| Criterion                            | Workarea (DBSeek)            | Embedded SQL (`FWExecStatement`)  |
| ------------------------------------ | ---------------------------- | --------------------------------- |
| Single record lookup by key          | **Best choice**              | Acceptable                        |
| Sequential scan by index             | **Best choice**              | Acceptable                        |
| Complex multi-table joins            | Poor (requires nested seeks) | **Best choice**                   |
| Aggregations (SUM, COUNT)            | Very poor                    | **Best choice**                   |
| Large result sets                    | **Better memory control**    | Good (but watch alias handling)   |
| Record locking for update            | **Required**                 | Use `FWExecStatement` + `TCSqlExec` (bypasses triggers) |
| Performance (key-based)              | **Fastest**                  | Slight overhead                   |
| Data dictionary trigger execution    | **Automatic**                | **Not triggered**                 |
| Index requirement                    | Must have suitable SIX index | Any column                        |
| Code readability for complex queries | Poor                         | **Best choice**                   |

> **Rule of thumb:** Use Workarea for CRUD operations on single records. Use Embedded SQL for reporting, aggregation, and complex joins.

---

## Index Awareness (SIX Dictionary)

Protheus indexes are defined in the SIX table. The first index of each table (order 1) is typically the primary key.

### Common Index Patterns

| Table | Order | Key Expression                                                            | Use Case                      |
| ----- | ----- | ------------------------------------------------------------------------- | ----------------------------- |
| SA1   | 1     | `A1_FILIAL + A1_COD + A1_LOJA`                                            | Primary key — customer lookup |
| SA1   | 3     | `A1_FILIAL + A1_CGC`                                                      | Find customer by tax ID       |
| SA2   | 1     | `A2_FILIAL + A2_COD + A2_LOJA`                                            | Primary key — vendor lookup   |
| SB1   | 1     | `B1_FILIAL + B1_COD`                                                      | Primary key — product lookup  |
| SD1   | 1     | `D1_FILIAL + D1_DOC + D1_SERIE + D1_FORNECE + D1_LOJA + D1_COD + D1_ITEM` | Incoming invoice item         |
| SD2   | 1     | `D2_FILIAL + D2_DOC + D2_SERIE + D2_CLIENTE + D2_LOJA + D2_COD + D2_ITEM` | Outgoing invoice item         |
| SE1   | 1     | `E1_FILIAL + E1_PREFIXO + E1_NUM + E1_PARCELA + E1_TIPO`                  | Accounts receivable           |
| SF2   | 1     | `F2_FILIAL + F2_DOC + F2_SERIE`                                           | Outgoing invoice header       |

### Using Indexes in Embedded SQL

For SQL Server, use the `%nolock%` DBAccess macro on read queries. This macro translates to `WITH (NOLOCK)` on MSSQL and is silently ignored on PostgreSQL/Oracle (MVCC), making it safe to use in cross-database code:

```sql
SELECT A1_COD, A1_NOME
FROM SA1010 SA1 WITH (%nolock%)
WHERE SA1.D_E_L_E_T_ = ' '
  AND SA1.A1_FILIAL = '01'
  AND SA1.A1_COD = '000001'
  AND SA1.A1_LOJA = '01'
```

> **Tip:** Always order WHERE clauses to match the index key expression order for the query optimizer.

---

## SQL Injection Prevention

**Never concatenate user input directly into SQL strings.** Use `FWExecStatement` to parameterize all dynamic values.

The safe pattern:
1. Build the SQL with `?` placeholders for every dynamic value (filter, branch, deletion flag).
2. Wrap it in `ChangeQuery()` for cross-database compatibility.
3. Instantiate with `FWExecStatement():New(cQuery)` (or `FWPreparedStatement` on libs older than `20211116`).
4. Bind values 1..N with `SetString()`, `SetNumeric()`, `SetDate()`, `SetBoolean()`, `SetIn()`.
5. Execute:
   - SELECT returning a cursor → `cAlias := oStatement:OpenAlias([cAlias][, cLifeTime, cTimeout])`
   - SELECT returning a single scalar → `xValue := oStatement:ExecScalar(cColumn[, cLifeTime, cTimeout])`
   - INSERT / UPDATE / DELETE → `TCSqlExec(oStatement:GetFixQuery())`
6. Always close opened aliases (`(cAlias)->(DBCloseArea())`) and call `oStatement:Destroy()`.

For LIKE clauses, build the `%` wildcard on the AdvPL side (`"%" + cSearch + "%"`) and bind the whole string as a single `?` parameter — this avoids cross-DB concat operator differences.

> **Do not use `SetUnsafe()` with user input.** It bypasses bind safety and reintroduces SQL injection risk; reserve it for identifiers built from constants.

> **Cache (optional):** `OpenAlias` / `ExecScalar` accept `cLifeTime` and `cTimeout` (seconds, as character) to reuse a cached result from DBAccess for hot, repeatedly executed queries.

> Read [references/query-patterns-and-examples.md](references/query-patterns-and-examples.md) for complete safe/unsafe code examples, the full method reference, and LIKE-clause examples.

---

## Common Anti-Patterns

| Anti-Pattern                                  | Problem                                              | Fix                                                                                   |
| --------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------- |
| Missing `D_E_L_E_T_` filter                   | Returns deleted records                              | Always add `WHERE D_E_L_E_T_ = ' '`                                                   |
| Missing branch filter                         | Returns records from all branches                    | Always add `AND XX_FILIAL = '...'`                                                    |
| `SELECT *` on Protheus tables                 | Returns dozens of system fields, slow                | List only the fields you need                                                         |
| Full table scan on SD1/SD2                    | Millions of rows, very slow                          | Use indexed columns in WHERE                                                          |
| Not using `%nolock%`                          | Lock contention on read queries                      | Add `WITH (%nolock%)` to SELECT tables — cross-DB safe (ignored on PostgreSQL/Oracle) |
| `D_E_L_E_T_` filter on JOINs missing          | Joined table returns deleted records                 | Add `D_E_L_E_T_ = ' '` to every table in JOIN                                         |
| Hardcoded company/branch codes                | Breaks in multi-company environments                 | Use `FWxFilial()`, `RetSQLName()`                                                     |
| Not closing query aliases / `FWExecStatement` | Memory leak, alias exhaustion                        | Always call `(cAlias)->(DBCloseArea())` and `oStatement:Destroy()`                    |
| Macro execution in SQL                        | SQL injection risk                                   | Use `FWExecStatement` for parameterized values                                    |
| Missing `RetSQLName()`                        | Wrong physical table name in multi-company           | Always use `RetSQLName("XXX")`                                                        |
| Creating procedures in source                 | Prohibited by SonarQube                              | Use SPManager for procedure management                                                |
| Using IIF in SQL expressions                  | Prohibited — use CASE WHEN or If/Else in AdvPL logic | Replace `IIF()` with `If/Else/EndIf` or SQL `CASE WHEN`                               |
| Direct access to SIX/SX2/SX3 via DbSelectArea | Prohibited metadata access                           | Use `RetSqlName()` for SX2, `FWSX3Util()` for SX3, standard APIs for SIX              |
| GetMV/ExistBlock inside loops                 | Performance degradation                              | Cache result in a variable before the loop                                            |

---

## Query Building Checklist

### Mandatory

- [ ] `D_E_L_E_T_ = ' '` included for every table in the query
- [ ] Branch filter (`XX_FILIAL`) included for every table
- [ ] Physical table name obtained via `RetSQLName("XXX")`
- [ ] User-provided values parameterized with `FWExecStatement` (preferred over raw `TCQuery`)
- [ ] Result alias name generated with `GetNextAlias()` and opened via `FWExecStatement:OpenAlias()`
- [ ] Opened alias closed with `DBCloseArea()` in all code paths (including errors) and `FWExecStatement:Destroy()` is called

### Performance

- [ ] Only required fields listed (no `SELECT *`)
- [ ] WHERE clause order matches index key expression
- [ ] `%nolock%` hint used for SQL Server read queries
- [ ] Pagination used for large result sets
- [ ] JOINs reference indexed columns

### Safety

- [ ] No macro-execution (`&cExpr`) in SQL strings
- [ ] No hardcoded company/branch codes
- [ ] Error handling wraps the query execution (Try-Catch)
- [ ] Workarea area saved and restored with `GetArea()`/`RestArea()`
- [ ] No `IIF()` in query construction or expressions — use `If/Else/EndIf` or SQL `CASE WHEN`
- [ ] No `CREATE PROCEDURE` in source — use SPManager
- [ ] `GetMV()`/`ExistBlock()` results cached before use in loops

> Refer to [references/sonarqube-rules-reference.md](../references/sonarqube-rules-reference.md) for the complete SonarQube rules reference.

---

## Cross-Database Compatibility

Protheus supports **PostgreSQL**, **MSSQL Server**, and **Oracle**. All generated SQL must work across all three, or use framework tools to handle differences:

- **`ChangeQuery(cQuery)`** — translates SQL syntax to the active database dialect (`TOP` → `LIMIT`, etc.). BeginSQL/EndSQL calls it automatically.
- **`TCGetDB()`** — returns `"MSSQL"`, `"ORACLE"`, or `"POSTGRES"` for DB-specific branching when `ChangeQuery()` is insufficient.
- **DBAccess macros** — `%nolock%`, `%notDel%`, `%table:XXX%`, `%Order:XXX%` — translated per database in SQL strings.
- **Best practice:** Always use ANSI-compatible SQL (`COALESCE`, `CASE WHEN`, `CONCAT`, `FETCH FIRST`) or rely on `ChangeQuery()`.

> Read [references/cross-database-compatibility.md](references/cross-database-compatibility.md) for full code examples, the DBAccess macros table, and the cross-database function equivalents reference (MSSQL vs PostgreSQL vs Oracle).

---

## Troubleshooting

- **Missing `D_E_L_E_T_` filter**: All Protheus queries must include `D_E_L_E_T_ = ' '` (or equivalent `%notDel%`) to exclude logically deleted records. Omitting this returns deleted rows.
- **Wrong physical table name**: Use `RetSQLName('alias')` to get the physical table name. Hardcoded names like `SA1010` will break across environments with different company codes.
- **Index not being used**: Ensure the `WHERE` clause column order matches the SIX index key expression. Use `%nolock%` hint for SQL Server read-only queries to avoid lock contention.
- **Branch/company filter missing**: Use `FWxFilial('alias')` for the branch filter. Hardcoding branch codes causes cross-branch data leakage.
- **SQL injection via macro-execution**: Never use `&(cExpr)` to build SQL strings. Use `FWExecStatement` to parameterize all dynamic values in queries.


