12 Commits
Author SHA1 Message Date
Hein cf9ea5fbda chore(release): bump version to 0.0.8
CI / Build (push) Skipped
CI / Test (push) Failing after 36s
Release / Test (push) Successful in 12s
Release / Release (push) Successful in 3m20s
Release / VSCode Extension (push) Successful in 23s
Release / AUR package (push) Successful in 41s
Release / Debian packages (push) Successful in 1m27s
Release / Windows installer (push) Successful in 1m39s
Release / RPM package (push) Successful in 2m16s
Release / DataGrip Plugin (push) Successful in 3m6s
2026-09-10 15:27:41 +02:00
Hein 5cdec88299 feat(format): expand the runtime safety gate
CI / Test (push) Successful in 37s
CI / Build (push) Successful in 52s
Replace the bare SemanticallyEqual call in every frontend (CLI fmt, LSP
formatting + rangeFormatting) with format.VerifySafe, which runs four
checks before any formatted output is emitted:

  - semantic equivalence  - the code token stream is unchanged
  - comment preservation  - no -- or /* */ comment is dropped, merged,
    split, reordered, or reworded (line endings / indentation normalised
    away; recurses into dollar-quoted bodies)
  - structural balance    - the () [] and BEGIN/CASE/IF/LOOP...END nesting
    profile matches, ignoring anything inside a comment or a string
  - idempotence           - a second format pass would not change it

On failure the CLI now prints the specific reason and keeps the original.

The comment check surfaced two real formatter bugs, both fixed in
formatBodyStatements:

  - multi-line /* */ comments inside a PL/pgSQL body had their interior
    lines re-split and reindented as if they were statements; they are
    now tracked and carried verbatim with the opening line
  - a column-0 -- line was glued onto the preceding line by the
    split-line-join, which merged consecutive comment lines into one

Regenerate testdata/corpus/test_mm_proc.pgsql (was carrying the mangled
output). TestCorpusIdempotentAndSafe now runs the full VerifySafe bundle;
add safety_test.go with targeted cases.
2026-09-10 15:17:07 +02:00
Hein 83b215fd25 feat(format): default output to dist/examples house style
CI / Test (push) Successful in 1m12s
CI / Build (push) Successful in 30s
Align the formatter defaults and layout with the hand-formatted reference
procedures in dist/examples so a clean `pgtidy fmt` produces the house style.

config.Default():
- align_param_types: false (no type-column alignment in param lists)
- plpgsql_declare_align_type / plpgsql_declare_align_eq: true

Formatter:
- routine header: leading-comma params at column 0, first param at one
  indent, RETURNS/LANGUAGE/volatility/SECURITY each indented one level
- %type / %rowtype printed tight (isPctTypeBoundary)
- DECLARE = / := / DEFAULT column padded only to the widest declaration
  that carries an assignment
- WHERE continuations in body UPDATE/DELETE: AND/OR aligned with WHERE
- EXCEPTION aligned to its enclosing BEGIN; column-0 comment continuations
  kept flush-left

Safety gate:
- SemanticallyEqual tolerates CRLF vs LF inside string literals (normNL);
  the formatter re-emits all layout with st.Newline, so a \r\n inside a
  multi-line string literal is normalisation, not a code change. This was
  why action_init and event_exec_func previously refused to format.

Corpus:
- add the four CRLF reference files as idempotence/safety fixtures
- regenerate test_a and test_mm_proc goldens

FOR...LOOP body indentation keeps the existing +1 convention (LOOP aligned
with FOR); the dist/examples use +2, so loop-body regions differ by
whitespace only.
2026-09-10 14:54:20 +02:00
warkanum 94d776e3de Merge pull request 'docs(lsp): LSP capability inventory, gaps & next steps (issue #3)' (#4) from issue-3-lsp-research into main
CI / Test (push) Successful in 6m35s
CI / Build (push) Successful in 8s
Reviewed-on: #4
2026-08-23 06:59:50 +00:00
SG Command 1be745b69c docs(lsp): add capability inventory, gaps, and next steps (issue #3)
CI / Test (pull_request) Successful in 35s
CI / Build (pull_request) Successful in 20s
2026-08-22 23:19:10 +02:00
Hein 25d40ad515 chore(release): bump version to 0.0.7
CI / Build (push) Successful in 20s
Release / Release (push) Successful in 36s
Release / Debian packages (push) Successful in 33s
Release / VSCode Extension (push) Successful in 35s
Release / AUR package (push) Successful in 55s
Release / RPM package (push) Successful in 56s
Release / Windows installer (push) Successful in 59s
Release / DataGrip Plugin (push) Successful in 4m29s
CI / Test (push) Successful in 33s
Release / Test (push) Successful in 31s
2026-07-17 14:29:45 +02:00
Hein bac966c2ac feat(format): add runtime semantic-equality safety gate
CI / Build (push) Successful in 23s
CI / Test (push) Successful in 31s
2026-07-17 14:29:24 +02:00
Hein 4abc89700f chore(release): bump version to 0.0.6
Release / AUR package (push) Successful in 53s
Release / DataGrip Plugin (push) Successful in 4m17s
Release / Release (push) Successful in 1m57s
Release / VSCode Extension (push) Successful in 56s
Release / Test (push) Successful in 1m50s
CI / Test (push) Successful in 1m51s
CI / Build (push) Successful in 21s
Release / RPM package (push) Successful in 39s
Release / Debian packages (push) Successful in 47s
Release / Windows installer (push) Successful in 47s
2026-07-17 13:21:20 +02:00
Hein ad52f21cc2 feat(datagrip): migrate from LSP4IJ to native CLI integration
CI / Test (push) Successful in 26s
CI / Build (push) Successful in 22s
2026-07-17 13:20:49 +02:00
Hein 60602d1de7 chore(release): bump version to 0.0.5
CI / Test (push) Successful in 25s
CI / Build (push) Successful in 20s
Release / Test (push) Failing after 1m51s
Release / Release (push) Has been skipped
Release / Debian packages (push) Has been skipped
Release / RPM package (push) Has been skipped
Release / Windows installer (push) Has been skipped
Release / AUR package (push) Has been skipped
Release / VSCode Extension (push) Has been skipped
Release / DataGrip Plugin (push) Has been skipped
2026-07-17 09:40:51 +02:00
warkanum 8d19258aa0 Merge pull request 'Fix PL/pgSQL formatting indentation' (#2) from issue-1-formatting-indenting into main
CI / Test (push) Successful in 26s
CI / Build (push) Successful in 29s
Reviewed-on: #2
2026-07-16 19:38:55 +00:00
Hein 5cba6beeb1 fix: align PLpgSQL formatting clauses
CI / Build (pull_request) Successful in 1m2s
CI / Test (pull_request) Successful in 43s
2026-07-15 21:11:58 +02:00
38 changed files with 4720 additions and 301 deletions
+12
View File
@@ -365,6 +365,12 @@ jobs:
with:
version: latest
- name: Set version from tag
working-directory: editors/vscode
run: |
TAG="${{ github.event.inputs.tag || github.ref_name }}"
npm pkg set version="${TAG#v}"
- name: Install and package
working-directory: editors/vscode
run: |
@@ -402,6 +408,12 @@ jobs:
distribution: temurin
java-version: '21'
- name: Set version from tag
working-directory: editors/datagrip
run: |
TAG="${{ github.event.inputs.tag || github.ref_name }}"
sed -i "s/^pluginVersion=.*/pluginVersion=${TAG#v}/" gradle.properties
- name: Build plugin
working-directory: editors/datagrip
run: ./gradlew buildPlugin
+8
View File
@@ -44,6 +44,10 @@ jobs:
cache: npm
cache-dependency-path: editors/vscode/package-lock.json
- name: Set version from tag
working-directory: editors/vscode
run: npm pkg set version="${GITHUB_REF_NAME#v}"
- name: Install and package
working-directory: editors/vscode
run: |
@@ -73,6 +77,10 @@ jobs:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Set version from tag
working-directory: editors/datagrip
run: sed -i "s/^pluginVersion=.*/pluginVersion=${GITHUB_REF_NAME#v}/" gradle.properties
- name: Build plugin
working-directory: editors/datagrip
run: ./gradlew buildPlugin
+3 -1
View File
@@ -164,4 +164,6 @@ dist
.pnp.*
temp/*
dist/*
dist/*
# Local build of the CLI binary
/pgtidy
+27 -3
View File
@@ -36,7 +36,7 @@ pkg/pgast/ — go-pgquery wrapper: SQL → real PG AST (lint, v2)
pkg/lint/ — rule engine + rule packs (v2)
pkg/lsp/ — LSP server (v3)
editors/vscode/ — VSCode extension (v3)
editors/datagrip/ — LSP4IJ integration (v4)
editors/datagrip/ — native JetBrains plugin, shells out to the CLI (v4)
testdata/corpus/ — real-world .pgsql procedures used as the safety/idempotence harness
```
@@ -59,6 +59,29 @@ testdata/corpus/ — real-world .pgsql procedures used as the safety/idempoten
3. **Idempotence**: `fmt(fmt(x)) == fmt(x)`.
4. **Graceful degradation**: any span the parser cannot handle is passed through verbatim
rather than corrupted.
5. **Runtime safety gate**: every frontend (CLI `fmt`, LSP `textDocument/formatting` and
`rangeFormatting`) calls `format.VerifySafe(src, out, style)` before writing or returning
formatted output. If it returns a non-nil error the formatter has a bug — the caller must
refuse to write/emit the result and keep the original source, never guess or best-effort
it. This is enforced at runtime, not just in `pkg/format/*_test.go`, so a formatter bug can
never silently drop or alter code. `VerifySafe` runs four checks:
- **Semantic equivalence** (`SemanticallyEqual`): re-lex both sides, compare the non-trivia
token streams — case-insensitive for identifiers/keywords, exact otherwise, recursing
into dollar-quoted bodies. Comments and whitespace are trivia and are ignored here.
- **Comment preservation** (`CommentsPreserved`): every `--` and `/* */` comment in `src`
reappears in `out`, in order, with the same content (line endings and indentation are
normalised away; dropping, merging, splitting, reordering, or rewording a comment is
not). Descends into dollar-quoted bodies.
- **Structural balance** (`StructurallyBalanced`): the `( ) [ ]` and BEGIN/CASE/IF/LOOP…END
nesting profile of `out` matches `src`, counting only real code tokens (comment and
string/dollar-quote contents are skipped).
- **Idempotence**: re-formatting `out` yields `out` unchanged.
**Line endings**: the formatter re-emits all layout with `st.Newline` (default `\n`), so a
CRLF input file is normalised to LF on write. This includes `\r\n` that sits *inside* a
multi-line string literal — a line-ending change there is layout normalisation, not a change
of code content, so `SemanticallyEqual` compares string tokens modulo `\r\n` ↔ `\n`
(`normNL` in `pkg/format/safety.go`). Set `newline: "\r\n"` in `.pgtidy.yaml` to keep CRLF.
## Commands
@@ -111,8 +134,9 @@ go fmt ./... # Format Go code
Per-platform VSIX (`win32/linux/darwin × x64/arm64`) built in CI matrix.
### V4 — DataGrip
- `editors/datagrip`: integrate via LSP4IJ (free, works across JetBrains editions incl.
DataGrip). No core changes expected.
- `editors/datagrip`: native JetBrains plugin (no LSP4IJ dependency) that shells out to the
`pgtidy` binary directly. Formatting and version info run `pgtidy fmt`/`version`; lint
diagnostics and quick-fixes run via a native `ExternalAnnotator` calling `pgtidy lint --json`.
## Verification
- **Formatter:** `go test ./...` runs golden-file tests + corpus harness asserting idempotence
+9
View File
@@ -63,6 +63,10 @@ func cmdFmt(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
return 2
}
out := format.File(parser.Parse(string(src)), st)
if err := format.VerifySafe(string(src), out, st); err != nil {
_, _ = fmt.Fprintf(stderr, "pgtidy: refusing to format stdin: formatter safety check failed: %v\n", err)
return 2
}
switch {
case check:
if out != string(src) {
@@ -86,6 +90,11 @@ func cmdFmt(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
continue
}
out := format.File(parser.Parse(string(src)), st)
if err := format.VerifySafe(string(src), out, st); err != nil {
_, _ = fmt.Fprintf(stderr, "pgtidy: refusing to format %s: formatter safety check failed: %v\n", path, err)
exit = 2
continue
}
changed := out != string(src)
if changed {
anyDiff = true
+30 -8
View File
@@ -1,6 +1,7 @@
package main
import (
"encoding/json"
"fmt"
"io"
"os"
@@ -18,9 +19,10 @@ import (
// file:line:col: [RULEID] severity: message
func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
var (
only []string // --only=ID,ID rule filter
fix bool
files []string
only []string // --only=ID,ID rule filter
fix bool
jsonOutput bool
files []string
)
for _, a := range args {
switch {
@@ -29,6 +31,8 @@ func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
return 0
case a == "--fix":
fix = true
case a == "--json":
jsonOutput = true
case strings.HasPrefix(a, "--only="):
ids := strings.Split(strings.TrimPrefix(a, "--only="), ",")
for _, id := range ids {
@@ -66,7 +70,13 @@ func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
return diags, nil
}
printDiags := func(diags []diagnostics.Diagnostic) {
var jsonDiags []diagnostics.Diagnostic
reportDiags := func(diags []diagnostics.Diagnostic) {
if jsonOutput {
jsonDiags = append(jsonDiags, diags...)
return
}
for _, d := range diags {
loc := d.File
if d.Line > 0 {
@@ -92,14 +102,14 @@ func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
_, _ = fmt.Fprintf(stderr, "pgtidy: %v\n", err)
return 2
}
if fix {
if fix && !jsonOutput {
fixed := lint.ApplyFixes(string(src), diags)
if fixed != string(src) {
_, _ = io.WriteString(stdout, fixed)
return 0
}
}
printDiags(diags)
reportDiags(diags)
if len(diags) > 0 {
found = true
}
@@ -115,7 +125,7 @@ func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
_, _ = fmt.Fprintf(stderr, "pgtidy: %v\n", err)
return 2
}
if fix {
if fix && !jsonOutput {
fixed := lint.ApplyFixes(string(src), diags)
if fixed != string(src) {
if err := os.WriteFile(path, []byte(fixed), 0o644); err != nil {
@@ -130,13 +140,24 @@ func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
}
}
}
printDiags(diags)
reportDiags(diags)
if len(diags) > 0 {
found = true
}
}
}
if jsonOutput {
if jsonDiags == nil {
jsonDiags = []diagnostics.Diagnostic{}
}
enc := json.NewEncoder(stdout)
if err := enc.Encode(jsonDiags); err != nil {
_, _ = fmt.Fprintf(stderr, "pgtidy: encoding json: %v\n", err)
return 2
}
}
if found {
return 1
}
@@ -150,6 +171,7 @@ Read SQL from files (or stdin) and report lint findings.
Flags:
--fix Apply autofixes for fixable rules (MIG001, MIG003) and rewrite files
--json Emit findings as a JSON array instead of text (ignores --fix)
--only=ID,... comma-separated rule IDs to enable (default: all rules)
-h, --help show this help
+4 -4
View File
@@ -47,14 +47,14 @@ insert_collapse_values: true # Fold multiple VALUES rows onto fewer lines
# --- Routines (functions / procedures) ---
align_param_types: true # Pad param names so type column aligns across all params
align_param_types: false # Pad param names so type column aligns across all params
routine_as_wrap: true # Newline before AS $$ (false = keep AS on same line)
# --- PL/pgSQL body ---
plpgsql_max_blank_lines: 1 # Max consecutive blank lines in body
plpgsql_declare_align_type: false # Align type column in DECLARE block
plpgsql_declare_align_eq: false # Align := / = in DECLARE block
plpgsql_max_blank_lines: 1 # Max consecutive blank lines in body
plpgsql_declare_align_type: true # Align type column in DECLARE block
plpgsql_declare_align_eq: true # Align := / = in DECLARE block (padded to the widest assigned declaration)
plpgsql_if_then_newline: true # THEN on its own line (false = same line as condition)
plpgsql_loop_collapse: true # Collapse empty loop bodies to one line
+116
View File
@@ -0,0 +1,116 @@
# PgTidy LSP Status & Roadmap
This document describes the current state of the PgTidy LSP server (`pkg/lsp`,
`cmd/pgtidy/lsp.go`), what it actually provides today, and concrete next steps.
It was written as part of issue #3 ("See what we can provide for LSP").
> Note: The V3 milestone is already implemented and shipping (`docs/todo.md` marks
> V3 done), so this is an inventory + gap analysis, not a greenfield proposal.
## What the server provides today
Verified by hand against the built binary (`pgtidy lsp`, JSON-RPC 2.0 over stdio,
`Content-Length` framing) — no external LSP library, all wire types hand-rolled.
### Advertised capabilities (`initialize` → `capabilities`)
| Capability | Value | Where |
|---|---|---|
| `textDocumentSync` | `1` (full sync) | `serverCaps` |
| `documentFormattingProvider` | `true` | `handle("initialize")` |
| `documentRangeFormattingProvider` | `true` | `handle("initialize")` |
| `codeActionProvider` | `true` | `handle("initialize")` |
### Supported methods
| Method | Direction | Behavior |
|---|---|---|
| `initialize` / `shutdown` / `exit` / `initialized` | req/resp + notif | Lifecycle. `exit`/`shutdown` acknowledged with `null` result. |
| `textDocument/didOpen` | notif | Stores document text; triggers `publishDiagnostics`. |
| `textDocument/didChange` | notif | Stores latest content version; triggers `publishDiagnostics`. |
| `textDocument/didClose` | notif | Drops text + fix cache; clears diagnostics with empty list. |
| `textDocument/formatting` | req/resp | Full-doc format via `pkg/format`; returns one `fullReplace` `TextEdit`. |
| `textDocument/rangeFormatting` | req/resp | Formats doc, returns minimal edit over the selected line range. |
| `textDocument/codeAction` | req/resp | Returns quick-fix `WorkspaceEdit`s for fixable diagnostics overlapping the range. |
| `textDocument/publishDiagnostics` | notif | Sent on every open/change; diagnostic code = `RuleID`, source = `pgtidy`. |
| `$/cancelRequest` | req | Ignored (per LSP, no response). |
| unknown | req/resp | `-32601 method not found` (when the request has an `id`). |
### Verified at runtime (e2e smoke test)
- `initialize` returns the capability block above.
- `didOpen` on `select * from t;` → `publishDiagnostics` with `COR001`
("SELECT * is fragile…", severity 4 = hint, code `COR001`).
- `textDocument/formatting` on that input → edit replacing with
`SELECT *\nFROM t;\n` (keyword casing + clause-per-line applied).
- `textDocument/hover` → `-32601 method not found` (not implemented — correct).
## What the server does NOT provide (gaps)
These are the most useful, well-scoped gaps to fill next. None are blockers for the
current shipping state.
1. **No `hover`.** `textDocument/hover` is unimplemented and returns `-32601`.
A natural first add: return the `RuleID` + a short explanation for diagnostics
on the hovered range, or a keyword/type doc for `hover` on SQL identifiers.
2. **No `documentSymbol` / `documentLink`.** No outline/symbol tree. For a formatter
that already parses `CREATE FUNCTION`/`PROCEDURE` headers into a CST, a symbol
provider listing functions/procedures would be low-cost and high-value in large
schema files.
3. **`hover`-style diagnostics shape.** Diagnostics currently use a `Range` whose
`end.character` is `start.character + 1` (a 1-char caret), not the actual
offending span. A real highlight range would improve editor UX.
4. **`textDocument/willSave` / `willSaveWaitUntil` / `didSave`.** No save hooks —
"format-on-save" must currently be driven by the client binding
`textDocument/formatting` to the editor's save event. A `willSaveWaitUntil`
handler would let the server own format-on-save.
5. **No `completion`.** `textDocument/completion` is not implemented. Not urgent for
a formatter/linter, but relevant if PL/pgSQL autocompletion (keywords, types) is
ever in scope.
6. **No diagnostics debounce/coalescing beyond full-sync.** Every `didChange`
re-runs the full lint engine. Fine for now; a debounce + incremental re-check
becomes relevant on large files.
7. **`initializationOptions` / workspace config.** `initialize` params are parsed
nowhere — no way to pass style overrides or a config path over the protocol.
8. **No `textDocument/prepareRename`, `rename`, `references`, `foldingRange`.**
Low priority; would be natural extensions once symbol info exists.
## Conventions to keep consistent
- **One core, many frontends.** The LSP reuses `pkg/diagnostics.Diagnostic` and
`pkg/lint` directly — no parallel diagnostic model. New LSP features should reuse
these, not fork them.
- **Safety gate is non-negotiable.** Both `formatting` and `rangeFormatting` call
`format.SemanticallyEqual(src, out)` before returning edits; on failure they return
an empty edit (keep original). Any new code path that formats must honor this
invariant (invariant #5 in `AGENTS.md`).
- **No new external deps.** The wire layer is intentionally dependency-free. New
protocol types should be added as local structs, not pulled in from an LSP library.
## Concrete next steps (recommended, smallest-first)
Ranked by effort/value for the smallest useful delta:
1. **Add a real diagnostic highlight range** (swap the 1-char caret for the actual
offending span) — ~1 file, no new method, immediate UX win. Reuses existing
`RuleID`/severity data.
2. **Add `textDocument/hover`** returning the rule explanation for the hovered
diagnostic, or a keyword/type glossary. Reuses `pkg/lint` rule metadata.
3. **Add `documentSymbol`** listing `CREATE FUNCTION`/`PROCEDURE` signatures.
Reuses the existing CST header parse in `pkg/format`.
4. **Add `willSaveWaitUntil`** to own format-on-save instead of relying on client
binding.
> Do NOT: expand the LSP surface into a broad design (workspace features,
incremental parsing, custom `textDocument/*` extensions) as part of this issue.
Keep any change scoped to the above and evidence-backed by a `pkg/lsp` test
(see `server_test.go` for the framed-request/response harness).
## Verification
- `go build ./cmd/pgtidy` succeeds.
- `go test ./...` passes (LSP unit tests in `pkg/lsp/server_test.go` exercise
`initialize`, formatting, range formatting, `didClose` diagnostics clearing).
- Runtime e2e smoke test (framed JSON-RPC over stdio) confirmed `initialize`
capabilities, `COR001` diagnostics, and a formatting edit; `hover` correctly
returns `-32601`.
+3 -3
View File
@@ -128,7 +128,7 @@ DataGrip enum conventions used below:
| DataGrip key | PgTidy key | Default | Notes |
|---|---|---|---|
| `ROUTINE_ARG_COMMA` | uses `commas` | `leading` | same setting as query lists |
| `ROUTINE_ARG_ALIGN_TYPES` | `align_param_types` | `true` | align type column in param list |
| `ROUTINE_ARG_ALIGN_TYPES` | `align_param_types` | `false` | align type column in param list |
| `ROUTINE_AS_WRAP` | `routine_as_wrap` | `true` | newline before `AS $$` |
### PL/pgSQL body
@@ -136,8 +136,8 @@ DataGrip enum conventions used below:
| DataGrip key | PgTidy key | Default | Notes |
|---|---|---|---|
| `IMP_COMMON_KEEP_BLANK_LINES_IN_CODE` | `plpgsql_max_blank_lines` | `1` | max consecutive blank lines in body |
| `IMP_DECLARE_ALIGN_TYPE` | `plpgsql_declare_align_type` | `false` | align type column in DECLARE block |
| `IMP_DECLARE_ALIGN_EQ` | `plpgsql_declare_align_eq` | `false` | align `:=` / `=` in DECLARE block |
| `IMP_DECLARE_ALIGN_TYPE` | `plpgsql_declare_align_type` | `true` | align type column in DECLARE block |
| `IMP_DECLARE_ALIGN_EQ` | `plpgsql_declare_align_eq` | `true` | align `:=` / `=` in DECLARE block |
| `IMP_IF_THEN_WRAP_THEN` | `plpgsql_if_then_newline` | `true` | THEN on its own line |
| `IMP_LOOP_COLLAPSE` | `plpgsql_loop_collapse` | `true` | collapse empty loop bodies |
+13 -2
View File
@@ -93,6 +93,14 @@ Legend: ✅ done · 🚧 in progress · ⬜ not started
- `fmt_test.go`: stdin, --check (un/formatted), -w idempotence, unknown-command.
- Safety invariants #2 (semantic equivalence), #3 (idempotence), #4 (graceful degradation)
are tested in `pkg/format` over the corpus.
- **Runtime safety gate** (`format.VerifySafe`, `pkg/format/safety.go`): every frontend runs
it before emitting. Bundles `SemanticallyEqual` (code token stream) + `CommentsPreserved`
(no comment dropped/merged/split/reworded, recursing into bodies) + `StructurallyBalanced`
(`()[]` / BEGIN·CASE·IF·LOOP…END profile, ignoring comment & string contents) + an
idempotence re-format. On failure the CLI prints the reason and keeps the original.
Caught two real bugs: multi-line `/* */` bodies were reindented as code, and col-0 `--`
lines were glued onto the previous line (merging consecutive comments) — both fixed in
`formatBodyStatements`.
---
@@ -119,6 +127,7 @@ Legend: ✅ done · 🚧 in progress · ⬜ not started
- `cmd/pgtidy/lsp.go`: `pgtidy lsp` subcommand; config discovered from cwd.
- `editors/vscode/`: TS extension using `vscode-languageclient`; launches `pgtidy lsp` via stdio; `.pgsql` mapped to `sql` language; `pgtidy.path` / `pgtidy.enable` settings.
- _Range formatting: future._
- Full capability inventory, runtime-verified gaps, and ranked next steps in `docs/lsp-status.md` (issue #3).
## ✅ V4 — DataGrip
- `editors/datagrip/`: Gradle-based JetBrains plugin targeting DataGrip 2024.3+ via LSP4IJ.
@@ -212,7 +221,7 @@ as a proxy (new_line for content, inline for single-arg subexpressions).
### ✅ Formatter — routine param alignment (`pkg/format/format.go`)
- `align_param_types`: `alignParamTypes()` pads param names so type columns align; default `true`.
- `align_param_types`: `alignParamTypes()` pads param names so type columns align; default `false` (house style: no type-column alignment in param lists).
- `routine_as_wrap`: when `false`, AS stays on the same line as the last option clause.
- Golden file `testdata/corpus/test_a.pgsql` updated to reflect aligned params.
@@ -220,7 +229,9 @@ as a proxy (new_line for content, inline for single-arg subexpressions).
- `plpgsql_max_blank_lines`: blank-line runs capped at the configured limit; default `1`.
- `plpgsql_declare_align_type` + `plpgsql_declare_align_eq`: two-pass declare formatter
measures name/type widths then pads for alignment; `writeDeclareAligned` helper.
measures name/type widths then pads for alignment; `writeDeclareAligned` helper. Both
default `true` (house style). The `=` column is padded only to the widest type among
declarations that actually carry an assignment, so a lone `x text = '…';` stays tight.
- `plpgsql_if_then_newline`: when `false`, `joinThenToCondition` merges THEN onto the
preceding condition line.
- `plpgsql_loop_collapse`: `tryCollapseLoop` detects empty FOR/WHILE loop bodies and
+7 -17
View File
@@ -1,37 +1,27 @@
# PgTidy — DataGrip / JetBrains Plugin
Formats and lints SQL files in DataGrip (and any JetBrains IDE) via the LSP4IJ plugin.
Formats SQL files in DataGrip (and any JetBrains IDE) by shelling out to the `pgtidy` binary.
## Requirements
- `pgtidy` binary on `PATH` — download from [releases](https://git.warky.dev/wdevs/PgTidy/releases) or build with `go install git.warky.dev/wdevs/pgtidy/cmd/pgtidy@latest`
- [LSP4IJ](https://plugins.jetbrains.com/plugin/23257-lsp4ij) plugin installed (free, by Red Hat)
- DataGrip 2024.3+ (or any JetBrains IDE 2024.3+)
## Install the plugin
### Option A — Install from disk (`.zip`)
1. Build: `./gradlew buildPlugin` (output in `build/distributions/`)
2. In DataGrip: **Settings → Plugins → ⚙ → Install Plugin from Disk…** → select the `.zip`
3. Restart the IDE
### Option B — Install LSP4IJ and configure manually (no plugin build needed)
1. Install **LSP4IJ** from the marketplace (**Settings → Plugins → Marketplace → search "LSP4IJ"**)
2. Go to **Settings → Language Servers → + (Add)**
3. Fill in:
- **Name:** `PgTidy`
- **Command:** `pgtidy lsp`
4. Under **Mappings**, add file patterns: `*.sql`, `*.pgsql`
5. Click **OK** and restart the IDE
## Features
- **Formatting** — `Code → Reformat Code` (`Ctrl+Alt+L`) formats the current SQL file
- **Diagnostics** — lint findings shown as inspections (MIG001–MIG003, COR001–003, NAM001–003)
- **Formatting** — `Format with PgTidy` (`Ctrl+Alt+Shift+P` or right-click in the editor) formats the current SQL file via `pgtidy fmt`
- **Version check** — `Tools → Show PgTidy Version` and the status bar widget show the installed `pgtidy` version
- **Diagnostics** — lint findings (MIG001–MIG003, COR001–003, NAM001–003) are shown inline as editor annotations, via a native `ExternalAnnotator` that runs `pgtidy lint --json`
- **Quick fixes** — intention actions for MIG001 (add `CONCURRENTLY`) and MIG003 (add `NOT VALID`)
## Troubleshooting
**Server not starting:** Check **View → Tool Windows → LSP4IJ Consoles** for stderr output. Most common cause: `pgtidy` not found on `PATH` — update the command to the full path, e.g. `/usr/local/bin/pgtidy lsp`.
**"Cannot start pgtidy" error:** the `pgtidy` binary isn't on `PATH` for the IDE process. Install it or restart the IDE after adding it to `PATH`.
**No diagnostics showing:** diagnostics only run on files with a `.sql` or `.pgsql` extension; check `pgtidy lint --json` runs cleanly on the file from a terminal with the same `PATH` as the IDE.
-1
View File
@@ -23,7 +23,6 @@ dependencies {
val platformType = providers.gradleProperty("platformType")
create(platformType, platformVersion)
bundledPlugin("com.intellij.database")
plugin("com.redhat.devtools.lsp4ij:${providers.gradleProperty("lsp4ijVersion").get()}")
}
}
-1
View File
@@ -5,4 +5,3 @@ pluginSinceBuild=243
# DataGrip 2024.3
platformVersion=2024.3
platformType=DB
lsp4ijVersion=0.20.1
@@ -0,0 +1,18 @@
package com.pgtidy.datagrip
data class PgTidyFix(
val offset: Int,
val end: Int,
val new: String,
val title: String,
)
data class PgTidyDiagnostic(
val ruleId: String,
val severity: String,
val message: String,
val file: String,
val line: Int,
val col: Int,
val fix: PgTidyFix?,
)
@@ -0,0 +1,50 @@
package com.pgtidy.datagrip
import com.intellij.lang.annotation.AnnotationHolder
import com.intellij.lang.annotation.ExternalAnnotator
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.editor.Document
import com.intellij.psi.PsiFile
class PgTidyExternalAnnotator : ExternalAnnotator<String, List<PgTidyDiagnostic>>() {
override fun collectInformation(file: PsiFile): String? {
val ext = file.virtualFile?.extension?.lowercase()
if (ext != "sql" && ext != "pgsql") return null
return file.viewProvider.document?.text
}
override fun doAnnotate(source: String): List<PgTidyDiagnostic> = PgTidyLintRunner.run(source)
override fun apply(file: PsiFile, diagnostics: List<PgTidyDiagnostic>, holder: AnnotationHolder) {
val document = file.viewProvider.document ?: return
for (d in diagnostics) {
if (d.line <= 0 || d.line > document.lineCount) continue
val range = lineTailRange(document, d.line, d.col) ?: continue
val severity = when (d.severity) {
"error" -> HighlightSeverity.ERROR
"warning" -> HighlightSeverity.WARNING
else -> HighlightSeverity.WEAK_WARNING
}
val builder = holder.newAnnotation(severity, "[${d.ruleId}] ${d.message}").range(range)
if (d.fix != null) {
builder.withFix(PgTidyQuickFix(d.fix))
}
builder.create()
}
}
/** Byte column [col] (1-based) to end-of-line, converted to a document TextRange. */
private fun lineTailRange(document: Document, line1Based: Int, col1Based: Int): com.intellij.openapi.util.TextRange? {
val lineIdx = line1Based - 1
val lineStart = document.getLineStartOffset(lineIdx)
val lineEnd = document.getLineEndOffset(lineIdx)
val lineText = document.getText(com.intellij.openapi.util.TextRange(lineStart, lineEnd))
val charCol = PgTidyOffsets.byteOffsetToCharIndex(lineText, col1Based - 1)
val start = lineStart + charCol
if (start >= lineEnd) return null
return com.intellij.openapi.util.TextRange(start, lineEnd)
}
}
@@ -0,0 +1,32 @@
package com.pgtidy.datagrip
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
object PgTidyLintRunner {
private val gson = Gson()
private val listType = TypeToken.getParameterized(List::class.java, PgTidyDiagnostic::class.java).type
/** Runs `pgtidy lint --json` over [source]. Returns an empty list if pgtidy is missing or output is unparseable. */
fun run(source: String): List<PgTidyDiagnostic> {
val proc = try {
ProcessBuilder("pgtidy", "lint", "--json")
.redirectErrorStream(false)
.start()
} catch (ex: Exception) {
return emptyList()
}
proc.outputStream.bufferedWriter().use { it.write(source) }
val output = proc.inputStream.bufferedReader().readText()
proc.errorStream.bufferedReader().readText()
proc.waitFor()
if (output.isBlank()) return emptyList()
return try {
gson.fromJson<List<PgTidyDiagnostic>>(output, listType) ?: emptyList()
} catch (ex: Exception) {
emptyList()
}
}
}
@@ -0,0 +1,28 @@
package com.pgtidy.datagrip
/**
* pgtidy reports positions as UTF-8 byte offsets (Go strings are byte slices);
* IntelliJ documents index text as UTF-16 chars. Converts one to the other.
*/
object PgTidyOffsets {
fun byteOffsetToCharIndex(text: String, byteOffset: Int): Int {
var bytes = 0
var i = 0
while (i < text.length) {
val cp = text.codePointAt(i)
val charCount = Character.charCount(cp)
val byteLen = utf8Length(cp)
if (bytes + byteLen > byteOffset) return i
bytes += byteLen
i += charCount
}
return text.length
}
private fun utf8Length(codePoint: Int): Int = when {
codePoint <= 0x7F -> 1
codePoint <= 0x7FF -> 2
codePoint <= 0xFFFF -> 3
else -> 4
}
}
@@ -0,0 +1,25 @@
package com.pgtidy.datagrip
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
import com.intellij.util.IncorrectOperationException
class PgTidyQuickFix(private val fix: PgTidyFix) : IntentionAction {
override fun getText(): String = fix.title
override fun getFamilyName(): String = "PgTidy"
override fun startInWriteAction(): Boolean = true
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean = editor != null
@Throws(IncorrectOperationException::class)
override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
val document = editor?.document ?: return
val text = document.text
val start = PgTidyOffsets.byteOffsetToCharIndex(text, fix.offset)
val end = PgTidyOffsets.byteOffsetToCharIndex(text, fix.end)
document.replaceString(start, end, fix.new)
}
}
@@ -1,9 +0,0 @@
package com.pgtidy.datagrip
import com.intellij.openapi.project.Project
import com.redhat.devtools.lsp4ij.server.ProcessStreamConnectionProvider
class PgTidyServerConnection(project: Project) : ProcessStreamConnectionProvider(
listOf("pgtidy", "lsp"),
project.basePath ?: System.getProperty("user.home"),
)
@@ -1,10 +0,0 @@
package com.pgtidy.datagrip
import com.intellij.openapi.project.Project
import com.redhat.devtools.lsp4ij.LanguageServerFactory
import com.redhat.devtools.lsp4ij.server.StreamConnectionProvider
class PgTidyServerFactory : LanguageServerFactory {
override fun createConnectionProvider(project: Project): StreamConnectionProvider =
PgTidyServerConnection(project)
}
@@ -5,13 +5,11 @@
<vendor url="https://git.warky.dev/wdevs/PgTidy">Warky Devs</vendor>
<description><![CDATA[
PostgreSQL formatter and linter powered by <a href="https://github.com/hein/pgtidy">pgtidy</a>.<br/>
Requires the <code>pgtidy</code> binary on PATH and the
<a href="https://plugins.jetbrains.com/plugin/23257-lsp4ij">LSP4IJ</a> plugin.
Requires the <code>pgtidy</code> binary on PATH.
]]></description>
<depends>com.intellij.modules.platform</depends>
<depends>com.intellij.database</depends>
<depends>com.redhat.devtools.lsp4ij</depends>
<actions>
<action id="com.pgtidy.FormatDocument"
@@ -33,17 +31,7 @@
<statusBarWidgetFactory id="com.pgtidy.StatusBarWidget"
implementation="com.pgtidy.datagrip.PgTidyStatusBarWidgetFactory"
order="last"/>
<externalAnnotator language="" implementationClass="com.pgtidy.datagrip.PgTidyExternalAnnotator"/>
</extensions>
<extensions defaultExtensionNs="com.redhat.devtools.lsp4ij">
<server id="com.pgtidy.lsp"
name="PgTidy"
factoryClass="com.pgtidy.datagrip.PgTidyServerFactory">
<description>PostgreSQL formatter and linter (pgtidy lsp)</description>
</server>
<fileNamePatternMapping patterns="*.sql;*.pgsql"
serverId="com.pgtidy.lsp"
languageId="SQL"/>
</extensions>
</idea-plugin>
+1 -1
View File
@@ -1,6 +1,6 @@
# Maintainer: Hein (Warky Devs) <hein@warky.dev>
pkgname=pgtidy-bin
pkgver=0.0.4
pkgver=0.0.8
pkgrel=1
pkgdesc="PostgreSQL SQL formatter and linter"
arch=('x86_64' 'aarch64')
+1 -1
View File
@@ -1,5 +1,5 @@
Name: pgtidy
Version: 0.0.4
Version: 0.0.8
Release: 1%{?dist}
Summary: PostgreSQL SQL formatter and linter
+3 -3
View File
@@ -130,12 +130,12 @@ func Default() Style {
InsertCollapseValues: true,
AlignParamTypes: true,
AlignParamTypes: false,
RoutineAsWrap: true,
PlpgsqlMaxBlankLines: 1,
PlpgsqlDeclareAlignType: false,
PlpgsqlDeclareAlignEq: false,
PlpgsqlDeclareAlignType: true,
PlpgsqlDeclareAlignEq: true,
PlpgsqlIfThenNewline: true,
PlpgsqlLoopCollapse: true,
+11 -11
View File
@@ -15,27 +15,27 @@ const (
// TextFix is a byte-range replacement that can be applied to the source SQL.
// Replace src[Offset:End] with New. An insertion has Offset == End.
type TextFix struct {
Offset int // byte offset in source (inclusive)
End int // byte offset in source (exclusive)
New string // replacement text
Title string // short description shown in editor UI
Offset int `json:"offset"` // byte offset in source (inclusive)
End int `json:"end"` // byte offset in source (exclusive)
New string `json:"new"` // replacement text
Title string `json:"title"` // short description shown in editor UI
}
// Diagnostic is a single lint finding.
type Diagnostic struct {
// RuleID is the stable identifier for the rule that produced this finding
// (e.g. "MIG001").
RuleID string
RuleID string `json:"ruleId"`
// Severity is the urgency level.
Severity Severity
Severity Severity `json:"severity"`
// Message is a human-readable description of the finding.
Message string
Message string `json:"message"`
// File is the path to the source file, or "" for stdin.
File string
File string `json:"file"`
// Line is the 1-based line number of the finding.
Line int
Line int `json:"line"`
// Col is the 1-based column number of the finding.
Col int
Col int `json:"col"`
// Fix is non-nil when an autofix is available for this diagnostic.
Fix *TextFix
Fix *TextFix `json:"fix,omitempty"`
}
+211 -13
View File
@@ -177,7 +177,10 @@ func formatDeclareVars(b *strings.Builder, toks []cst.Tok, st config.Style) {
if nw > nameColW {
nameColW = nw
}
if tw > typeColW {
// typeColW drives the '='/':='/DEFAULT column (align_eq only), so
// only declarations that actually carry an assignment participate —
// a bare "name type;" must not widen it.
if tw > typeColW && declHasAssignment(body) {
typeColW = tw
}
}
@@ -207,7 +210,7 @@ func formatDeclareVars(b *strings.Builder, toks []cst.Tok, st config.Style) {
writeDeclareAligned(b, body, st, nameColW, typeColW)
} else {
for j, t := range body {
if j > 0 && needSpace(body[j-1].Tok, t.Tok) {
if j > 0 && needSpace(body[j-1].Tok, t.Tok) && !isPctTypeBoundary(body, j) {
b.WriteByte(' ')
}
b.WriteString(caseText(t.Tok, st))
@@ -220,6 +223,32 @@ func formatDeclareVars(b *strings.Builder, toks []cst.Tok, st config.Style) {
}
}
// declHasAssignment reports whether a DECLARE variable body (name type … ) has
// a default assignment ( := / = / DEFAULT ) at paren depth 0.
func declHasAssignment(body []cst.Tok) bool {
depth := 0
for _, t := range body {
switch t.Tok.Kind {
case lexer.LParen, lexer.LBracket:
depth++
case lexer.RParen, lexer.RBracket:
if depth > 0 {
depth--
}
}
if depth != 0 {
continue
}
if t.Tok.Kind == lexer.Operator && (t.Tok.Text == ":=" || t.Tok.Text == "=") {
return true
}
if t.Tok.Kind == lexer.Ident && lowerASCII(t.Tok.Text) == "default" {
return true
}
}
return false
}
// declareNameTypeWidth returns the rendered width of the name and type portions
// of a DECLARE variable declaration (without the default assignment).
// Format is: [name type [:= default]] or [name type [DEFAULT default]].
@@ -256,7 +285,7 @@ func declareNameTypeWidth(body []cst.Tok, st config.Style) (nameW, typeW int) {
}
var tb strings.Builder
for j, t := range typeTokens {
if j > 0 && needSpace(typeTokens[j-1].Tok, t.Tok) {
if j > 0 && needSpace(typeTokens[j-1].Tok, t.Tok) && !isPctTypeBoundary(typeTokens, j) {
tb.WriteByte(' ')
}
tb.WriteString(caseText(t.Tok, st))
@@ -315,7 +344,7 @@ func writeDeclareAligned(b *strings.Builder, body []cst.Tok, st config.Style, na
var typeStr strings.Builder
for j, t := range typeTokens {
if j > 0 && needSpace(typeTokens[j-1].Tok, t.Tok) {
if j > 0 && needSpace(typeTokens[j-1].Tok, t.Tok) && !isPctTypeBoundary(typeTokens, j) {
typeStr.WriteByte(' ')
}
typeStr.WriteString(caseText(t.Tok, st))
@@ -369,6 +398,22 @@ func formatBodyStatements(text string, st config.Style) string {
normalised := strings.ReplaceAll(text, "\r\n", "\n")
rawLines := strings.Split(normalised, "\n")
// Mark the continuation lines of every multi-line /* … */ block comment.
// Those lines are comment content, not code: they must be carried verbatim
// with the comment's opening line, never split off and reindented as if
// they were statements of their own.
inBlockComment := make([]bool, len(rawLines))
for _, t := range lexer.Lex(normalised) {
if t.Kind != lexer.BlockComment {
continue
}
n := strings.Count(t.Text, "\n")
start := t.Line - 1 // lexer Line is 1-based within normalised
for k := 1; k <= n && start+k < len(inBlockComment); k++ {
inBlockComment[start+k] = true
}
}
maxBlanks := st.PlpgsqlMaxBlankLines
if maxBlanks < 0 {
maxBlanks = 0
@@ -429,7 +474,12 @@ func formatBodyStatements(text string, st config.Style) string {
}
case "exception":
inException = true
effectiveDepth = 0
// EXCEPTION belongs to its nearest enclosing BEGIN, so align it one
// level in from the current block body (col 0 for the outermost).
effectiveDepth = blockDepth - 1
if effectiveDepth < 0 {
effectiveDepth = 0
}
}
baseIndent := strings.Repeat(st.Indent, effectiveDepth)
@@ -441,13 +491,9 @@ func formatBodyStatements(text string, st config.Style) string {
stmtLines = joinThenToCondition(stmt)
}
for i, ll := range stmtLines {
if i == 0 || ll.indent == "" {
result.WriteString(baseIndent)
} else {
result.WriteString(ll.indent)
}
result.WriteString(ll.text)
formattedLines := formatBodyStmtLines(stmtLines, baseIndent, st)
for _, line := range formattedLines {
result.WriteString(line)
result.WriteString(nl)
}
@@ -458,8 +504,21 @@ func formatBodyStatements(text string, st config.Style) string {
stmt = nil
}
for _, rawLine := range rawLines {
for j, rawLine := range rawLines {
line := strings.TrimRight(rawLine, "\r")
if inBlockComment[j] {
// Verbatim continuation of a multi-line block comment: glue it to the
// bline holding the comment's opening line.
if len(stmt) > 0 {
last := &stmt[len(stmt)-1]
last.text += "\n" + line
} else {
stmt = append(stmt, bline{text: line})
}
continue
}
indent := leadingWhitespace(line)
stripped := line[len(indent):]
@@ -472,6 +531,12 @@ func formatBodyStatements(text string, st config.Style) string {
fw := lowerASCII(firstBodyKeyword(stripped))
isColZero := indent == ""
joinToPrev := isColZero && parenDepth == 0 && len(stmt) > 0 && !sqlClauseKw[fw]
// A col-0 comment-only line is its own thing: never glue it onto the
// previous line — doing so buries a code line's trailing text in a
// comment and collapses consecutive -- comment lines into one.
if joinToPrev && len(significantBodyTokens(stripped)) == 0 {
joinToPrev = false
}
// Don't join a col-0 continuation to a comment-only preceding bline:
// the comment has no structural keyword so `continue ;` at col-0 would
// disappear into the comment text and be invisible to the lexer.
@@ -574,6 +639,139 @@ func formatBodyStatements(text string, st config.Style) string {
return result.String()
}
// formatBodyStmtLines formats one flushed PL/pgSQL statement at its contextual
// base indent. Multi-line UPDATE/DELETE statements inside PL/pgSQL get their
// top-level SET/WHERE/AND/OR clauses realigned under the statement while nested
// subqueries keep their original indentation. Non-DML statements keep
// continuation indentation, except that standalone structural keywords such as
// THEN are aligned with the block opener.
func formatBodyStmtLines(lines []bline, baseIndent string, st config.Style) []string {
if len(lines) == 0 {
return nil
}
if looksLikeMultiLineBodyDML(lines) {
return reindentBodyDML(lines, baseIndent, st)
}
out := make([]string, 0, len(lines))
for i, ll := range lines {
text := ll.text
indent := baseIndent
switch {
case i > 0 && ll.indent == "" && len(significantBodyTokens(text)) == 0:
// A column-0 comment line trailing a multi-line (commented-out)
// statement is a continuation the author left flush-left — keep it
// there rather than re-indenting it to block depth.
indent = ""
case i > 0 && ll.indent != "" && !isStandaloneBodyKeyword(ll.text, "then", "else", "elsif", "elseif"):
indent = ll.indent
}
out = append(out, indent+text)
}
return out
}
func looksLikeMultiLineBodyDML(lines []bline) bool {
if len(lines) < 2 {
return false
}
kw := lowerASCII(firstBodyKeyword(lines[0].text))
return kw == "update" || kw == "delete"
}
func reindentBodyDML(lines []bline, baseIndent string, st config.Style) []string {
out := make([]string, 0, len(lines)+1)
afterWhere := false
parenDepth := 0
for i, ll := range lines {
text := strings.TrimRight(ll.text, " ")
lineDepth := parenDepth
kw := lowerASCII(firstBodyKeyword(text))
if afterWhere && lineDepth == 0 && kw != "and" && kw != "or" {
out = append(out, baseIndent+st.Indent+st.Indent+strings.TrimSpace(text))
afterWhere = false
updateBodyParenDepth(text, &parenDepth)
continue
}
if lineDepth == 0 && (kw == "set" || kw == "where" || kw == "values" || kw == "returning") {
if kw == "where" {
whereText := strings.TrimSpace(text)
fields := strings.Fields(whereText)
nextKw := ""
if i+1 < len(lines) {
nextKw = lowerASCII(firstBodyKeyword(lines[i+1].text))
}
if len(fields) > 1 && (nextKw == "and" || nextKw == "or") {
out = append(out, baseIndent+fields[0])
out = append(out, baseIndent+st.Indent+st.Indent+strings.TrimSpace(whereText[len(fields[0]):]))
afterWhere = false
continue
}
afterWhere = len(fields) == 1
}
out = append(out, baseIndent+strings.TrimSpace(text))
continue
}
if lineDepth == 0 && (kw == "and" || kw == "or") {
// House style: AND/OR line up with the WHERE keyword; the first
// predicate is indented two levels under it.
out = append(out, baseIndent+strings.TrimSpace(text))
afterWhere = false
updateBodyParenDepth(text, &parenDepth)
continue
}
if i == 0 {
out = append(out, baseIndent+strings.TrimSpace(text))
} else if ll.indent != "" {
out = append(out, ll.indent+strings.TrimSpace(text))
} else {
out = append(out, baseIndent+strings.TrimSpace(text))
}
afterWhere = false
updateBodyParenDepth(text, &parenDepth)
}
return out
}
func updateBodyParenDepth(s string, depth *int) {
for _, tok := range lexer.Lex(s) {
switch tok.Kind {
case lexer.LParen, lexer.LBracket:
(*depth)++
case lexer.RParen, lexer.RBracket:
if *depth > 0 {
(*depth)--
}
}
}
}
func significantBodyTokens(s string) []cst.Tok {
var toks []cst.Tok
for _, tok := range lexer.Lex(s) {
if tok.IsTrivia() || tok.Kind == lexer.EOF {
continue
}
toks = append(toks, cst.Tok{Tok: tok})
}
return toks
}
func isStandaloneBodyKeyword(s string, kws ...string) bool {
toks := significantBodyTokens(s)
if len(toks) != 1 || toks[0].Tok.Kind != lexer.Ident {
return false
}
low := lowerASCII(toks[0].Tok.Text)
for _, kw := range kws {
if low == kw {
return true
}
}
return false
}
// joinThenToCondition merges a THEN line (on its own bline) into the preceding
// condition line when plpgsql_if_then_newline is false.
func joinThenToCondition(lines []bline) []bline {
+1 -1
View File
@@ -462,7 +462,7 @@ func dmlInline(toks []cst.Tok, st config.Style) string {
}
var b strings.Builder
for i, t := range toks {
if i > 0 && needSpace(toks[i-1].Tok, t.Tok) {
if i > 0 && needSpace(toks[i-1].Tok, t.Tok) && !isPctTypeBoundary(toks, i) {
b.WriteByte(' ')
}
// Space after comma in calls: func(a, b) vs func(a,b).
+31 -5
View File
@@ -147,8 +147,9 @@ func (p *printer) writeCreateFunction(cf *cst.CreateFunction) {
paramTexts = alignParamTypes(paramTexts)
}
first := p.st.Indent + " " // align item text one column past the comma
cont := p.st.Indent
// House style: first parameter indented one level; leading-comma
// continuation lines carry the comma at column 0 followed by one space.
first := p.st.Indent
for i, text := range paramTexts {
param := cf.Params[i]
p.nl()
@@ -159,8 +160,7 @@ func (p *printer) writeCreateFunction(cf *cst.CreateFunction) {
p.b.WriteString(",")
}
} else {
p.b.WriteString(cont)
p.b.WriteString(",")
p.b.WriteString(", ")
p.b.WriteString(text)
}
// Emit trailing inline comment from the separator (e.g. --description after param).
@@ -179,6 +179,7 @@ func (p *printer) writeCreateFunction(cf *cst.CreateFunction) {
for _, clause := range cf.Options {
p.nl()
p.b.WriteString(p.st.Indent)
p.b.WriteString(p.inline(clause))
}
if cf.As != nil {
@@ -218,7 +219,7 @@ func (p *printer) inline(toks []cst.Tok) string {
}
var b strings.Builder
for i, t := range toks {
if i > 0 && needSpace(toks[i-1].Tok, t.Tok) {
if i > 0 && needSpace(toks[i-1].Tok, t.Tok) && !isPctTypeBoundary(toks, i) {
b.WriteByte(' ')
}
var prev lexer.Token
@@ -254,6 +255,31 @@ func (p *printer) trailingComments(lead cst.Trivia) {
// tightOps are operators printed without surrounding spaces.
var tightOps = map[string]bool{"::": true, ":": true, "->": true, "->>": true}
// isPctTypeBoundary reports whether the gap between toks[i-1] and toks[i] sits
// inside a %TYPE / %ROWTYPE modifier (e.g. core.tbl%rowtype), which is printed
// tight like "::" rather than as the modulo operator.
func isPctTypeBoundary(toks []cst.Tok, i int) bool {
if i <= 0 || i >= len(toks) {
return false
}
isPct := func(t lexer.Token) bool { return t.Kind == lexer.Operator && t.Text == "%" }
isTypeWord := func(t lexer.Token) bool {
if t.Kind != lexer.Ident {
return false
}
l := lowerASCII(t.Text)
return l == "type" || l == "rowtype"
}
prev, cur := toks[i-1].Tok, toks[i].Tok
if isPct(prev) && isTypeWord(cur) {
return true // space after %
}
if isPct(cur) && i+1 < len(toks) && isTypeWord(toks[i+1].Tok) {
return true // space before %
}
return false
}
// parenKws are keywords that always take a space before '(' because they
// introduce a subquery or a bracketed clause, not a function-call argument list.
var parenKws = map[string]bool{
+73 -57
View File
@@ -7,7 +7,6 @@ import (
"testing"
"git.warky.dev/wdevs/pgtidy/pkg/config"
"git.warky.dev/wdevs/pgtidy/pkg/lexer"
"git.warky.dev/wdevs/pgtidy/pkg/parser"
)
@@ -24,13 +23,13 @@ func TestFormatHeaderGolden(t *testing.T) {
want := "--select * from dropall('resolvespec_login');\n" +
"CREATE OR REPLACE FUNCTION resolvespec_login(\n" +
" INOUT p_data jsonb\n" +
" ,OUT p_success boolean\n" +
" ,OUT p_error text\n" +
" INOUT p_data jsonb\n" +
", OUT p_success boolean\n" +
", OUT p_error text\n" +
")\n" +
"LANGUAGE plpgsql\n" +
"VOLATILE\n" +
"SECURITY DEFINER\n" +
" LANGUAGE plpgsql\n" +
" VOLATILE\n" +
" SECURITY DEFINER\n" +
"AS\n" +
"$$\nbegin end;\n$$;\n"
@@ -107,6 +106,66 @@ func TestFormatMmProcBroken(t *testing.T) {
}
}
func TestFormatIssue1PLpgSQLIndenting(t *testing.T) {
src := "CREATE FUNCTION f() RETURNS void LANGUAGE plpgsql AS $$\n" +
"DECLARE\n" +
" r_lp record;\n" +
"BEGIN\n" +
" if r_lp.total > 0\n" +
" and r_lp.totaldone >= r_lp.total\n" +
" then\n" +
" update core.process u\n" +
" set status = 'done'\n" +
" where u.rid_process = r_lp.rid_process\n" +
" and nv(u.status) <> 'done';\n" +
" elsif r_lp.total > 0\n" +
" then\n" +
" update core.process u\n" +
" set status = 'open'\n" +
" where u.rid_process = r_lp.rid_process\n" +
" and nv(u.status) <> 'open';\n" +
"\n" +
" end if;\n" +
"$$;\n"
want := "CREATE FUNCTION f(\n" +
")\n" +
" RETURNS void\n" +
" LANGUAGE plpgsql\n" +
"AS\n" +
"$$\n" +
"DECLARE\n" +
" r_lp record;\n" +
"BEGIN\n" +
" if r_lp.total > 0\n" +
" and r_lp.totaldone >= r_lp.total\n" +
" then\n" +
" update core.process u\n" +
" set status = 'done'\n" +
" where\n" +
" u.rid_process = r_lp.rid_process\n" +
" and nv(u.status) <> 'done';\n" +
" elsif r_lp.total > 0\n" +
" then\n" +
" update core.process u\n" +
" set status = 'open'\n" +
" where\n" +
" u.rid_process = r_lp.rid_process\n" +
" and nv(u.status) <> 'open';\n" +
"\n" +
" end if;\n" +
"$$;\n"
got := format(src)
if got != want {
t.Errorf("issue #1 PL/pgSQL indenting\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
checkDML(t, "issue #1 PL/pgSQL indenting", got)
if !semanticallyEqual(src, got) {
t.Errorf("issue #1 PL/pgSQL indenting changed semantics")
}
}
func TestCorpusIdempotentAndSafe(t *testing.T) {
dir := filepath.Join("..", "..", "testdata", "corpus")
entries, err := os.ReadDir(dir)
@@ -125,62 +184,19 @@ func TestCorpusIdempotentAndSafe(t *testing.T) {
}
src := string(data)
once := format(src)
twice := format(once)
if once != twice {
t.Errorf("%s: not idempotent", e.Name())
}
if !semanticallyEqual(src, once) {
t.Errorf("%s: formatting changed semantics", e.Name())
// VerifySafe bundles every runtime gate: semantic equivalence, comment
// preservation, structural balance, and idempotence.
if err := VerifySafe(src, once, config.Default()); err != nil {
t.Errorf("%s: %v", e.Name(), err)
}
}
if seen == 0 {
t.Skip("no corpus files")
}
t.Logf("formatted %d corpus files (idempotent + semantically equal)", seen)
t.Logf("verified %d corpus files (semantic + comments + structure + idempotence)", seen)
}
// semanticallyEqual compares the non-trivia token streams of two sources,
// treating unquoted identifiers/keywords case-insensitively and everything
// else (strings, numbers, operators, punctuation) exactly. Dollar-quoted body
// tokens are compared recursively so body whitespace normalization does not
// trigger a false failure.
// semanticallyEqual is a test-local alias for the exported safety check.
func semanticallyEqual(a, b string) bool {
ta := significant(a)
tb := significant(b)
if len(ta) != len(tb) {
return false
}
for i := range ta {
if ta[i].Kind != tb[i].Kind {
return false
}
switch ta[i].Kind {
case lexer.Ident:
if !strings.EqualFold(ta[i].Text, tb[i].Text) {
return false
}
case lexer.DollarString:
_, innerA, _, okA := splitDollarQuote(ta[i].Text)
_, innerB, _, okB := splitDollarQuote(tb[i].Text)
if okA != okB || (okA && !semanticallyEqual(innerA, innerB)) {
return false
}
default:
if ta[i].Text != tb[i].Text {
return false
}
}
}
return true
}
func significant(src string) []lexer.Token {
var out []lexer.Token
for _, t := range lexer.Lex(src) {
if t.Kind == lexer.EOF || t.IsTrivia() {
continue
}
out = append(out, t)
}
return out
return SemanticallyEqual(a, b)
}
+259
View File
@@ -0,0 +1,259 @@
package format
import (
"fmt"
"strings"
"git.warky.dev/wdevs/pgtidy/pkg/config"
"git.warky.dev/wdevs/pgtidy/pkg/lexer"
"git.warky.dev/wdevs/pgtidy/pkg/parser"
)
// VerifySafe runs every safety invariant against a formatting result before it
// is written to disk or returned to an editor. src is the original input, out
// the formatted output, and st the style out was produced with. It returns nil
// when out is safe to emit, otherwise an error naming the invariant that failed.
//
// The checks, in order of cost:
//
// 1. Semantic equivalence — the non-trivia (code) token stream is unchanged:
// identifiers/keywords compare case-insensitively, everything else exactly,
// recursing into dollar-quoted bodies. Comments and whitespace are trivia
// and are deliberately ignored here.
// 2. Comment preservation — every -- and /* */ comment in src reappears in out,
// in the same order, with the same content (ignoring only trailing
// whitespace and CRLF/LF). The formatter may move or re-indent a comment but
// must never drop, merge, split, or reword one.
// 3. Structural balance — the ( ) [ ] and BEGIN/CASE/IF/LOOP…END nesting
// profile of out matches src's, counting only real code tokens (anything
// inside a comment or a string/dollar-quoted literal is ignored).
// 4. Idempotence — formatting out again yields out unchanged.
//
// Any failure means the formatter has a bug: the caller must keep the original
// source and never emit out.
func VerifySafe(src, out string, st config.Style) error {
if !SemanticallyEqual(src, out) {
return fmt.Errorf("code token stream changed")
}
if err := CommentsPreserved(src, out); err != nil {
return err
}
if err := StructurallyBalanced(src, out); err != nil {
return err
}
if reformatted := File(parser.Parse(out), st); reformatted != out {
return fmt.Errorf("output is not idempotent (a second format pass would change it)")
}
return nil
}
// SemanticallyEqual reports whether a and b have the same non-trivia token
// stream, i.e. formatting may only ever change whitespace/comment trivia and
// layout — it must never add, remove, or alter a token of actual code.
// Unquoted identifiers/keywords compare case-insensitively (casing is a
// style choice); everything else (strings, numbers, operators, punctuation)
// must match exactly. Dollar-quoted body tokens are compared recursively so
// that independent body reformatting doesn't trigger a false failure.
//
// The CLI and LSP must call this (via VerifySafe) before ever writing or
// emitting formatted output: if it returns false, the formatter has a bug and
// the original source must be kept, never the (corrupting) formatted output.
func SemanticallyEqual(a, b string) bool {
ta := significantTokens(a)
tb := significantTokens(b)
if len(ta) != len(tb) {
return false
}
for i := range ta {
if ta[i].Kind != tb[i].Kind {
return false
}
switch ta[i].Kind {
case lexer.Ident:
if !strings.EqualFold(ta[i].Text, tb[i].Text) {
return false
}
case lexer.String, lexer.EscapeString, lexer.BitString, lexer.HexString, lexer.UnicodeString:
// A CRLF vs LF difference inside a multi-line string literal is a
// line-ending normalisation, not a change of code content — the
// formatter always re-emits layout with st.Newline. Compare the
// literal modulo \r\n ↔ \n.
if normNL(ta[i].Text) != normNL(tb[i].Text) {
return false
}
case lexer.DollarString:
_, innerA, _, okA := splitDollarQuote(ta[i].Text)
_, innerB, _, okB := splitDollarQuote(tb[i].Text)
if okA != okB || (okA && !SemanticallyEqual(innerA, innerB)) {
return false
}
default:
if ta[i].Text != tb[i].Text {
return false
}
}
}
return true
}
// CommentsPreserved reports whether every comment in a survives into b with its
// text intact. Comments are compared in document order; each is reduced to its
// sequence of non-blank text lines (line endings normalised, every line trimmed
// of surrounding whitespace, blank lines dropped) so that the formatter is free
// to move or re-indent a comment but can never drop, merge, split, reorder, or
// reword one. Comments inside dollar-quoted bodies are included (the bodies are
// lexed recursively). A non-nil error describes the first divergence.
func CommentsPreserved(a, b string) error {
ca := comments(a)
cb := comments(b)
if len(ca) != len(cb) {
return fmt.Errorf("comment count changed: input has %d, output has %d", len(ca), len(cb))
}
for i := range ca {
if ca[i] != cb[i] {
return fmt.Errorf("comment %d/%d changed:\n input: %q\n output: %q", i+1, len(ca), ca[i], cb[i])
}
}
return nil
}
// comments returns the normalised text of every -- and /* */ comment in src, in
// order, descending into dollar-quoted bodies.
func comments(src string) []string {
var out []string
for _, t := range lexer.Lex(src) {
switch t.Kind {
case lexer.LineComment, lexer.BlockComment:
out = append(out, normComment(t.Text))
case lexer.DollarString:
if _, inner, _, ok := splitDollarQuote(t.Text); ok {
out = append(out, comments(inner)...)
}
}
}
return out
}
// normComment canonicalises a comment token to its content — the ordered list of
// non-blank text lines, each stripped of surrounding whitespace, joined with LF.
// Line endings and indentation are layout, not content, so they are discarded;
// dropping or rewording an actual line of comment text still shows up.
func normComment(s string) string {
s = strings.ReplaceAll(s, "\r\n", "\n")
s = strings.ReplaceAll(s, "\r", "\n")
var lines []string
for _, ln := range strings.Split(s, "\n") {
if ln = strings.TrimSpace(ln); ln != "" {
lines = append(lines, ln)
}
}
return strings.Join(lines, "\n")
}
// StructurallyBalanced reports whether a and b have the same delimiter and block
// nesting profile: identical counts of ( ) [ ] and of the PL/pgSQL block
// keywords BEGIN / CASE / IF / LOOP / END (and the compound END IF / END LOOP /
// END CASE), plus an identical running paren/bracket depth trace. Only real code
// tokens are counted — anything inside a -- or /* */ comment is trivia and is
// skipped, and string / dollar-quoted literals are opaque single tokens whose
// contents never register (dollar-quoted bodies are recursed into separately).
//
// Given SemanticallyEqual, this is defence in depth: an independent re-count
// with different code that catches a structural token slipping through a bug in
// the token-stream comparison (e.g. its dollar-quote or CRLF handling), and it
// pins down *where* the structure broke.
func StructurallyBalanced(a, b string) error {
pa := structureProfile(a)
pb := structureProfile(b)
if pa.parenDepthTrace != pb.parenDepthTrace {
return fmt.Errorf("parenthesis/bracket nesting changed")
}
for _, k := range structureKeys {
if pa.counts[k] != pb.counts[k] {
return fmt.Errorf("structural token %q count changed: input %d, output %d", k, pa.counts[k], pb.counts[k])
}
}
return nil
}
var structureKeys = []string{"(", ")", "[", "]", "begin", "case", "if", "loop", "end", "end if", "end loop", "end case"}
type structProfile struct {
counts map[string]int
// parenDepthTrace is the sequence of running ( ) [ ] depths after each
// bracket token, joined with commas — a compact fingerprint of the nesting
// shape that diverges as soon as an open/close is added, dropped, or moved.
parenDepthTrace string
}
func structureProfile(src string) structProfile {
p := structProfile{counts: map[string]int{}}
var trace strings.Builder
depth := 0
toks := significantTokens(src) // trivia (comments/whitespace) already excluded
for i := 0; i < len(toks); i++ {
t := toks[i]
switch t.Kind {
case lexer.LParen:
p.counts["("]++
depth++
fmt.Fprintf(&trace, "%d,", depth)
case lexer.RParen:
p.counts[")"]++
depth--
fmt.Fprintf(&trace, "%d,", depth)
case lexer.LBracket:
p.counts["["]++
depth++
fmt.Fprintf(&trace, "%d,", depth)
case lexer.RBracket:
p.counts["]"]++
depth--
fmt.Fprintf(&trace, "%d,", depth)
case lexer.Ident:
switch lowerASCII(t.Text) {
case "begin", "case", "if", "loop":
p.counts[lowerASCII(t.Text)]++
case "end":
p.counts["end"]++
if i+1 < len(toks) && toks[i+1].Kind == lexer.Ident {
switch lowerASCII(toks[i+1].Text) {
case "if":
p.counts["end if"]++
case "loop":
p.counts["end loop"]++
case "case":
p.counts["end case"]++
}
}
}
case lexer.DollarString:
if _, inner, _, ok := splitDollarQuote(t.Text); ok {
sub := structureProfile(inner)
for _, k := range structureKeys {
p.counts[k] += sub.counts[k]
}
trace.WriteString("[" + sub.parenDepthTrace + "]")
}
}
}
p.parenDepthTrace = trace.String()
return p
}
// normNL collapses CRLF to LF so string literals compare independent of the
// source file's line-ending convention.
func normNL(s string) string { return strings.ReplaceAll(s, "\r\n", "\n") }
// significantTokens lexes src and returns its tokens excluding EOF and trivia
// (whitespace/comments).
func significantTokens(src string) []lexer.Token {
var out []lexer.Token
for _, t := range lexer.Lex(src) {
if t.Kind == lexer.EOF || t.IsTrivia() {
continue
}
out = append(out, t)
}
return out
}
+91
View File
@@ -0,0 +1,91 @@
package format
import (
"strings"
"testing"
"git.warky.dev/wdevs/pgtidy/pkg/config"
)
func TestCommentsPreserved(t *testing.T) {
cases := []struct {
name string
a, b string
wantErr bool
}{
{"identical", "select 1; -- note", "select 1;\n-- note", false},
{"reindented block comment", "/* a\n b */ select 1", " /* a\nb */\nselect 1", false},
{"crlf line comment", "-- note\r\nselect 1", "-- note\nselect 1", false},
{"dropped comment", "select 1; -- keep me\nselect 2;", "select 1;\nselect 2;", true},
{"merged comments", "-- one\n-- two\nselect 1", "-- one -- two\nselect 1", true},
{"reworded comment", "-- alpha\nselect 1", "-- beta\nselect 1", true},
{"comment inside body preserved", // -- inside a dollar-quoted body
"do $$ begin\n-- inner\nperform 1;\nend $$;",
"DO\n$$\nbegin\n -- inner\n perform 1;\nend\n$$;", false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
err := CommentsPreserved(c.a, c.b)
if (err != nil) != c.wantErr {
t.Fatalf("CommentsPreserved(%q, %q) err = %v, wantErr %v", c.a, c.b, err, c.wantErr)
}
})
}
}
func TestCommentsPreservedIgnoresCodeText(t *testing.T) {
// A ( ; keyword etc. inside a comment must not be read as code by the check.
a := "select 1; -- ( begin case end ) ;\nselect 2;"
b := "select 1;\nselect 2;\n-- ( begin case end ) ;"
if err := CommentsPreserved(a, b); err != nil {
t.Fatalf("comment content that looks like code tripped the check: %v", err)
}
}
func TestStructurallyBalanced(t *testing.T) {
if err := StructurallyBalanced("select f((a+b)*c) from t", "select f( ( a + b ) * c )\nfrom t"); err != nil {
t.Errorf("whitespace-only reformat flagged: %v", err)
}
// Delimiters that live inside a comment or a string must not count.
if err := StructurallyBalanced("select ')(' as x -- ((((\nfrom t", "select ')(' as x\n-- ((((\nfrom t"); err != nil {
t.Errorf("comment/string delimiters counted: %v", err)
}
if err := StructurallyBalanced("select (a) from t", "select (a from t"); err == nil {
t.Errorf("dropped ')' not detected")
}
}
func TestVerifySafeCatchesNonIdempotent(t *testing.T) {
src := "create function f() returns void language sql as $$ select 1 $$;"
out := format(src)
if err := VerifySafe(src, out, config.Default()); err != nil {
t.Fatalf("clean format rejected: %v", err)
}
// A hand-mangled "output" that differs from what the formatter would produce
// must be rejected (idempotence gate).
if err := VerifySafe(src, out+"\n\n\n", config.Default()); err == nil {
t.Errorf("non-idempotent output accepted")
}
}
func TestVerifySafeBlockCommentInBody(t *testing.T) {
// Regression: a multi-line /* */ comment inside a PL/pgSQL body was being
// re-split and reindented as if its lines were statements.
src := "CREATE FUNCTION f() RETURNS void LANGUAGE plpgsql AS $$\n" +
"BEGIN\n" +
" /*\n" +
" update t u\n" +
" set x = 1\n" +
" where u.id = 2\n" +
" and u.y = 3;\n" +
" */\n" +
" perform 1;\n" +
"END $$;\n"
out := format(src)
if err := VerifySafe(src, out, config.Default()); err != nil {
t.Fatalf("block comment in body mangled: %v", err)
}
if !strings.Contains(out, "where u.id = 2") {
t.Errorf("block comment interior lost a line:\n%s", out)
}
}
+7
View File
@@ -124,6 +124,10 @@ func (s *server) handle(raw []byte) bool {
s.reply(req.ID, []textEdit{})
return false
}
if err := format.VerifySafe(text, formatted, s.cfg); err != nil {
s.reply(req.ID, []textEdit{})
return false
}
s.reply(req.ID, []textEdit{fullReplace(text, formatted)})
case "textDocument/rangeFormatting":
var p rangeFormattingParams
@@ -224,6 +228,9 @@ func (s *server) rangeFormat(text string, r lspRange) []textEdit {
if formatted == text {
return nil
}
if err := format.VerifySafe(text, formatted, s.cfg); err != nil {
return nil
}
// Split both versions into lines, keeping the trailing newline attached to
// each element so that joining them reconstructs the original string.
+797
View File
@@ -0,0 +1,797 @@
--select * from dropall('action_init','core');
CREATE OR REPLACE FUNCTION core.action_init(
p_table text
, p_rid integer
, INOUT p_parms jsonb default json_build_object()
, OUT p_retval integer
, OUT p_errmsg text
)
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
AS
$$
DECLARE
--Error Handling--
m_funcname text = 'core.action_init';
m_errmsg text;
m_errcontext text;
m_errdetail text;
m_errhint text;
m_errstate text;
m_retval integer;
--Error Handling--
--r_workflow core.workflowitem%rowtype;
r_tasklist core.tasklist%rowtype;
r_taskitem core.taskitem%rowtype;
r_taskitem_prev core.taskitem%rowtype;
r_taskitem_parent core.taskitem%rowtype;
r_mastertaskitem core.mastertaskitem%rowtype;
r_mastertaskitemevent core.mastertaskitemevent%rowtype;
r_taskitemevent core.taskitemevent%rowtype;
r_taskitemevent_parent core.taskitemevent%rowtype;
m_rid_hub_user integer;
m_rid_hub_payload integer;
m_temp_rid integer;
m_temp_rid_list integer[];
j_results jsonb;
BEGIN
p_retval = 0;
p_errmsg = '';
m_rid_hub_user = _bv(core.f_get_user_hub_rid(), (
select hub.rid_hub
from core.hub
where hub.hubtype = 'program'
limit 1
));
m_rid_hub_payload = _try_integers(p_parms ->> 'rid_hub', p_parms ->> 'rid_hub_payload');
if nv(p_rid) = 0
then
raise 'Invalid or no p_rid';
end if;
---select * from meta.table_prefix t where t.tablename = 'mastertaskitemevent'
if p_table::citext in ('taskitemevent', 'tiv')
then
raise exception 'Action Init is not allowed on task item events. Please use the mastertaskitemevent or taskitem as entry point.';
elsif p_table::citext in ('mastertaskitemevent', 'mtev')
then
select *
from core.mastertaskitemevent mtev
where mtev.rid_mastertaskitemevent = p_rid
into r_mastertaskitemevent;
if r_mastertaskitemevent.inactive > 0
then
raise exception 'The template task item event is inactive. rid_mastertaskitemevent = %',r_mastertaskitemevent.rid_mastertaskitemevent;
end if;
select ttie.*
from core.taskitemevent ttie
where ttie.rid_taskitemevent = _try_integer(p_parms ->> 'parent_rid_taskitemevent', 0)
into r_taskitemevent_parent;
-- try to get the given task item
select *
from core.taskitem ti
where
ti.rid_taskitem = _try_integer(p_parms ->> 'rid_taskitem', -1)
and ti.rid_mastertaskitem = r_mastertaskitemevent.rid_mastertaskitem
into r_taskitem;
if _try_integer(p_parms ->> 'parent_rid_taskitem', 0) > 0
then
select *
from core.taskitem ti
where ti.rid_taskitem = _try_integer(p_parms ->> 'parent_rid_taskitem', 0)
into r_taskitem_parent;
end if;
select *
from core.tasklist tl
where tl.rid_tasklist = r_taskitem.rid_tasklist
into r_tasklist;
-- perform log_event(m_funcname,format(E'Action Init via %s=%s Item:%s(%s rid_taskitem:%s[%s] < %s %s[%s]) Event:%s(%s[%s],parent_rid_taskitemevent:%s[%s])
-- p_parms:%s',p_table,p_rid
-- , r_mastertaskitem.description, r_mastertaskitem.rid_mastertaskitem,r_taskitem.rid_taskitem
-- ,r_taskitem.status, r_taskitem_parent.description
-- ,r_taskitem_parent.rid_taskitem,r_taskitem_parent.status
-- , r_mastertaskitemevent.description,r_mastertaskitemevent.status, r_mastertaskitemevent.rid_mastertaskitemevent
-- , r_taskitemevent_parent.rid_taskitemevent, r_taskitemevent_parent.status
-- , p_parms),bt_enum('eventlog','local notice'));
-- raise 'p_rid=%=% r_taskitem.status =% r_taskitem_parent.status=%,% r_mastertaskitemevent.status=% pps=%'
-- ,p_table,p_rid, r_taskitem.status,r_taskitem_parent.description,r_taskitem_parent.status, r_mastertaskitemevent.status
-- ,(select ti.status
-- from core.taskitem ti
-- where ti.rid_taskitem = r_taskitem_parent.rid_taskitem_parentaction);
-- if r_mastertaskitemevent.status = core._enumi('eventstatus','complete')
-- and r_taskitem_parent.rid_taskitem > 0
-- and exists (
-- select 1
-- from core.taskitem ti
-- where ti.rid_taskitem = r_taskitem_parent.rid_taskitem_parentaction
-- and ti.status in (core._enumi('eventstatus','complete')
-- ,core._enumi('eventstatus','error')
-- ,core._enumi('eventstatus','retry'))
-- )
-- then
-- r_taskitem = null;
--
-- end if;
--If there is no action, try getting the same open one of this type for this hub
if nv(r_taskitem.rid_taskitem) = 0
then
--Create item linked to task for this hub if not found
select r.p_retval, r.p_errmsg, r.p_a_rid_taskitem, r.p_parameters
from core.taskitem_get_or_prime(_jsonb_object_cat(
_jsonb_object_cat(r_taskitem_parent.jsonvalue
, jsonb_build_object('AOP', null, 'controls', null, 'rid_taskitem', null, 'message', null, 'subject', null,
'alerttype', null)
)
, p_parms
, jsonb_build_object(
'rid_mastertaskitem', r_mastertaskitemevent.rid_mastertaskitem
, 'rid_mastertaskitemevent', r_mastertaskitemevent.rid_mastertaskitemevent
, 'parent_rid_taskitemevent', r_taskitemevent_parent.rid_taskitemevent
, 'parent_rid_taskitem', _bv(r_taskitem_parent.rid_taskitem
, _try_integer(p_parms ->> 'parent_rid_taskitem', 0)
)
, 'rid_tasklist_prev', _try_integers(p_parms ->> 'rid_tasklist_prev', p_parms ->> 'rid_tasklist'
, r_tasklist.rid_tasklist::text, r_taskitem.rid_tasklist::text)
, 'rid_taskitem_parentaction', p_parms -> 'rid_taskitem_parentaction'
--,'rid_tasklist', r_tasklist.rid_tasklist
)
)) r
into m_retval,m_errmsg,m_temp_rid_list,j_results;
if m_retval > 0
then
raise exception '%',m_errmsg using hint = 'in taskitem_get_or_prime 2';
end if;
else
m_temp_rid_list = array [r_taskitem.rid_taskitem]::integer[];
end if;
p_parms = jsonb_build_object();
for m_temp_rid in (
select u.n
from unnest(m_temp_rid_list) u(n)
)
loop
select *
from core.taskitem ti
where ti.rid_taskitem = m_temp_rid
into r_taskitem;
select *
from core.tasklist tl
where tl.rid_tasklist = r_taskitem.rid_tasklist
into r_tasklist;
if nv(r_tasklist.rid_tasklist) = 0
then
select *
from core.tasklist tl
where
tl.rid_tasklist = r_taskitem.rid_tasklist
or nv(r_taskitem.rid_tasklist) = 0
and
(tl.rid_hub = m_rid_hub_payload
or tl.rid_hub = r_taskitem_parent.rid_hub
)
and tl.rid_mastertask in (
select mti.rid_mastertask
from core.mastertaskitem mti
where
mti.rid_mastertaskitem = r_mastertaskitemevent.rid_mastertaskitem
and coalesce(mti.inactive, 0) = 0
)
order by tl.rid_tasklist desc
limit 1
into r_tasklist;
end if;
select tie.*
from core.taskitemevent tie
where
tie.rid_taskitem = r_taskitem.rid_taskitem
and tie.rid_mastertaskitemevent = r_mastertaskitemevent.rid_mastertaskitemevent
--and tie.status = r_mastertaskitemevent.status
order by tie.rid_taskitemevent desc
limit 1
into r_taskitemevent;
perform log_event(m_funcname, format(E'__Processing task item %s(%s, status=%s) for event %s(%s) status=%s'
, r_taskitem.description, r_taskitem.rid_taskitem, r_taskitem.status
, r_taskitemevent.description, r_mastertaskitemevent.rid_mastertaskitemevent, r_taskitemevent.status
), bt_enum('eventlog', 'local notice'));
if nv(r_taskitem.status) in (4, 5, 6, 11) --11 Resolved
and r_mastertaskitemevent.status is distinct from r_taskitem.status
and existS (
select 1
from core.taskitemevent e
where
e.rid_taskitem = r_taskitem.rid_taskitem
and e.status = r_taskitem.status
)
--Or errors to errors
-- or nv(r_taskitem.status) in (10, 11)
-- and nv(r_taskitemevent.status) in (1, 2, 11, 10)
-- and not exists (
-- select 1
-- from core.taskitem ti2
-- where ti2.rid_parenttaskitem = r_taskitem.rid_taskitem
-- and ti2.status in (1,2,10,11)
-- )
-- )
-- and nv(r_mastertaskitem.jsonvalue->'AOP'->>'errorcode') = ''
or p_parms ->> 'new_task' in ('1', 'true')
then
select r.p_retval, r.p_errmsg, r.p_a_rid_taskitem, r.p_parameters
from core.taskitem_get_or_prime(_jsonb_object_cat(
_jsonb_object_cat(r_taskitem.jsonvalue, jsonb_build_object('AOP', null, 'controls', null)),
p_parms
, jsonb_build_object(
'rid_mastertaskitem', r_mastertaskitemevent.rid_mastertaskitem
, 'rid_mastertaskitemevent', r_mastertaskitemevent.rid_mastertaskitemevent
, 'parent_rid_taskitemevent', r_taskitemevent.rid_taskitemevent
, 'rid_tasklist', r_tasklist.rid_tasklist
, 'parent_rid_taskitem'
, _bv(r_taskitem.rid_taskitem
, _try_integer(p_parms ->> 'parent_rid_taskitem', 0)
, r_taskitem_parent.rid_taskitem)
, 'new_item', false
, 'status', 1
, 'rid_tasklist_prev', _try_integers(p_parms ->> 'rid_tasklist_prev', p_parms ->> 'rid_tasklist',
r_taskitem.rid_tasklist::text)
, 'rid_taskitem_parentaction', p_parms -> 'rid_taskitem_parentaction'
)
)) r
into m_retval,m_errmsg,m_temp_rid_list,j_results;
if m_retval > 0
then
raise exception '%',m_errmsg using hint = 'in taskitem_get_or_prime 2';
end if;
--raise notice 'Re-evaluated task item % for event % m_temp_rid_list=%',r_taskitem.rid_taskitem,r_mastertaskitemevent.rid_mastertaskitemevent, m_temp_rid_list;
if array_length(m_temp_rid_list, 1) > 1
and r_taskitem.rid_taskitem <> any (m_temp_rid_list)
or (p_parms ->> 'new_task' in ('1', 'true')
and r_taskitem.status in (4, 11)
)
then
perform log_event(m_funcname, format(E'*.* Fired event %s(%s) for completed action : %s New Actions: %s'
, r_mastertaskitemevent.description, r_mastertaskitemevent.rid_mastertaskitemevent, r_taskitem.description
, m_temp_rid_list), bt_enum('eventlog', 'debug'));
r_taskitem_prev = r_taskitem;
select ti.rid_taskitem
from core.taskitem ti
where
ti.rid_taskitem = any (m_temp_rid_list)
and ti.rid_mastertaskitem = r_taskitem.rid_mastertaskitem
into r_taskitem;
select *
from core.mastertaskitem mti
where mti.rid_mastertaskitem = r_taskitem.rid_mastertaskitem
into r_mastertaskitem;
r_taskitemevent = null;
-- else
--
-- perform log_event(m_funcname,format(E'Cannot fire event (Already Done,Cancelled,Error) (%s,%s,%s), on %s(%s) status = %s'
-- ,r_mastertaskitemevent.description
-- ,r_mastertaskitemevent.rid_mastertaskitemevent
-- ,r_mastertaskitemevent.status
-- ,r_taskitem.rid_taskitem
-- ,r_taskitem.description
-- ,r_taskitem.status
-- ),bt_enum('eventlog','debug'));
--
-- continue;
end if;
end if;
---Allow firing events again.
if nv(r_taskitemevent.rid_taskitemevent) > 0
then
-- if nv(r_taskitemevent.retval) < 2
-- then
perform log_event(m_funcname, format(E'**Updating event %s(%s) for action : %s'
, r_mastertaskitemevent.description, r_mastertaskitemevent.rid_mastertaskitemevent, r_taskitem.description
), bt_enum('eventlog', 'debug'));
--SET session_replication_role = DEFAULT;
update core.taskitemevent u
set
retval = 0
, createddatetime = now()
, jsonvalue = _jsonb_object_cat(u.jsonvalue, p_parms -> '_event_jsonvalue',
jsonb_build_object('rid_mastertaskitem_complete', case
when
_try_integer(
r_taskitemevent_parent.jsonvalue ->>
'rid_mastertaskitem_complete',
0) <>
r_mastertaskitemevent.rid_mastertaskitem
then r_taskitemevent_parent.jsonvalue ->> 'rid_mastertaskitem_complete'
else null
end
))
where u.rid_taskitemevent = r_taskitemevent.rid_taskitemevent
returning u.*
into r_taskitemevent;
select r.p_retval, r.p_errmsg
from core.event_created(r_taskitemevent.rid_taskitemevent) r
into m_retval,m_errmsg;
if m_retval > 0
then
raise exception '%',m_errmsg;
end if;
update core.taskitemevent u
set
jsonvalue = _jsonb_object_cat(u.jsonvalue, jsonb_build_object('rid_taskitemevent_retries',
_jsonb_object_cat(u.jsonvalue ->
'rid_taskitemevent_retries',
jsonb_build_array(r_taskitemevent.rid_taskitemevent))))
where u.rid_taskitemevent = r_taskitemevent_parent.rid_taskitemevent;
-- end if;
else
--SET session_replication_role = DEFAULT;
insert
into core.taskitemevent( createddatetime
, description
, guid
, rid_mastertaskitemevent
, rid_taskitem
, duedatetime
, errmsg
, escalated
, outcome
, status
, jsonvalue)
select now()
, format('%s %s', evt.description, nv((
select count(1)
from core.taskitemevent ev2
where
ev2.rid_taskitem = r_taskitem.rid_taskitem
and ev2.rid_mastertaskitemevent = r_mastertaskitemevent.rid_mastertaskitemevent
)) + 1
)
, newid()
, r_mastertaskitemevent.rid_mastertaskitemevent
, r_taskitem.rid_taskitem
, (
select eri.p_timestamp
from core.interval_fetch('mastertaskitemevent', r_mastertaskitemevent.rid_mastertaskitemevent
, r_taskitem.rid_hub, 'duedatetime',
jsonb_build_object('rid_taskitem', r_taskitem.rid_taskitem)) eri
limit 1
)
, null
, _try_integer(p_parms ->> 'escalated', 0)
, evt.outcome
, evt.status
, _jsonb_object_cat(p_parms -> '_event_jsonvalue', case
when nv(r_taskitem.rid_taskitem) = 0
then jsonb_build_object('rid_hub', m_rid_hub_payload)
else jsonb_build_object()
end
, jsonb_build_object('parent_rid_taskitem',
_try_integer(p_parms ->> 'parent_rid_taskitem', r_taskitem.rid_taskitem)
, 'parent_rid_taskitemevent', r_taskitemevent_parent.rid_taskitemevent
, 'rid_mastertaskitem_complete', case
when _try_integer(
r_taskitemevent_parent.jsonvalue ->>
'rid_mastertaskitem_complete', 0) <>
evt.rid_mastertaskitem
then r_taskitemevent_parent.jsonvalue ->> 'rid_mastertaskitem_complete'
else null
end)
)
from core.mastertaskitemevent evt
where evt.rid_mastertaskitemevent = r_mastertaskitemevent.rid_mastertaskitemevent
returning taskitemevent.*
into r_taskitemevent;
if r_taskitemevent.rid_taskitemevent > 0
then
perform log_event(m_funcname, format(E'**Created event %s(%s) for action : %s'
, r_mastertaskitemevent.description, r_mastertaskitemevent.rid_mastertaskitemevent, r_taskitem.description
), bt_enum('eventlog', 'debug'));
update core.taskitemevent u
set
jsonvalue = _jsonb_object_cat(u.jsonvalue, jsonb_build_object('rid_taskitemevent_spawned',
_jsonb_object_cat(u.jsonvalue ->
'rid_taskitemevent_spawned',
jsonb_build_array(r_taskitemevent.rid_taskitemevent))))
where u.rid_taskitemevent = r_taskitemevent_parent.rid_taskitemevent;
end if;
end if;
/*
if nv(r_taskitemevent.rid_taskitemevent) > 0
then
-- if nv(r_taskitemevent.retval) < 2
-- then
perform log_event(m_funcname,format(E'Updating event %s(%s) for action : %s'
,r_mastertaskitemevent.description,r_mastertaskitemevent.rid_mastertaskitemevent, r_taskitem.description
),bt_enum('eventlog','debug'));
perform meta.add_transaction_event(txid_current(), 'core.action_init'
,replace(replace(replace(replace(replace($CC$
DO $ICC$
DECLARE
m_retval integer;
m_errmsg text;
r_taskitemevent core.taskitemevent%rowtype;
r_taskitemevent_parent core.taskitemevent%rowtype;
r_mastertaskitemevent core.mastertaskitemevent%rowtype;
BEGIN
select *
from core.taskitemevent tie
where tie.rid_taskitemevent = [rid_taskitemevent]
into r_taskitemevent;
select *
from core.taskitemevent tie
where tie.rid_taskitemevent = [rid_taskitemevent_parent]
into r_taskitemevent_parent;
select *
from core.mastertaskitemevent t
where t.rid_mastertaskitemevent = [rid_mastertaskitemevent]
into r_mastertaskitemevent;
update core.taskitemevent u
set
retval = 0
, createddatetime = now()
, jsonvalue = _jsonb_object_cat(u.jsonvalue,$JJA$[event_json_value]$JJA$::jsonb, jsonb_build_object('rid_mastertaskitem_complete', case
when
_try_integer(
r_taskitemevent_parent.jsonvalue ->>
'rid_mastertaskitem_complete',
0) <>
r_mastertaskitemevent.rid_mastertaskitem
then r_taskitemevent_parent.jsonvalue ->> 'rid_mastertaskitem_complete'
else null
end
))
where u.rid_taskitemevent = r_taskitemevent.rid_taskitemevent
returning u.*
into r_taskitemevent;
select r.p_retval,r.p_errmsg
from core.event_created(r_taskitemevent.rid_taskitemevent) r
into m_retval,m_errmsg;
if m_retval > 0
then
raise exception '%',m_errmsg;
end if;
update core.taskitemevent u
set
jsonvalue = _jsonb_object_cat(u.jsonvalue, jsonb_build_object('rid_taskitemevent_retries',
_jsonb_object_cat(u.jsonvalue -> 'rid_taskitemevent_retries',
jsonb_build_array(r_taskitemevent.rid_taskitemevent))))
where u.rid_taskitemevent = r_taskitemevent_parent.rid_taskitemevent;
END;
$ICC$;
$CC$,'[rid_taskitemevent_parent]', nv(r_taskitemevent_parent.rid_taskitemevent)::text)
,'[rid_taskitemevent]', nv(r_taskitemevent.rid_taskitemevent)::text)
,'[rid_mastertaskitemevent]', nv(r_mastertaskitemevent.rid_mastertaskitemevent)::text)
,'[rid_taskitem]', nv(r_taskitem.rid_taskitem)::text)
,'[event_json_value]' ,coalesce((p_parms->'_event_jsonvalue')::text,'{}'))
)
,30, format('action_init_%s',r_taskitemevent.rid_taskitemevent);
SET session_replication_role = DEFAULT;
*/
-------------------------------------------__Recursive Rules__---------------------------------------------------
-- --activate rule
-- if r_taskitemevent_parent.rid_taskitemevent > 0
-- and r_taskitemevent_parent.jsonvalue->>'rid_mastertaskitem_activate' is not null
-- and _try_integer(r_taskitemevent_parent.jsonvalue->>'rid_mastertaskitem_activate',-1) > 0
-- and not exists (
-- select 1
-- from core.taskitem ti
-- where ti.rid_mastertaskitem = _try_integer(r_taskitemevent_parent.jsonvalue->>'rid_mastertaskitem_activate',-1)
-- and ti.rid_hub = m_rid_hub_payload
-- )
-- then
-- if _try_integer(r_taskitemevent_parent.jsonvalue->>'rid_mastertaskitem_activate',-1) <> r_taskitem.rid_mastertaskitem
-- and _try_integer(r_taskitemevent_parent.jsonvalue->>'rid_mastertaskitem_activate',-1) <> nv(r_mastertaskitem.rid_parentmastertaskitem)
-- then
-- insert into core.taskitemevent(rid_taskitem,rid_mastertaskitemevent,description,status,createddatetime,guid,jsonvalue)
-- select r_taskitem.rid_taskitem
-- , mtev.rid_mastertaskitemevent
-- , mtev.description
-- , mtev.status
-- , now()
-- , newid()
-- , _jsonb_object_cat(mtev.jsonvalue,p_parms->'_event_jsonvalue', jsonb_build_object('rid_mastertaskitem_activate',r_taskitemevent_parent.jsonvalue->>'rid_mastertaskitem_activate', 'simulated', true
-- , 'parent_rid_taskitem',_try_integer(p_parms ->> 'parent_rid_taskitem',0)
-- ))
-- from core.mastertaskitemevent mtev
-- where mtev.rid_mastertaskitem = r_taskitem.rid_mastertaskitem
-- and coalesce(mtev.inactive,0) = 0
-- and mtev.status = 2
-- and not exists (
-- select 1
-- from core.taskitemevent tie
-- where tie.rid_taskitem = r_taskitem.rid_taskitem
-- and tie.rid_mastertaskitemevent = mtev.rid_mastertaskitemevent
-- )
-- ;
-- elseif _try_integer(r_taskitemevent_parent.jsonvalue->>'rid_mastertaskitem_activate',-1) = r_taskitem.rid_mastertaskitem
-- then
-- --We reached the target taskitem, so we can mark it activate
-- insert into core.taskitemevent(rid_taskitem,rid_mastertaskitemevent,description,status,createddatetime,guid,jsonvalue)
-- select r_taskitem.rid_taskitem
-- , mtev.rid_mastertaskitemevent
-- , mtev.description
-- , mtev.status
-- , now()
-- , newid()
-- , _jsonb_object_cat( mtev.jsonvalue,p_parms->'_event_jsonvalue',jsonb_build_object('parent_rid_taskitem',_try_integer(p_parms ->> 'parent_rid_taskitem',0)))
-- from core.mastertaskitemevent mtev
-- where mtev.rid_mastertaskitem = r_taskitem.rid_mastertaskitem
-- and coalesce(mtev.inactive,0) = 0
-- and mtev.status = 2
-- and not exists (
-- select 1
-- from core.taskitemevent tie
-- where tie.rid_taskitem = r_taskitem.rid_taskitem
-- and tie.rid_mastertaskitemevent = mtev.rid_mastertaskitemevent
-- )
-- ;
-- end if;
--
-- end if;
--
-- --complete rule
-- if r_taskitemevent_parent.rid_taskitemevent > 0
-- and r_taskitemevent_parent.jsonvalue->>'rid_mastertaskitem_complete' is not null
-- and _try_integer(r_taskitemevent_parent.jsonvalue->>'rid_mastertaskitem_complete',-1) > 0
-- and not exists (
-- select 1
-- from core.taskitem ti
-- where ti.rid_mastertaskitem = _try_integer(r_taskitemevent_parent.jsonvalue->>'rid_mastertaskitem_complete',-1)
-- and ti.rid_hub = m_rid_hub_payload
-- )
-- then
-- if _try_integer(r_taskitemevent_parent.jsonvalue->>'rid_mastertaskitem_complete',-1) <> r_taskitem.rid_mastertaskitem
-- and _try_integer(r_taskitemevent_parent.jsonvalue->>'rid_mastertaskitem_complete',-1) <> nv(r_mastertaskitem.rid_parentmastertaskitem)
-- then
-- insert into core.taskitemevent(rid_taskitem,rid_mastertaskitemevent,description,status,createddatetime,guid,jsonvalue)
-- select r_taskitem.rid_taskitem
-- , mtev.rid_mastertaskitemevent
-- , mtev.description
-- , mtev.status
-- , now()
-- , newid()
-- , _jsonb_object_cat(mtev.jsonvalue,p_parms->'_event_jsonvalue', jsonb_build_object('rid_mastertaskitem_complete',r_taskitemevent_parent.jsonvalue->>'rid_mastertaskitem_complete', 'simulated', true, 'parent_rid_taskitem',_try_integer(p_parms ->> 'parent_rid_taskitem',0)))
-- from core.mastertaskitemevent mtev
-- where mtev.rid_mastertaskitem = r_taskitem.rid_mastertaskitem
-- and coalesce(mtev.inactive,0) = 0
-- and mtev.status = 4
-- and not exists (
-- select 1
-- from core.taskitemevent tie
-- where tie.rid_taskitem = r_taskitem.rid_taskitem
-- and tie.rid_mastertaskitemevent = mtev.rid_mastertaskitemevent
-- )
-- ;
-- elseif _try_integer(r_taskitemevent_parent.jsonvalue->>'rid_mastertaskitem_complete',-1) = r_taskitem.rid_mastertaskitem
-- then
-- --We reached the target taskitem, so we can mark it activate
-- insert into core.taskitemevent(rid_taskitem,rid_mastertaskitemevent,description,status,createddatetime,guid,jsonvalue)
-- select r_taskitem.rid_taskitem
-- , mtev.rid_mastertaskitemevent
-- , mtev.description
-- , mtev.status
-- , now()
-- , newid()
-- , _jsonb_object_cat(mtev.jsonvalue,p_parms->'_event_jsonvalue',jsonb_build_object('parent_rid_taskitem',_try_integer(p_parms ->> 'parent_rid_taskitem',0)))
-- from core.mastertaskitemevent mtev
-- where mtev.rid_mastertaskitem = r_taskitem.rid_mastertaskitem
-- and coalesce(mtev.inactive,0) = 0
-- and mtev.status = 2
-- and not exists (
-- select 1
-- from core.taskitemevent tie
-- where tie.rid_taskitem = r_taskitem.rid_taskitem
-- and tie.rid_mastertaskitemevent = mtev.rid_mastertaskitemevent
-- )
-- ;
-- end if;
-- end if;
p_parms := _jsonb_object_cat(p_parms, to_jsonb(r_taskitemevent));
end loop;
-- perform log_event(m_funcname
-- ,format('Post event insert: rid_taskitemevent=%s rid_mastertaskitem_complete=%s rid_mastertaskitem=%s'
-- ,r_taskitemevent_parent.rid_taskitemevent
-- ,r_taskitemevent_parent.jsonvalue->>'rid_mastertaskitem_complete'
-- ,r_taskitem.rid_mastertaskitem
-- ),bt_enum('eventlog','local notice'));
elsif p_table::citext in ('taskitem', 'wfl')
then
select ti.*
from core.taskitem ti
where ti.rid_taskitem = p_rid
into r_taskitem;
select ti.*
from core.mastertaskitem ti
where ti.rid_mastertaskitem = r_taskitem.rid_mastertaskitem
into r_mastertaskitem;
select tl.*
from core.tasklist tl
where tl.rid_tasklist = r_taskitem.rid_tasklist
into r_tasklist;
--Create item linked to task for this hub if not found
perform log_event(m_funcname, format(E'Action Init via %s=%s Item:%s(%s rid_taskitem:%s)
p_parms:%s', p_table, p_rid
, r_mastertaskitem.description, r_mastertaskitem.rid_mastertaskitem, r_taskitem.rid_taskitem
, p_parms), bt_enum('eventlog', 'local notice'));
select r.p_retval, r.p_errmsg, r.p_a_rid_taskitem
from core.taskitem_get_or_prime(_jsonb_object_cat(
jsonb_build_object('rid_tasklist', r_tasklist.rid_tasklist
, 'rid_mastertaskitemevent',
r_mastertaskitemevent.rid_mastertaskitemevent
, 'rid_taskitem_parentaction', p_parms -> 'rid_taskitem_parentaction'
), to_jsonb(r_taskitem), p_parms)) r
into m_retval,m_errmsg,m_temp_rid_list;
if m_retval > 0
then
raise exception '%',m_errmsg using hint = 'in taskitem_get_or_prime 3';
end if;
if array_length(m_temp_rid_list, 1) > 1
then
select array_agg(to_jsonb(ti))
from core.taskitem ti
where ti.rid_taskitem = any (m_temp_rid_list)
into p_parms;
else
m_temp_rid = m_temp_rid_list[1];
select *
from core.taskitem ti
where ti.rid_taskitem = m_temp_rid
into r_taskitem;
p_parms = to_jsonb(r_taskitem);
end if;
elsif p_table::citext in ('mastertaskitem', 'mal')
then
select ti.*
from core.mastertaskitem ti
where ti.rid_mastertaskitem = p_rid
into r_mastertaskitem;
if r_mastertaskitem.inactive > 0
then
raise exception 'The template task item is inactive. rid_mastertaskitem = %',r_mastertaskitem.rid_mastertaskitem;
end if;
perform log_event(m_funcname, format(E'Action Init via %s=%s Item:%s(%s)
p_parms:%s', p_table, p_rid
, r_mastertaskitem.description, r_mastertaskitem.rid_mastertaskitem
, p_parms), bt_enum('eventlog', 'local notice'));
select r.p_retval, r.p_errmsg, r.p_a_rid_taskitem
from core.taskitem_get_or_prime(_jsonb_object_cat(to_jsonb(r_mastertaskitem)
, p_parms
, jsonb_build_object('rid_mastertaskitemevent', r_mastertaskitemevent.rid_mastertaskitemevent
, 'rid_taskitem_parentaction',
p_parms -> 'rid_taskitem_parentaction'
))
) r
into m_retval,m_errmsg,m_temp_rid_list;
if m_retval > 0
then
raise exception '%',m_errmsg using hint = 'in taskitem_get_or_prime 4';
end if;
if array_length(m_temp_rid_list, 1) > 1
then
select array_agg(to_jsonb(ti))
from core.taskitem ti
where ti.rid_taskitem = any (m_temp_rid_list)
into p_parms;
else
m_temp_rid = m_temp_rid_list[1];
select *
from core.taskitem ti
where ti.rid_taskitem = m_temp_rid
into r_taskitem;
p_parms = to_jsonb(r_taskitem);
end if;
end if;
if nv(r_taskitem.rid_mastertaskitem) = 0
then
raise warning 'No template for the task item. rid_mastertaskitem = %',r_taskitem.rid_mastertaskitem;
end if;
EXCEPTION
WHEN others THEN
GET STACKED DIAGNOSTICS
m_errmsg = MESSAGE_TEXT
,m_errcontext = PG_EXCEPTION_CONTEXT
,m_errdetail = PG_EXCEPTION_DETAIL
,m_errhint = PG_EXCEPTION_HINT
,m_errstate = RETURNED_SQLSTATE;
p_errmsg := format('%s table:%s rid:%s',
get_err_msg(m_funcname, m_errmsg, m_errcontext, m_errdetail, m_errhint, m_errstate), p_table,
p_rid);
p_retval = 1;
END;
$$;
+988
View File
@@ -0,0 +1,988 @@
--select * from dropall('event_exec_func','core');
CREATE OR REPLACE FUNCTION core.event_exec_func(
p_event_parameters jsonb
, p_item_parameters jsonb
, OUT p_retval integer
, OUT p_errorcode text
, OUT p_errmsg text
, OUT p_output jsonb
)
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
AS
$$
DECLARE
--Error Handling--
m_funcname text = 'core.event_exec_func';
m_errmsg text;
m_errcode text;
m_errcontext text;
m_errdetail text;
m_errhint text;
m_errstate text;
m_retval integer;
--Error Handling--
r_taskitem core.taskitem%rowtype;
r_taskitem_parent core.taskitem%rowtype;
r_mastertaskitem core.mastertaskitem%rowtype;
r_taskitemevent core.taskitemevent%rowtype;
m_rid integer;
j_output jsonb;
j_all_parms jsonb;
j_controls jsonb;
j_tmp jsonb;
m_exec_funcname citext;
m_exec_schema citext;
m_sql citext;
r_lp record;
r_hub core.hub%rowtype;
a_async_procs citext[];
a_broker_procs citext[];
a_noerror_procs citext[];
BEGIN
j_controls = jsonb_build_array();
p_retval = 0;
p_errmsg = '';
m_exec_schema = 'public';
a_async_procs = array ['']; --hsync_upload_clienttohyphen
a_broker_procs = array ['']; --hsync_upload_clienttohyphen
a_noerror_procs = array ['ui_clientscenario_commit'];
-- if p_event_parameters -> 'AFN_RID_ACTIONFUNCTION_CODE' is null
-- then
-- p_retval = 1;
-- p_errmsg = '[ECR0001] No AFN_RID_ACTIONFUNCTION_CODE inside the AOP pack.';
--
-- select m.v[1]
-- from regexp_matches(p_errmsg, '\[([^\]]+)\]','ig') m(v)
-- where length(m.v[1]) between 2 and 10
-- order by m.v[1]
-- into p_errorcode;
--
-- return;
-- end if;
select *
from core.taskitemevent tie
where tie.rid_taskitemevent = _try_integer(p_item_parameters ->> 'rid_taskitemevent', 0)
into r_taskitemevent;
select *
from core.taskitem ti
where ti.rid_taskitem = _try_integer(p_item_parameters ->> 'rid_taskitem', r_taskitemevent.rid_taskitem)
into r_taskitem;
select *
from core.mastertaskitem mti
where
mti.rid_mastertaskitem = _try_integer(p_item_parameters ->> 'rid_mastertaskitem', 0)
or r_taskitem.rid_mastertaskitem > 0 and mti.rid_mastertaskitem = r_taskitem.rid_mastertaskitem
order by mti.rid_mastertaskitem desc
limit 1
into r_mastertaskitem;
m_exec_funcname = r_mastertaskitem.functionname_code;
if m_exec_funcname ilike '%.%'
then
m_exec_schema = split_part(m_exec_funcname, '.', 1);
m_exec_funcname = split_part(m_exec_funcname, '.', 2);
end if;
if nv(m_exec_funcname) = ''
then
perform log_event(m_funcname, format(
E'No function to execute for MasterTaskItem %s (rid_mastertaskitem=%s) and TaskItem %s (rid_taskitem=%s)',
r_mastertaskitem.description, r_mastertaskitem.rid_mastertaskitem, r_taskitem.description,
r_taskitem.rid_taskitem), bt_enum('eventlog', 'local notice'));
return;
end if;
-- if m_exec_funcname ilike '%payment%plan%'
-- then
-- raise exception 'Payment Plan Test Error % [RTY0001]', m_exec_funcname;
-- end if;
j_all_parms = _jsonb_object_cat(r_taskitem.jsonvalue,
j_all_parms
, (
select jsonb_object_agg(k, p_event_parameters -> k)
from jsonb_object_keys(p_event_parameters) keys(k)
where jsonb_typeof(p_event_parameters -> keys.k) not in ('object', 'array', 'null')
), (
select jsonb_object_agg(k, p_item_parameters -> k)
from jsonb_object_keys(p_item_parameters) keys(k)
where jsonb_typeof(p_item_parameters -> keys.k) not in ('object', 'array', 'null')
)
);
if r_taskitem.rid_taskitem > 0 and r_taskitem.rid_taskitem_parentaction > 0
then
select ti.*
from core.taskitem ti
where ti.rid_taskitem = r_taskitem.rid_taskitem_parentaction
into r_taskitem_parent;
end if;
select af.jsonvalue -> 'controls'
from core.actionfunction af
where
(
af.functionname = m_exec_funcname
or af.functionname = r_mastertaskitem.functionname_code
)
and af.fntype = 'pgsql-function'::citext
into j_controls;
if jsonb_typeof(p_item_parameters -> 'AOP' -> 'jsonvalue' -> 'controls_code') = 'array'
then
j_controls = _jsonb_object_cat(j_controls, p_item_parameters -> 'AOP' -> 'jsonvalue' -> 'controls');
end if;
if jsonb_typeof(p_event_parameters -> 'jsonvalue' -> 'controls_code') = 'array'
then
j_controls = _jsonb_object_cat(j_controls, p_event_parameters -> 'jsonvalue' -> 'controls');
end if;
if jsonb_typeof(j_all_parms -> 'controls_code') = 'array'
then
j_controls = _jsonb_object_cat(j_controls, j_all_parms -> 'controls_code');
end if;
if jsonb_typeof(r_mastertaskitem.jsonvalue -> 'controls_code') = 'array'
then
j_controls = _jsonb_object_cat(j_controls, r_mastertaskitem.jsonvalue -> 'controls_code');
end if;
raise notice 'Controls: %', j_controls::text;
j_all_parms = _jsonb_object_cat(j_all_parms,
(
select jsonb_object_agg(ctr.j ->> 'name'
, case
when nv(j_all_parms ->> (ctr.j ->> 'name')) = ''
then ctr.j -> 'props' ->> 'value'
else j_all_parms ->> (ctr.j ->> 'name')
end
order by i
)
from jsonb_array_elements(j_controls) with ordinality ctr(j, i)
where nv(ctr.j -> 'props' ->> 'value') <> ''
)
);
if nv(j_all_parms ->> 'rid_hub_user') = ''
then
j_all_parms = _jsonb_object_cat(j_all_parms, jsonb_build_object(
'rid_hub_user', _bv(core.f_get_user_hub_rid(), (
select hub.rid_hub
from core.hub
where hub.hubtype = 'program'
limit 1
)),
'login', f_getuser()
));
end if;
if nv(j_all_parms ->> 'login') = ''
then
j_all_parms = _jsonb_object_cat(j_all_parms, jsonb_build_object(
'login', f_getuser()
));
end if;
if _try_integer(j_all_parms ->> 'rid_hub', 0) > 0
then
select *
from core.hub
where rid_hub = _try_integer(j_all_parms ->> 'rid_hub', 0)
into r_hub;
if r_hub.hubtype in ('client', 'lead')
then
j_all_parms = _jsonb_object_cat(j_all_parms, (
select jsonb_build_object(
'rid_adproclient', cli.rid_adproclient
, 'rid_client', cli.rid_adproclient
)
from t_adproclient cli
where cli.rid_hub = r_hub.rid_hub
limit 1
));
elsif r_hub.hubtype in ('trader')
then
j_all_parms = _jsonb_object_cat(j_all_parms, (
select jsonb_build_object(
'rid_trader', tdr.rid_trader
, 'rid_adprocreditor', tdr.rid_trader
)
from t_trader tdr
where tdr.rid_hub = r_hub.rid_hub
limit 1
));
end if;
end if;
j_all_parms = _jsonb_object_cat(j_all_parms, jsonb_build_object('AOP', null));
-- perform log_event(m_funcname, format('p_event_parameters=%s p_item_parameters=%s j_all_parms=%s', p_event_parameters::text,
-- p_item_parameters::text, j_all_parms::text), bt_enum('eventlog', 'local notice'));
if (
select 1
from core.actionfunction af
where
(
af.functionname = m_exec_funcname
or af.functionname = r_mastertaskitem.functionname_code
)
and af.fntype = 'pgsql-function'::citext
)
then
if m_exec_funcname = 'event_nextaction'
then
select r.p_retval, r.p_errmsg, r.p_data
from core.event_nextaction(j_all_parms) r
into m_retval,m_errmsg, j_output;
if m_retval > 0
then
p_retval = m_retval;
p_errmsg = m_errmsg;
select m.v[1]
from regexp_matches(p_errmsg, '\[([^\]]+)\]', 'ig') m(v)
where length(m.v[1]) between 2 and 10
order by m.v[1]
into p_errorcode;
return;
end if;
elsif m_exec_funcname in ('event_pre_error_check')
then
select r.p_retval, r.p_errmsg, r.p_errorcode, r.p_parms
from core.event_pre_error_check(_jsonb_object_cat(j_all_parms, jsonb_build_object(
'rid_taskitem', r_taskitem.rid_taskitem
, 'rid_tasklist', r_taskitem.rid_tasklist
, 'rid_mastertaskitem', r_mastertaskitem.rid_mastertaskitem
))) r
into p_retval,p_errmsg, p_errorcode, p_output;
return;
elsif m_exec_funcname = 'ui_maint_commitem'
then
select r.p_retval, r.p_errmsg, r.p_options
from core.ui_maint_commitem(_jsonb_object_cat(
r_taskitem.jsonvalue
, r_taskitemevent.jsonvalue
, jsonb_build_object(
'rid_taskitem', r_taskitem.rid_taskitem,
'rid_mastertaskitem', r_taskitem.rid_mastertaskitem,
'rid_hub_user', j_all_parms ->> 'rid_hub_user',
'rid_hub_payload', _try_integers(j_all_parms ->> 'rid_hub', j_all_parms ->> 'rid_hub_payload')
)
)) r
into m_retval,m_errmsg, j_output;
if m_retval > 0
then
p_retval = m_retval;
p_errmsg = m_errmsg;
select m.v[1]
from regexp_matches(p_errmsg, '\[([^\]]+)\]', 'ig') m(v)
where length(m.v[1]) between 2 and 10
order by m.v[1]
into p_errorcode;
return;
end if;
elsif m_exec_funcname = 'ui_modify_paymentplan'
then
select r.p_retval, r.p_errmsg, r.p_data
from ui_modify_paymentplan(j_all_parms) r
into m_retval,m_errmsg, j_output;
if m_retval > 0
then
p_retval = m_retval;
p_errmsg = m_errmsg;
select m.v[1]
from regexp_matches(p_errmsg, '\[([^\]]+)\]', 'ig') m(v)
where length(m.v[1]) between 2 and 10
order by m.v[1]
into p_errorcode;
return;
end if;
elsif m_exec_funcname in ('f_checkforreapprove_client')
then
select f_checkforreapprove_client(cli.rid_adproclient)
from t_adproclient cli
where cli.rid_hub = r_taskitem.rid_hub
into m_retval;
if m_retval > 0
then
p_retval = 1;
p_errmsg = 'Re-approval is required for this client and it needs to be uploaded to the PDA.';
p_errorcode = 'CLIAPRV1';
p_output = jsonb_build_object('message', p_errmsg, 'actionstatus', 'error',
'errorcode', p_errorcode
, 'alerttype', 'error'
, 'in_modal', true
);
return;
else
p_retval = 0;
p_errmsg = '';
p_errorcode = '';
p_output = jsonb_build_object('actionstatus', 'success', 'delete_after', true);
return;
end if;
elsif m_exec_funcname in ('c_checkforreapprove')
then
select c_checkforreapprove(cli.rid_adproclient)
from t_adproclient cli
where cli.rid_hub = r_taskitem.rid_hub
into m_retval;
if m_retval > 0
then
p_retval = 1;
p_errmsg = 'Re-approval is required for this client`s payment plan and it needs to be uploaded to the PDA.';
p_errorcode = 'CLIAPRV2';
p_output = jsonb_build_object('message', p_errmsg, 'actionstatus', 'error', 'errorcode'
, p_errorcode
, 'alerttype', 'error'
, 'in_modal', true);
return;
else
p_retval = 0;
p_errmsg = '';
p_errorcode = '';
p_output = jsonb_build_object('actionstatus', 'success', 'delete_after', true);
return;
end if;
elsif m_exec_funcname in ('action_init_hublinks')
then
select r.p_retval, r.p_errmsg
from core.action_init_hublinks(_jsonb_object_cat(p_item_parameters, jsonb_build_object(
'rid_taskitem', r_taskitem.rid_taskitem,
'rid_mastertaskitem', r_taskitem.rid_mastertaskitem,
'rid_hub_user', j_all_parms ->> 'rid_hub_user',
'event_status', r_taskitemevent.status,
'parent_rid_taskitemevent', r_taskitemevent.rid_taskitemevent,
'calling_function', m_funcname
))) r
into m_retval, m_errmsg;
if m_retval > 0
then
p_retval = m_retval;
p_errmsg = m_errmsg;
select m.v[1]
from regexp_matches(p_errmsg, '\[([^\]]+)\]', 'ig') m(v)
where length(m.v[1]) between 2 and 10
order by m.v[1]
into p_errorcode;
return;
end if;
elsif m_exec_funcname in ('ui_clientscenario_commit')
then
if coalesce(_try_integers(j_all_parms ->> 'rid_clientscenario', j_all_parms ->> 'p_rid_clientscenario'), 0) = 0
then
select s.rid_clientscenario
from clientscenario s
inner join t_adproclient c on c.rid_adproclient = s.rid_adproclient
and c.rid_hub = r_taskitem.rid_hub
where s.status in (1, 2)
order by s.status, s.rid_clientscenario desc
limit 1
into m_rid;
if nv(m_rid) = 0 and r_taskitem.rid_hub_link > 0
then
--Falback to link parent client when tasklink is for client.
select s.rid_clientscenario
from core.hub_link h
inner join t_adproclient c on c.rid_hub = h.rid_hub_parent
and h.parent_hubtype = 'client'
inner join clientscenario s on s.rid_adproclient = c.rid_adproclient
and s.status in (1, 2)
where h.rid_hub_link = r_taskitem.rid_hub_link
order by s.status, s.rid_clientscenario desc
limit 1
into m_rid;
end if;
j_all_parms = _jsonb_object_cat(j_all_parms, jsonb_build_object(
'rid_clientscenario', m_rid
));
end if;
select r.p_retval, r.p_errmsg
from public.ui_clientscenario_commit(_try_integers(j_all_parms ->> 'rid_clientscenario'
, j_all_parms ->> 'p_rid_clientscenario', null)
, null
, _jsonb_object_cat(p_item_parameters, jsonb_build_object(
'rid_taskitem', r_taskitem.rid_taskitem,
'rid_hub', r_taskitem.rid_hub,
'rid_mastertaskitem', r_taskitem.rid_mastertaskitem,
'rid_hub_user', j_all_parms ->> 'rid_hub_user',
'event_status', r_taskitemevent.status,
'parent_rid_taskitemevent', r_taskitemevent.rid_taskitemevent,
'calling_function', m_funcname
))) r
into m_retval, m_errmsg;
if p_event_parameters ->> 'no_error' in ('1', 'true') or p_item_parameters ->> 'no_error' in ('1', 'true')
then
select m.v[1]
from regexp_matches(p_errmsg, '\[([^\]]+)\]', 'ig') m(v)
where length(m.v[1]) between 2 and 10
order by m.v[1]
into p_errorcode;
return;
end if;
if m_retval > 0
then
p_retval = m_retval;
p_errmsg = m_errmsg;
select m.v[1]
from regexp_matches(p_errmsg, '\[([^\]]+)\]', 'ig') m(v)
where length(m.v[1]) between 2 and 10
order by m.v[1]
into p_errorcode;
return;
end if;
elsif exists (
select ns.nspname, pro.*
from pg_proc pro
inner join pg_namespace ns on ns.oid = pro.pronamespace
and ns.nspname not in ('postgres', 'pg_catalog')
and ns.nspname = m_exec_schema
where pro.proname = m_exec_funcname
)
and m_exec_funcname in
('clfrm_builderrorlist', 'upl_builduploaderrors', 'clfrm_propcalc_errlist', 'ui_clientscenarion_errorlist')
then
raise notice 'Running %',m_exec_funcname;
p_retval = 0;
p_errmsg = '';
m_errdetail = '';
SET session_replication_role = DEFAULT;
if not exists (
select cli.rid_adproclient
from t_adproclient cli
where cli.rid_hub = r_taskitem.rid_hub
)
then
p_retval = 1;
p_errmsg = 'No client found for the hub.';
return;
end if;
with
cli as (
select cli.rid_adproclient
from t_adproclient cli
where cli.rid_hub = r_taskitem.rid_hub
)
, src as (
select o.errordescription as errordescription
, o.errorcode as errorcode
, o.error_priority as priority
, o.errortype
from clfrm_builderrorlist((
select cli.rid_adproclient
from cli
limit 1
), 0, 0) o
where m_exec_funcname in ('clfrm_builderrorlist')
union
select o.errordescription as errordescription
, o.errorcode as errorcode
, o.error_priority as priority
, o.errortype
from clfrm_builderrorlist((
select cli.rid_adproclient
from cli
limit 1
), 0, 1) o
where m_exec_funcname in ('clfrm_builderrorlist')
union
select o.errordescription as errordescription, o.errorcode as errorcode, 1 as priority, 'error' as errortype
from clfrm_propcalc_errlist((
select cli.rid_adproclient
from cli
limit 1
)) o
where m_exec_funcname = 'm_exec_funcname'
union
select o.errdescription as errordescription
, o.errorcode as errorcode
, o.severity as priority
, o.errtype as errortype
from upl_builduploaderrors((
select cli.rid_adproclient
from cli
limit 1
)
, _bv(j_all_parms ->> 'p_approve_mode', j_all_parms ->> 'approve_mode', j_all_parms ->> 'mode', 'normal')) o
where
m_exec_funcname in ('upl_builduploaderrors')
union
select elm.j ->> 'errordescription' as errordescription
, elm.j ->> 'errorcode' as errorcode
, _try_integer(elm.j ->> 'priority', 1) as priority
, _bv(elm.j ->> 'errortype', 'error') as errortype
from ui_clientscenarion_errorlist((
select sc.rid_clientscenario
from cli
inner join clientscenario sc on sc.rid_adproclient = cli.rid_adproclient
and sc.status = 1
order by sc.rid_clientscenario desc
limit 1
)) o
cross join jsonb_array_elements(case
when jsonb_typeof(o.p_data) = 'object' then jsonb_build_array(o.p_data)
when jsonb_typeof(o.p_data) = 'array' then o.p_data
else jsonb_build_array()
end) elm(j)
where m_exec_funcname in ('ui_clientscenarion_errorlist')
order by priority
)
, composed as (
select
-- src.errorcode
-- , src.errordescription
-- , src.priority as errorpriority
-- , src.errortype
row_number() over (partition by src.errorcode, src.errordescription order by src.priority desc) as rn
, evt2.rid_mastertaskitemevent as rid_mastertaskitemevent
, mti.rid_mastertaskitem as rid_mastertaskitem
, ti.rid_taskitem as rid_taskitem
, core.f_get_user_hub_rid() as rid_hub_user
, jsonb_build_object(
'errordescription', src.errordescription
, 'errorcode', src.errorcode
, 'errortype', src.errortype
, 'errorpriority', src.priority
) as jsonvalue
from src
--Get the main linked event
left outer join core.mastertaskitemevent ev1 on ev1.rid_mastertaskitem = r_taskitem.rid_mastertaskitem
and nv(ev1.errorcode) <> ''
and (
ev1.errorcode = src.errorcode
or ev1.errorcode in (
select m.interface_code
from core.error_code_map(jsonb_build_object('filter_code', src.errorcode)) m
where nv(m.interface_code) <> ''
)
)
left outer join core.mastertaskitemeventreaction er1
on er1.rid_mastertaskitemevent = ev1.rid_mastertaskitemevent
-- Fallback to the global event list
left outer join core.mastertaskitemevent evt2 on (
evt2.rid_mastertaskitemevent =
er1.rid_mastertaskitemevent_target
or evt2.status in (2)
and nv(evt2.errorcode) <> ''
and (
evt2.errorcode = src.errorcode
or evt2.errorcode in (
select m.interface_code
from core.error_code_map(jsonb_build_object('filter_code', src.errorcode)) m
where nv(m.interface_code) <> ''
)
)
)
and nv(evt2.inactive) = 0
left outer join core.mastertaskitem mti on mti.rid_mastertaskitem = evt2.rid_mastertaskitem
left outer join core.taskitem ti on ti.rid_mastertaskitem = mti.rid_mastertaskitem
and ti.rid_hub = r_taskitem.rid_hub
and nv(ti.rid_hub_link) = nv(r_taskitem.rid_hub_link)
and ti.status in (1, 2, 3)
)
select jsonb_agg(_jsonb_object_cat(to_jsonb(mti), to_jsonb(ti), jsonb_strip_nulls(to_jsonb(c))))
filter ( where c.rn = 1 )
, count(1)
from composed c
left outer join core.mastertaskitem mti on mti.rid_mastertaskitem = c.rid_mastertaskitem
left outer join core.taskitem ti on ti.rid_taskitem = c.rid_taskitem
into j_tmp, m_retval;
if m_retval > 0
then
-- update core.taskitem u
-- set jsonvalue = jsonb_build_object('errorlist', j_tmp)
-- where u.rid_taskitem = r_taskitem.rid_taskitem;
p_output =
jsonb_build_object('actionstatus', 'error', 'errorcode', 'ERRLIST', 'errorlist', j_tmp, 'delete_caller',
true);
else
p_output =
jsonb_build_object('message', null, 'actionstatus', 'success', 'errorcode', null, 'delete_after', true,
'errorlist', null);
end if;
perform log_event(m_funcname, format(E'List Function (%s) Results Code=%s Error=%s.
MasterTaskItem:%s \n p_output:%s \nm_errdetail:%s'
, m_exec_funcname
, p_errorcode
, p_errmsg
, r_mastertaskitem.description
, p_output
, m_errdetail
)
, bt_enum('eventlog', 'local error'));
return;
--m_retval =0;
elsif exists (
select ns.nspname, pro.*
from pg_proc pro
inner join pg_namespace ns on ns.oid = pro.pronamespace
and ns.nspname not in ('postgres', 'pg_catalog')
and ns.nspname = m_exec_schema
where pro.proname = m_exec_funcname
)
then
with
args as (
select ns.nspname
, pro.proname
, t.typname
, i.i
, pro.proargnames[i - 1] as argname
, pro.proargmodes[i - 1] as mode
, CASE
WHEN pro.proargdefaults IS NOT NULL
AND i.i - 1 > (pro.pronargs - pro.pronargdefaults)
THEN split_part(
pg_get_expr(pro.proargdefaults, 0),
',',
i.i - 1 - (pro.pronargs - pro.pronargdefaults)
)::text
ELSE NULL::text
END as default_value
, row_number() over (order by i.i) as number
--,pro.*
from pg_proc pro
inner join pg_namespace ns on ns.oid = pro.pronamespace
and ns.nspname not in ('postgres', 'pg_catalog')
JOIN LATERAL generate_subscripts(pro.proallargtypes, 1) AS i ON true
JOIN pg_type t ON t.oid = pro.proallargtypes[i - 1]
where
pro.proname = m_exec_funcname
and ns.nspname = m_exec_schema
--pro.proname in ('hsync_upload_clienttohyphen')
AND (pro.proargmodes IS NULL OR pro.proargmodes[i - 1] in ('i', 'b'))
)
, argval as (
select args.*
--, _bv(j_all_parms ->> args.argname,args.default_value) as argvalue
, case
when nv(j_all_parms ->> args.argname) in ('', '0', 'null')
and args.argname ilike 'p_%' and
nv(j_all_parms ->> replace(args.argname, 'p_', '')) not in ('', '0', 'null')
then j_all_parms ->> replace(args.argname, 'p_', '')
when nv(j_all_parms ->> args.argname) <> '' then j_all_parms ->> args.argname
when args.argname ilike 'p_%' and nv(j_all_parms ->> replace(args.argname, 'p_', '')) <> ''
then j_all_parms ->> replace(args.argname, 'p_', '')
when args.argname ilike 'p_login%' and j_all_parms ->> 'login' is not null
then j_all_parms ->> 'login'
when args.argname ilike 'p_user%' and j_all_parms ->> 'login' is not null
then j_all_parms ->> 'login'
when nv(args.default_value) <> '' then args.default_value
else null::text
end as argvalue
from args
)
select (
select string_agg(format('%s::%s', case
when a.typname ilike '%int%' then a.argvalue
else quote_literal(a.argvalue)
end, a.typname), ', ') as args
from argval a
)
, (
select count(1)
from argval a
where a.argvalue is null
)
, (
select format('[ECR0002] Cannot execute function %s. The following arguments are missing: %s (Defaults: %s) ',
m_exec_funcname, string_agg(a.argname, ','), string_agg(a.default_value, ','))
from argval a
where a.argvalue is null
)
into m_sql,m_retval, m_errmsg, m_errcode;
raise notice 'Function % executed with arguments: %', m_exec_funcname, m_sql;
if m_retval > 0
then
p_retval = 1;
p_errmsg = m_errmsg;
select m.v[1]
from regexp_matches(p_errmsg, '\[([^\]]+)\]', 'ig') m(v)
where length(m.v[1]) between 2 and 10
order by m.v[1]
into p_errorcode;
perform log_event(m_funcname, format(
E'Error on Function (%s) Code=%s Error=%s. MasterTaskItem:%s \n SQL:%s \n j_all_parms:%s \n Controls: %s'
, m_exec_funcname
, p_errorcode
, p_errmsg
, r_mastertaskitem.description
, m_sql
, substr((j_all_parms || jsonb_build_object('AOP', null))::text, 1, 100000)
, j_controls::text
)
, bt_enum('eventlog', 'local error'));
return;
end if;
m_sql = format('select to_jsonb(r) as val from %s.%s(%s) r ;', m_exec_schema, m_exec_funcname, m_sql);
perform log_event(m_funcname, format('Executing Function (%s) with SQL: %s', m_exec_funcname, m_sql),
bt_enum('eventlog', 'local notice'));
if m_exec_funcname::citext = any (a_broker_procs)
then
select r.p_retval, r.p_errmsg
from agent_job_add(format('exec %s', m_sql)
, get_agent_freequeue(3)
, format('%s(taskitemevent=%s)', m_exec_funcname, r_taskitemevent.rid_taskitemevent)
, 15
) r
into m_retval,m_errmsg;
elsif m_exec_funcname::citext = any (a_async_procs)
then
select _bv(j.val -> 'val', j.val)
from autoexec_query(m_sql, jsonb_build_object()) j(val)
into j_output;
else
execute m_sql into j_output;
end if;
p_output = j_output;
if p_event_parameters ->> 'no_error' in ('1', 'true') or p_item_parameters ->> 'no_error' in ('1', 'true')
then
select m.v[1]
from regexp_matches(p_errmsg, '\[([^\]]+)\]', 'ig') m(v)
where length(m.v[1]) between 2 and 10
order by m.v[1]
into p_errorcode;
perform log_event(m_funcname, format('Error(!) on Function (%s) Code=%s Error=%s. MasterTaskItem:%s Response:%s'
, m_exec_funcname
, p_errorcode
, p_errmsg
, r_mastertaskitem.description
, substr(j_output::text, 1, 10000)
)
, bt_enum('eventlog', 'local error'));
return;
end if;
if jsonb_typeof(j_output) in ('number', 'boolean', 'string')
then
perform log_event(m_funcname,
format('Function (%s) returned scalar value: %s', m_exec_funcname, j_output::text),
bt_enum('eventlog', 'debug'));
if j_all_parms -> 'p_output' is not null
and (j_all_parms ->> 'p_output')::citext is distinct from j_output::citext
then
p_retval = 1;
p_errmsg =
'Output value does not match expected value. Expected: ' || j_all_parms ->> 'p_output' || ' Actual: ' ||
j_output::text;
return;
end if;
end if;
if _try_integer(j_output ->> 'p_retval', 0) > 0
and m_exec_funcname::citext <> all (a_noerror_procs)
then
p_retval = _try_integer(j_output ->> 'p_retval', 0);
p_errmsg = format('%s', j_output ->> 'p_errmsg');
p_errorcode = _bv(j_output ->> 'p_error_code', j_output ->> 'p_errorcode', '');
perform log_event(m_funcname, format('Error on Function (%s) Code=%s Error=%s. MasterTaskItem:%s Response:%s'
, m_exec_funcname
, p_errorcode
, p_errmsg
, r_mastertaskitem.description
, substr(j_output::text, 1, 10000)
)
, bt_enum('eventlog', 'local error'));
if nv(p_errorcode) = ''
then
select m.v[1]
from regexp_matches(p_errmsg, '\[([^\]]+)\]', 'ig') m(v)
where length(m.v[1]) between 2 and 10
order by m.v[1]
into p_errorcode;
if nv(p_errorcode) = ''
then
p_errorcode = 'ECR0003';
end if;
end if;
return;
end if;
else
perform log_event(m_funcname, format('Function (%s) not defined for this event (%s).',
p_event_parameters -> 'AFN_RID_ACTIONFUNCTION_CODE' ->> 'functionname'
, r_mastertaskitem.description)
, bt_enum('eventlog', 'local error'));
select m.v[1]
from regexp_matches(p_errmsg, '\[([^\]]+)\]', 'ig') m(v)
where length(m.v[1]) between 2 and 10
order by m.v[1]
into p_errorcode;
return;
end if;
else
raise notice 'Type not supported: %', p_event_parameters -> 'AFN_RID_ACTIONFUNCTION_CODE' ->> 'fntype';
end if;
--select * from core.actionfunction
if nv(p_errorcode) = '' and nv(p_errmsg) <> ''
then
select m.v[1]
from regexp_matches(p_errmsg, '\[([^\]]+)\]', 'ig') m(v)
where length(m.v[1]) between 2 and 10
order by m.v[1]
into p_errorcode;
if nv(p_errorcode) = ''
then
p_errorcode = 'PRCERR';
end if;
end if;
EXCEPTION
WHEN others THEN
GET STACKED DIAGNOSTICS
m_errmsg = MESSAGE_TEXT
,m_errcontext = PG_EXCEPTION_CONTEXT
,m_errdetail = PG_EXCEPTION_DETAIL
,m_errhint = PG_EXCEPTION_HINT
,m_errstate = RETURNED_SQLSTATE;
p_errmsg := get_err_msg(m_funcname, m_errmsg, m_errcontext, m_errdetail, m_errhint, m_errstate);
p_retval = 1;
if nv(p_errorcode) = '' and nv(p_errmsg) <> ''
then
select m.v[1]
from regexp_matches(p_errmsg, '\[([^\]]+)\]', 'ig') m(v)
where length(m.v[1]) between 2 and 10
order by m.v[1]
into p_errorcode;
if nv(p_errorcode) = ''
then
p_errorcode = 'PRCERR';
end if;
end if;
END;
$$;
--Tests
/*
select (regexp_matches('[ECR0001] No AFN inside the AOP pack.', '\[([^\]]+)\]'))[1]
, (regexp_matches('[21] No AFN inside the AOP pack. [45345]', '\[([^\]]+)\]'))[1]
, (regexp_matches('A Test', '\[([^\]]+)\]'))[1]
*/
/*
select *
from core.taskitemevent e
where e.rid_taskitem = 26344
limit
;*/
/*
select *
from core.event_exec_func((
select _jsonb_object_cat( to_jsonb(e)
,jsonb_build_object('AFN_RID_ACTIONFUNCTION_CODE',to_jsonb(af)))
from core.taskitemevent e
inner join core.taskitem ti on ti.rid_taskitem = e.rid_taskitem
inner join core.actionoption ao on ao.guid = ti.jsonvalue -> 'AOP' ->> 'guid'
inner join core.actionfunction af on af.rid_actionfunction = ao.rid_actionfunction_code
where e.rid_taskitem = 26344
),(select _jsonb_object_cat( to_jsonb(e)
,jsonb_build_object('AFN_RID_ACTIONFUNCTION_CODE',to_jsonb(af)))
from core.taskitem e
inner join core.mastertaskitem mti
on mti.rid_mastertaskitem = e.rid_mastertaskitem
inner join core.actionoption ao on ao.guid = e.jsonvalue -> 'AOP' ->> 'guid'
inner join core.actionfunction af on af.rid_actionfunction = ao.rid_actionfunction_code
where e.rid_taskitem = 26344
))
selecT * from v_eventlog
select * from core.taskitem ti
order by ti.rid_taskitem desc
*/
File diff suppressed because it is too large Load Diff
+613
View File
@@ -0,0 +1,613 @@
--select * from dropall('ui_action_event','core');
CREATE OR REPLACE FUNCTION core.ui_action_event(
p_payload INOUT jsonb
, OUT p_retval integer
, OUT p_errmsg text
)
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
AS
$$
DECLARE
--Error Handling--
m_funcname text = 'core.ui_action_event';
m_errmsg text;
m_errcontext text;
m_errdetail text;
m_errhint text;
m_errstate text;
m_retval integer;
--Error Handling--
m_rid_hub integer;
m_rid_hub_link integer;
m_rid_hub_user integer;
m_temp_rid integer;
m_final_rid_taskitem integer;
m_temp_rid_list integer[];
m_status integer;
m_outcome integer;
r_mastertaskitemevent core.mastertaskitemevent%rowtype;
r_taskitem core.taskitem%rowtype;
r_taskitemevent core.taskitemevent%rowtype;
r_mastertaskitem core.mastertaskitem%rowtype;
r_mastertask core.mastertask%rowtype;
r_lp record;
m_check_error boolean := false;
m_cancel boolean := false;
j_result jsonb;
j_obj jsonb;
m_tm timestamp ;
BEGIN
m_tm = clock_timestamp();
p_retval = 0;
p_errmsg = '';
j_result = jsonb_build_array();
m_rid_hub = _try_integer(p_payload ->> 'rid_hub', 0);
m_rid_hub_user = _try_integer(p_payload ->> 'rid_hub_user', core.f_get_user_hub_rid());
m_status = _try_integer(p_payload ->> 'status', 0);
m_outcome = _try_integer(p_payload ->> 'outcome', 0);
m_check_error = (p_payload ->> 'check_error')::citext in ('1', 'true');
m_cancel = (p_payload ->> 'cancel')::citext in ('1', 'true');
select *
from core.mastertaskitemevent mte
where mte.rid_mastertaskitemevent = _try_integer(p_payload ->> 'rid_mastertaskitemevent', 0)
into r_mastertaskitemevent;
select mti.*
from core.mastertaskitem mti
where
mti.rid_mastertaskitem = _try_integer(p_payload ->> 'rid_mastertaskitem', 0)
and _try_integer(p_payload ->> 'rid_mastertaskitem', 0) > 0
or mti.rid_mastertaskitem = r_mastertaskitemevent.rid_mastertaskitem
into r_mastertaskitem;
select mt.*
from core.mastertask mt
where
mt.rid_mastertask = _try_integer(p_payload ->> 'rid_mastertask', 0)
and _try_integer(p_payload ->> 'rid_mastertask', 0) > 0
or mt.rid_mastertask = r_mastertaskitem.rid_mastertask
into r_mastertask;
select *
from core.taskitem ti
where ti.rid_taskitem = _try_integer(p_payload ->> 'rid_taskitem', 0)
into r_taskitem;
if r_taskitem.rid_taskitem > 0
then
perform set_config('session.rid_taskitem', r_taskitem.rid_taskitem::text, false);
end if;
if nv(r_mastertaskitem.rid_mastertaskitem) = 0
then
if r_taskitem.rid_taskitem > 0
then
select *
from core.mastertaskitem mti
where mti.rid_mastertaskitem = r_taskitem.rid_mastertaskitem
into r_mastertaskitem;
elsif r_mastertask.rid_mastertask > 0
then
select *
from core.mastertaskitem mti
where
mti.rid_mastertask = r_mastertask.rid_mastertask
and coalesce(mti.inactive, 0) = 0
and coalesce(mti.rid_parentmastertaskitem, 0) = 0
order by mti.seqno nulls first, mti.rid_mastertaskitem
limit 1
into r_mastertaskitem;
end if;
end if;
if nv(r_mastertaskitemevent.rid_mastertaskitemevent) = 0
then
if r_mastertaskitem.rid_mastertaskitem > 0
then
select *
from core.mastertaskitemevent mte
where
mte.rid_mastertaskitem = r_mastertaskitem.rid_mastertaskitem
and coalesce(mte.inactive, 0) = 0
and mte.status in (1, 2)
order by mte.status desc, mte.seqno nulls first, mte.rid_mastertaskitemevent
limit 1
into r_mastertaskitemevent;
end if;
end if;
---Check error handler
if m_check_error
then
if nv(r_mastertaskitemevent.status) <> 13
then
select evt.*
from core.mastertaskitemevent evt
where
evt.rid_mastertaskitem = r_mastertaskitem.rid_mastertaskitem
and evt.status = 13
order by evt.seqno desc, evt.rid_mastertaskitemevent desc
into r_mastertaskitemevent;
end if;
--perform log_event(m_funcname,format('Init check error for rid_mastertaskitem:%s hub:%s ',r_mastertaskitemevent.rid_mastertaskitem,m_rid_hub),bt_enum('eventlog','local notice'));
if r_mastertaskitemevent.rid_mastertaskitemevent > 0
then
select r.p_retval, r.p_errmsg, r.p_parms
from core.action_init('mastertaskitemevent', r_mastertaskitemevent.rid_mastertaskitemevent
, _jsonb_object_cat(p_payload,
jsonb_build_object('check_error', true, 'rid_hub', m_rid_hub, 'createmode', 'existing'))
) r
into m_retval,m_errmsg, p_payload;
if m_retval > 0
then
p_retval = m_retval;
p_errmsg = m_errmsg;
return;
end if;
else
p_retval = 0;
p_errmsg = 'Error check enabled but no error event found for mastertaskitem.';
end if;
return;
end if;
---Check cancel handler
if m_cancel
then
if nv(r_mastertaskitemevent.status) <> 6
then
select evt.*
from core.mastertaskitemevent evt
where
evt.rid_mastertaskitem = r_mastertaskitem.rid_mastertaskitem
and evt.status = 6
order by evt.seqno desc, evt.rid_mastertaskitemevent desc
into r_mastertaskitemevent;
end if;
perform log_event(m_funcname,
format('Init cancel for rid_mastertaskitem:%s hub:%s ', r_mastertaskitemevent.rid_mastertaskitem,
m_rid_hub), bt_enum('eventlog', 'local notice'));
if r_mastertaskitemevent.rid_mastertaskitemevent > 0
then
select r.p_retval, r.p_errmsg, r.p_parms
from core.action_init('mastertaskitemevent', r_mastertaskitemevent.rid_mastertaskitemevent
, _jsonb_object_cat(p_payload,
jsonb_build_object('cancel', true, 'rid_hub', m_rid_hub, 'createmode', 'existing'))
) r
into m_retval,m_errmsg, p_payload;
if m_retval > 0
then
p_retval = m_retval;
p_errmsg = m_errmsg;
return;
end if;
else
p_retval = 0;
p_errmsg = 'cancel called but no cancel event found for mastertaskitem.';
end if;
return;
end if;
if nv(m_rid_hub) = 0 and r_taskitem.rid_taskitem > 0 and r_taskitem.rid_hub > 0
then
m_rid_hub = r_taskitem.rid_hub;
end if;
--perform log_event(m_funcname,format('Init %s(%s) m_rid_hub: %s p_payload: %s',r_taskitem.description,r_taskitem.rid_taskitem,m_rid_hub,p_payload::text),bt_enum('eventlog','local notice'));
if nv(m_rid_hub) = 0
then
p_errmsg := 'Missing required parameter: rid_hub';
p_retval = 1;
return;
end if;
if nv(r_mastertaskitemevent.rid_mastertaskitemevent) = 0
then
p_errmsg := 'Missing required parameter: rid_mastertaskitemevent';
p_retval = 1;
return;
end if;
perform log_event(m_funcname,
format(E'Init rid_mastertaskitemevent %s for rid_mastertaskitem:%s hub:%s \np_payload:%s',
r_mastertaskitemevent.rid_mastertaskitemevent, r_mastertaskitem.rid_mastertaskitem,
m_rid_hub, p_payload), bt_enum('eventlog', 'local notice'));
select r.p_retval, r.p_errmsg, r.p_a_rid_taskitem
from core.taskitem_get_or_prime(_jsonb_object_cat(p_payload, jsonb_build_object(
'parent_rid_taskitem', case
when r_taskitem.rid_taskitem = _try_integer(p_payload ->> 'parent_rid_taskitem', 0)
then r_taskitem.rid_taskitem_parentaction
else _try_integer(p_payload ->> 'parent_rid_taskitem',
r_taskitem.rid_taskitem_parentaction)
end
, 'rid_mastertaskitem', r_mastertaskitem.rid_mastertaskitem
, 'rid_mastertaskitemevent', r_mastertaskitemevent.rid_mastertaskitemevent
, 'status', r_mastertaskitemevent.status
, 'create_mode', p_payload ->> 'create_mode'
--,'create_mode','new'
))) r
into m_retval,m_errmsg,m_temp_rid_list;
if m_retval > 0
then
raise 'Error initializing taskitem %',m_errmsg;
end if;
for m_temp_rid in select * from unnest(m_temp_rid_list)
loop
select *
from core.taskitem ti
where ti.rid_taskitem = m_temp_rid
into r_taskitem;
if nv(r_taskitem.rid_taskitem) = 0
then
p_retval = 1;
p_errmsg = format('Task item not created or found. %s', m_temp_rid);
return;
end if;
select tie.*
from core.taskitemevent tie
where
tie.rid_mastertaskitemevent = r_mastertaskitemevent.rid_mastertaskitemevent
and tie.rid_taskitem = r_taskitem.rid_taskitem
into r_taskitemevent;
--raise exception 'Show me the error: % %',r_taskitemevent.rid_taskitemevent, r_taskitemevent.rid_taskitem;
--raise notice 'New/Update r_taskitemevent? %',r_taskitemevent;
-- if r_taskitem.rid_taskitem > 0
-- and p_payload->'jsonvalue'->>'errorcode' is not null
-- then
-- update core.taskitem u
-- set jsonvalue = _jsonb_object_cat(u.jsonvalue, jsonb_build_object(
-- 'errorcode', _bv(p_payload->'jsonvalue'->>'errorcode',u.jsonvalue->>'errorcode')
-- ,'errormessage',_bv( p_payload->'jsonvalue'->>'errormessage',u.jsonvalue->>'errormessage')
-- ,'alerttype', _bv(p_payload->'jsonvalue'->>'alerttype',u.jsonvalue->>'alerttype')
-- ))
-- where u.rid_taskitem = r_taskitem.rid_taskitem;
--
-- end if;
if r_taskitemevent.rid_taskitemevent > 0
then
update core.taskitemevent u
set
retval = 0
, errmsg = null
, jsonvalue = _jsonb_object_cat(u.jsonvalue, (
select mte.jsonvalue
from core.mastertaskitemevent mte
where mte.rid_mastertaskitemevent = u.rid_mastertaskitemevent
limit 1
), p_payload -> 'jsonvalue')
where u.rid_taskitemevent = r_taskitemevent.rid_taskitemevent;
select r.p_retval, r.p_errmsg
from core.event_created(r_taskitemevent.rid_taskitemevent
, jsonb_build_object('operation', 'UPDATE')) r
into m_retval,m_errmsg;
if m_retval > 0
then
raise '%',m_errmsg;
end if;
select e.*
from core.taskitemevent e
where e.rid_taskitemevent = r_taskitemevent.rid_taskitemevent
into r_taskitemevent;
if length(r_taskitemevent.errmsg) > 0
then
p_retval = 1;
p_errmsg = r_taskitemevent.errmsg;
end if;
else
insert
into core.taskitemevent( createddatetime, description, rid_mastertaskitemevent, rid_taskitem, status
--, duedatetime
--, escalated
, jsonvalue, outcome, rid_hub_user, retval)
select now()
, r_mastertaskitemevent.description
, r_mastertaskitemevent.rid_mastertaskitemevent
, r_taskitem.rid_taskitem
, _bv(r_mastertaskitemevent.status, m_status)
, _jsonb_object_cat((
select mte.jsonvalue
from core.mastertaskitemevent mte
where mte.rid_mastertaskitemevent = r_mastertaskitemevent.rid_mastertaskitemevent
limit 1
), p_payload -> 'jsonvalue')
, m_outcome
, m_rid_hub_user
, 0
where
not exists (
select 1
from core.taskitemevent tie
where
tie.rid_mastertaskitemevent = r_mastertaskitemevent.rid_mastertaskitemevent
and tie.rid_taskitem = r_taskitem.rid_taskitem
)
returning taskitemevent.*
into r_taskitemevent;
-- select r.p_retval,r.p_errmsg
-- from core.event_created(r_taskitemevent.rid_taskitemevent
-- ,jsonb_build_object('operation','UPDATE')) r
-- into m_retval,m_errmsg;
--
-- if m_retval > 0
-- then
-- raise '%',m_errmsg;
-- end if;
select e.*
from core.taskitemevent e
where e.rid_taskitemevent = r_taskitemevent.rid_taskitemevent
into r_taskitemevent;
if length(r_taskitemevent.errmsg) > 1
then
p_retval = 1;
p_errmsg = r_taskitemevent.errmsg;
end if;
end if;
with
recursive
tasklist_events as (
select ti.rid_taskitem
, 1 as level
, coalesce(ti.seqno, 0) as seqno
, ti.status
from core.taskitem ti
inner join core.mastertaskitem mti on mti.rid_mastertaskitem = ti.rid_mastertaskitem
where
(ti.rid_tasklist = r_taskitem.rid_tasklist
or ti.rid_tasklist in (
select tl.rid_tasklist_parent
from core.tasklistlink tl
where tl.rid_tasklist_child = r_taskitem.rid_tasklist
union
select tl.rid_tasklist_child
from core.tasklistlink tl
where tl.rid_tasklist_parent = r_taskitem.rid_tasklist
)
)
-- and mti.rid_mastertaskitem in (
-- select ev2.rid_mastertaskitem
-- from core.mastertaskitemeventreaction er
-- inner join core.mastertaskitemevent ev2 on ev2.rid_mastertaskitemevent = er.rid_mastertaskitemevent_target
-- where er.rid_mastertaskitemevent = r_taskitemevent.rid_mastertaskitemevent
-- )
union
select ti.rid_taskitem
, te.level + 1
, coalesce(ti.seqno, 0) as seqno
, ti.status
from core.taskitem ti
inner join tasklist_events te on te.rid_taskitem = ti.rid_parenttaskitem
)
select e.rid_taskitem
from tasklist_events e
where nv(e.status) in (2, 10)
order by e.level desc, e.status desc, e.seqno desc
limit 1
into m_final_rid_taskitem;
j_obj = _jsonb_object_cat(to_jsonb(r_taskitemevent), jsonb_build_object(
'jsonvalue', _jsonb_object_cat(r_taskitemevent.jsonvalue, jsonb_build_object(
'rid_tasklist', r_taskitem.rid_tasklist,
'rid_tasklist_event', (
select jsonb_agg(distinct ti.rid_tasklist)
from core.mastertaskitemeventreaction er
inner join core.mastertaskitemevent mtiet
on mtiet.rid_mastertaskitemevent = er.rid_mastertaskitemevent_target
inner join core.taskitem ti on ti.rid_mastertaskitem = mtiet.rid_mastertaskitem
and ti.rid_hub = r_taskitem.rid_hub
and ti.rid_tasklist = r_taskitem.rid_tasklist
where er.rid_mastertaskitemevent = r_taskitemevent.rid_mastertaskitemevent
)
, 'rid_tasklist_next', (
select jsonb_agg(distinct tl.rid_tasklist)
from core.taskitem ti
inner join core.mastertaskitem mti on mti.rid_mastertaskitem = ti.rid_mastertaskitem
inner join core.mastertask mtj on mti.guid = mti.jsonvalue ->> 'guid_mastertask_next'
inner join core.tasklist tl on tl.rid_mastertask = mtj.rid_mastertask
and tl.rid_hub = r_taskitem.rid_hub
where ti.rid_taskitem = r_taskitemevent.rid_taskitem
)
, 'select_taskitem_guid', (
select ti.guid
from core.taskitem ti
where
(m_final_rid_taskitem > 0
and ti.rid_taskitem = m_final_rid_taskitem
or nv(m_final_rid_taskitem) = 0
and ti.rid_tasklist = r_taskitem.rid_tasklist
)
order by
case
when ti.status in (10) then 2
when ti.status in (0, 1, 2) then 1
else 99
end
, ti.status desc, ti.seqno desc
limit 1
)
, 'openactions', (
select count(ti.rid_taskitem) filter ( where
coalesce(ti.status, 0) in (
core._enumi('eventstatus', 'todo'), core._enumi('eventstatus', 'planned')
)
)
from core.taskitem ti
where
ti.rid_tasklist in (
select r_taskitem.rid_tasklist
union
select tl.rid_tasklist_child
from core.tasklistlink tl
where tl.rid_tasklist_parent = r_taskitem.rid_tasklist
union
select tl.rid_tasklist_parent
from core.tasklistlink tl
where tl.rid_tasklist_child = r_taskitem.rid_tasklist
)
and nv(ti.subitem) = 0
and coalesce(ti.status, 0) <> coalesce(core._enumi('eventstatus', 'Canceled'), 0)
)
))
, 'WFL', to_jsonb(r_taskitem)));
j_obj = _jsonb_object_cat(j_obj
, jsonb_build_object('jsonvalue'
, _jsonb_object_cat(
j_obj -> 'jsonvalue'
, jsonb_build_object('complete'
, (j_obj -> 'jsonvalue' ->> 'rid_tasklist_event' is null
and j_obj -> 'jsonvalue' ->> 'rid_tasklist_next' is null
and _try_integer(j_obj -> 'jsonvalue' ->> 'openactions', 0) = 0
and nv(r_taskitemevent.jsonvalue ->> 'errorcode') not ilike 'E%'
)
)
)
)
);
j_result = j_result || j_obj;
if m_final_rid_taskitem > 0 and m_final_rid_taskitem is distinct from r_taskitem.rid_taskitem
then
j_obj = _jsonb_object_cat(to_jsonb(ev), jsonb_build_object(
'jsonvalue', jsonb_build_object(
'rid_tasklist', ti.rid_tasklist
, 'select_taskitem_guid', (
select ti.guid
from core.taskitem ti
where
(m_final_rid_taskitem > 0
and ti.rid_taskitem = m_final_rid_taskitem
or nv(m_final_rid_taskitem) = 0
and ti.rid_tasklist = r_taskitem.rid_tasklist
)
order by case when ti.status in (0, 1, 2) then 1 else 99 end, ti.status desc, ti.seqno desc
limit 1
)
, 'openactions', (
select count(ti.rid_taskitem) filter ( where
coalesce(ti.status, 0) in (
core._enumi('eventstatus', 'todo'), core._enumi('eventstatus', 'planned')
)
)
from core.taskitem ti
where
ti.rid_tasklist = r_taskitem.rid_tasklist
and nv(ti.subitem) = 0
and coalesce(ti.status, 0) <> coalesce(core._enumi('eventstatus', 'Canceled'), 0)
limit 1
)
)
, 'WFL', to_jsonb(ti)
)
)
from core.taskitem ti
inner join lateral (
select *
from core.taskitemevent ev
where
ev.rid_taskitem = ti.rid_taskitem
and ev.status in (2, 4)
order by ev.status desc
limit 1
) ev on ev.rid_taskitem = ti.rid_taskitem
where ti.rid_taskitem = m_final_rid_taskitem;
j_result = j_result || j_obj;
end if;
end loop;
--perform log_event(m_funcname,format('Result j_result: %s',j_result),bt_enum('eventlog','local notice'));
p_payload = j_result;
EXCEPTION
WHEN others THEN
GET STACKED DIAGNOSTICS
m_errmsg = MESSAGE_TEXT
,m_errcontext = PG_EXCEPTION_CONTEXT
,m_errdetail = PG_EXCEPTION_DETAIL
,m_errhint = PG_EXCEPTION_HINT
,m_errstate = RETURNED_SQLSTATE;
p_errmsg := get_err_msg(m_funcname, m_errmsg, m_errcontext, m_errdetail, m_errhint, m_errstate);
p_retval = 1;
perform log_event(m_funcname, format('Benchmark Err %s Items: %s', array_length(m_temp_rid_list, 1),
(clock_timestamp() - m_tm)::interval::text),
bt_enum('eventlog', 'local notice'));
END;
$$;
/*
select * from core.ui_action_event(jsonb_build_object('check_error',true
,'rid_mastertaskitem',(
select mti.rid_mastertaskitem from core.mastertaskitem mti
where mti.guid = '7a7348ec-7db6-4f13-bca7-c7840de1d22c'
),'rid_hub',20405))
;
select * from core.ui_action_event(jsonb_build_object('check_error',true
,'rid_mastertaskitem',(
select mti.rid_mastertaskitem from core.mastertaskitem mti
where mti.guid = '7a7348ec-7db6-4f13-bca7-c7840de1d22c'
),'rid_hub',20405))
;
select * from v_eventlog
selecT * from core.taskitem ti
order by ti.rid_taskitem desc
select mti.rid_mastertask,mti.rid_mastertaskitem, mti.description, mti.guid from core.mastertaskitem mti order by mti.rid_mastertaskitem desc;
*/
--select cli.rid_hub from t_adproclient cli where cli.clientid = '92092101980891'
+17 -17
View File
@@ -1,28 +1,28 @@
--select * from dropall('resolvespec_login');
CREATE OR REPLACE FUNCTION resolvespec_login(
INOUT p_data jsonb
,OUT p_success boolean
,OUT p_error text
INOUT p_data jsonb
, OUT p_success boolean
, OUT p_error text
)
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
AS
$$
DECLARE
--Error Handling--
m_funcname text = 'resolvespec_login';
m_errmsg text;
m_errcontext text;
m_errdetail text;
m_errhint text;
m_errstate text;
m_retval integer;
m_funcname text = 'resolvespec_login';
m_errmsg text;
m_errcontext text;
m_errdetail text;
m_errhint text;
m_errstate text;
m_retval integer;
--Error Handling--
m_rid_user integer;
m_rid_hub integer;
m_pass_hashed citext[];
m_session jsonb;
m_rid_user integer;
m_rid_hub integer;
m_pass_hashed citext[];
m_session jsonb;
m_allow_hash_auth boolean;
BEGIN
m_allow_hash_auth = _try_integer( p_data->'claims'->>'rid_user',0) > 0;
+124 -119
View File
@@ -1,84 +1,84 @@
CREATE OR REPLACE FUNCTION mm_proc(
p_doctype text --File Types: html, docx, text, allfieldvalues
,p_commtype text --Merge Data Type: e.g. SMS,Email,All (Will be passed through to menu/taglist funct)
,p_template bytea --Source/Template file to be merge
,p_data_prefix text --Table prefix for data source. e.g. tcli (client) , sta (standard letters)
,p_data_rid integer --rid for given table prefix
,p_filterdata json = NULL --Advanced filter data.
,OUT p_doc bytea --Merged document output
,OUT p_docguid text --optional guid to store document guid from tempfile table. (For disk reading)
,OUT p_retval integer
,OUT p_errmsg text
p_doctype text --File Types: html, docx, text, allfieldvalues
, p_commtype text --Merge Data Type: e.g. SMS,Email,All (Will be passed through to menu/taglist funct)
, p_template bytea --Source/Template file to be merge
, p_data_prefix text --Table prefix for data source. e.g. tcli (client) , sta (standard letters)
, p_data_rid integer --rid for given table prefix
, p_filterdata json = NULL --Advanced filter data.
, OUT p_doc bytea --Merged document output
, OUT p_docguid text --optional guid to store document guid from tempfile table. (For disk reading)
, OUT p_retval integer
, OUT p_errmsg text
)
LANGUAGE plpgsql
VOLATILE
LANGUAGE plpgsql
VOLATILE
AS
$$
DECLARE
--Error Handling--
m_funcname text = 'mm_proc';
m_errmsg text;
m_errcontext text;
m_errdetail text;
m_errhint text;
m_errstate text;
m_retval integer;
m_funcname text = 'mm_proc';
m_errmsg text;
m_errcontext text;
m_errdetail text;
m_errhint text;
m_errstate text;
m_retval integer;
--Error Handling--
m_blankexec text;
m_comma text;
m_debug_exestr text;
m_exec_orderstr text;
m_execfilter text;
m_execstr text;
m_fields json;
m_guid citext;
m_json json;
m_json_full json;
m_json_full_complex jsonb;
m_result text;
m_tablefilter text;
m_temppath citext;
m_user citext;
m_exttype citext;
m_returnvalues boolean = false;
m_data_prefix citext;
m_data_rid integer;
m_rid_comm integer;
m_blankexec text;
m_comma text;
m_debug_exestr text;
m_exec_orderstr text;
m_execfilter text;
m_execstr text;
m_fields json;
m_guid citext;
m_json json;
m_json_full json;
m_json_full_complex jsonb;
m_result text;
m_tablefilter text;
m_temppath citext;
m_user citext;
m_exttype citext;
m_returnvalues boolean = false;
m_data_prefix citext;
m_data_rid integer;
m_rid_comm integer;
m_rid_commattachment integer;
m_rid_obl integer;
m_doctype citext;
m_ltime timestamp;
m_hasfilestream boolean;
m_rid_obligation integer;
g_debug boolean;
g_tag_s text;
g_tag_e text;
g_tag_split text;
g_tag_oper text;
g_mtype_root integer = 0;
g_mtype_field integer = 1;
g_mtype_tblfield integer = 2;
g_mtype_tblroot integer = 3;
g_mtype_aggfield integer = 4;
g_mtype_picture integer = 5;
g_mtype_special integer = 6;
g_mtype_filter integer = 7;
g_mtype_condfield integer = 8;
g_mtype_docreplace integer = 9;
g_mtype_html integer = 10;
g_benchmark integer;
a_tblroot integer[];
a_types citext[];
a_inner_selected citext[];
r_template record;
r_doc record;
r_lp record;
r_lp_t record;
r_lp_c record;
r_retval record;
r_tmp record;
m_rid_obl integer;
m_doctype citext;
m_ltime timestamp;
m_hasfilestream boolean;
m_rid_obligation integer;
g_debug boolean;
g_tag_s text;
g_tag_e text;
g_tag_split text;
g_tag_oper text;
g_mtype_root integer = 0;
g_mtype_field integer = 1;
g_mtype_tblfield integer = 2;
g_mtype_tblroot integer = 3;
g_mtype_aggfield integer = 4;
g_mtype_picture integer = 5;
g_mtype_special integer = 6;
g_mtype_filter integer = 7;
g_mtype_condfield integer = 8;
g_mtype_docreplace integer = 9;
g_mtype_html integer = 10;
g_benchmark integer;
a_tblroot integer[];
a_types citext[];
a_inner_selected citext[];
r_template record;
r_doc record;
r_lp record;
r_lp_t record;
r_lp_c record;
r_retval record;
r_tmp record;
--r_lp_prev record;
m_start timestamp = clock_timestamp();
m_start timestamp = clock_timestamp();
BEGIN
p_retval = 0;
p_errmsg = '';
@@ -98,17 +98,17 @@ BEGIN
m_user = f_getuser();
-- if p_doctype ilike '%plain%' and f_iscompressed(p_template) in ('none')
-- and byteatotext(p_template) not ilike '%'|| G_TAG_S || '%' || G_TAG_E || '%'
-- then
-- raise warning 'No tags to merge in plain template.';
-- p_doc = p_template;
-- return;
-- end if;
-- and byteatotext(p_template) not ilike '%'|| G_TAG_S || '%' || G_TAG_E || '%'
-- then
-- raise warning 'No tags to merge in plain template.';
-- p_doc = p_template;
-- return;
-- end if;
-- perform log_event(m_funcname,format('INIT _ type=%s commtype=%s p_data_prefix=%s p_data_rid=%s Template:%s'
-- ,p_doctype, p_commtype,p_data_prefix,p_data_rid
-- ,case when octet_length(p_template) < 100 then byteatotext(p_template) else octet_length(p_template)::text end
-- ),bt_enum('eventlog','local notice'));
-- ,p_doctype, p_commtype,p_data_prefix,p_data_rid
-- ,case when octet_length(p_template) < 100 then byteatotext(p_template) else octet_length(p_template)::text end
-- ),bt_enum('eventlog','local notice'));
if p_data_prefix::citext = 'com'
then
@@ -138,7 +138,9 @@ BEGIN
select ctl.rid_parent as rid_parent
, ctl.tableprefix as parent_prefix
from tasklist ctl --inner join t_adproclient cli on cli.rid_adproclient = ctl.rid_parent -- and ctl.tableprefix = 'tcli'
from tasklist ctl
--inner join t_adproclient cli on cli.rid_adproclient = ctl.rid_parent
-- and ctl.tableprefix = 'tcli'
where ctl.rid_tasklist = p_data_rid into m_data_rid, m_data_prefix;
elsif p_data_prefix::citext = 'ehr'
@@ -168,7 +170,7 @@ BEGIN
into r_doc;
-- perform log_event(m_funcname,format('init = p_doctype = %s p_data_prefix = %s p_data_rid = %s p_template[size] = %s'
-- ,p_doctype,p_data_prefix,p_data_rid,octet_length(p_template)),bt_enum('eventlog','local notice'));
-- ,p_doctype,p_data_prefix,p_data_rid,octet_length(p_template)),bt_enum('eventlog','local notice'));
if p_doctype = 'allfieldvalues'
then
@@ -291,7 +293,7 @@ BEGIN
from src d ;
--CREATE INDEX "idx_tmp_merge_init_src_type" ON tmp_merge_init_src USING btree (merge_type);
--CREATE INDEX "idx_tmp_merge_init_src_tag" ON tmp_merge_init_src USING btree (mergetag);
--CREATE INDEX "idx_tmp_merge_init_src_tag" ON tmp_merge_init_src USING btree (mergetag);
--HTML field types has a special meaning in the frontend. We treat them as normal fields.
update tmp_merge_init_src u
@@ -513,8 +515,8 @@ BEGIN
)
then
-- perform log_event(m_funcname,format('NO TAGS p_doctype=%s p_commtype=%s p_data_prefix=%s p_data_rid=%s'
-- ,p_doctype, p_commtype,p_data_prefix,p_data_rid),bt_enum('eventlog','local notice'));
--
-- ,p_doctype, p_commtype,p_data_prefix,p_data_rid),bt_enum('eventlog','local notice'));
--
p_doc = p_template;
return;
@@ -545,15 +547,16 @@ BEGIN
/*
---Use this code to test
select pl_writefile('/mnt/t/temp/t.docx',r.p_doc) ,r.*
from mm_proc('docx','allfieldvalues', (
select pl_writefile('/mnt/t/temp/t.docx',r.p_doc)
,r.*
from mm_proc('docx','allfieldvalues', (
select tat.maindoc
from templateattachment tat
--cross join view_blob(tat.rid_templateattachment,'templateattachment','maindoc','rid_templateattachment','utf8') v
where tat.description ilike 'Form 16 (%'
), 'TCLI',1000016) r
), 'TCLI',1000016) r
*/
/*
/*
drop table if exists debug_merge_init_src;
create table debug_merge_init_src as
@@ -632,7 +635,7 @@ BEGIN
end if;*/
if G_BENCHMARK = 1
then
then
perform log_event(m_funcname,format('Perf Merge Replace SinceStart: %s Duration: %s', clock_timestamp() - m_start, clock_timestamp() - m_ltime),bt_enum('eventlog','local notice'));
m_ltime = clock_timestamp();
end if;
@@ -735,7 +738,7 @@ BEGIN
, '\[(.*?)\]', 'ig') r(v) into m_errmsg,m_retval;
if m_retval > 0
then
then
raise exception E'The following data fields could not be found for prefix % \r\n%', ifblnk(m_data_prefix, 'Null'), m_errmsg using hint = 'in ID replace process';
end if;
@@ -1130,22 +1133,22 @@ BEGIN
)
loop
-- perform log_event(m_funcname,format(' Tables %s=%s src=%s rn=%s' ,p_data_prefix,p_data_rid
-- ,r_lp_t.source,r_lp_t.rn
-- ),bt_enum('eventlog','local notice')
-- --,(select jsonb_agg(row_to_json(f)::jsonb) from tmp_merge_init_fields f)::text
-- );
-- ,r_lp_t.source,r_lp_t.rn
-- ),bt_enum('eventlog','local notice')
-- --,(select jsonb_agg(row_to_json(f)::jsonb) from tmp_merge_init_fields f)::text
-- );
if r_lp_t.table_name = any(a_inner_selected) and nv(r_lp_t.table_name ) <> ''
then
then
raise notice 'Table used as inner table: %', r_lp_t.table_name;
continue;
end if;
--raise notice 'Field:% Table:% Tag: % Type: %', r_lp_t.field_name, r_lp_t.table_name, r_lp_t.mergetag, r_lp_t.merge_type;
if nv(r_lp_t.field_name) = '' and r_lp_t.merge_type not in (G_MTYPE_TBLROOT,G_MTYPE_SPECIAL)
then
then
if G_DEBUG
then
then
perform log_event(m_funcname,format('Blank field name on Complex merge for p_doctype=%s, p_commtype=%s, p_data_prefix=%s, p_data_rid=%s
field_name=%s, merge_type=%s, table_name=%s
' ,p_doctype, p_commtype,p_data_prefix,p_data_rid
@@ -1159,11 +1162,11 @@ BEGIN
end if;
if nv(m_exec_orderstr) = '' and nv(r_lp_t.parent_order_string) <> ''
then
then
m_exec_orderstr = r_lp_t.parent_order_string;
--raise notice 'Applying order % by for % %.', r_lp_t.parent_order_string, r_lp_t.parent_table_name,r_lp_t.field_name;
/*
select s.parent_order_string
select s.parent_order_string
from tmp_merge_init_fields f
inner join tmp_merge_init_src s on s.parent_rid = r_lp_t.rid
and s.merge_type = G_MTYPE_TBLROOT
@@ -1171,12 +1174,12 @@ BEGIN
where f.source = r_lp_t.source
limit 1
into m_exec_orderstr;
*/
*/
end if;
if nv(r_lp_t.ops_string) = ''
then
then
r_lp_t.ops_string = r_lp_t.field_name;
end if;
@@ -1192,19 +1195,19 @@ BEGIN
end loop;
if r_lp_t.merge_type in (G_MTYPE_TBLFIELD, G_MTYPE_CONDFIELD)
then
then
m_execstr = format($S$%s|| '%s"%s":' || json_build_object('value',json_agg(%s::text %s), 'type', '%s')::text %s$S$
,m_execstr,m_comma,r_lp_t.mergetag
,r_lp_t.ops_string
, m_exec_orderstr, r_lp_t.merge_type, E'\r\n');
elseif r_lp_t.merge_type = G_MTYPE_SPECIAL--special fields
then
then
m_execstr = format($S$%s|| '%s"%s":' || json_build_object('value',%s, 'type', '%s')::text %s$S$
,m_execstr,m_comma,r_lp_t.mergetag,quote_literal(r_lp_t.tagvalue), r_lp_t.merge_type, E'\r\n');
--raise notice 'Special Field: %s',r_lp_t;
elseif r_lp.merge_type = G_MTYPE_PICTURE
then
then
m_execstr = format($S$%s|| '%s"%s":'
|| json_build_object('value',%s::text, 'type', '%s'
, 'w', mailmerge_specialfield('width', '%s', %s) ,'h', mailmerge_specialfield('height', '%s', %s))::text %s$S$
@@ -1212,7 +1215,7 @@ BEGIN
,r_lp.mergetag,quote_nullable(m_data_rid),r_lp.mergetag,quote_nullable(m_data_rid), E'\r\n');
elseif nv(r_lp_t.field_name) <> ''
then
then
m_execstr = format($S$%s|| '%s"%s":' || json_build_object('value',%s::text, 'type', '%s')::text %s$S$
,m_execstr,m_comma,r_lp_t.mergetag
, r_lp_t.ops_string
@@ -1225,7 +1228,7 @@ BEGIN
m_blankexec = format($S$%s|| '%s"%s":' || json_build_object('value','', 'type', '%s')::text %s$S$,m_blankexec,m_comma,r_lp_t.mergetag, r_lp_t.merge_type, E'\r\n');
if r_lp_t.rn = 1
then
then
--Inner level tables (2)
--raise notice 'Begin: parent: %', r_lp_t;
for r_lp_c in (
@@ -1330,7 +1333,9 @@ BEGIN
inner
join tmp_merge_init_fields d
on d . mergetag = c . mergetag and d . tblparent = r_lp_t . tblid
where f . grand_rid = r_lp_t . parent_rid and f . merge_type = G_MTYPE_TBLROOT --and f.parent_rid = any(a_tblroot) --and d.table_level > 0
where f . grand_rid = r_lp_t . parent_rid and f . merge_type = G_MTYPE_TBLROOT
--and f.parent_rid = any(a_tblroot)
--and d.table_level > 0
group by f . rid ) loop
raise notice 'Inner Loop: %', r_lp_c . qry;
@@ -1345,7 +1350,7 @@ BEGIN
end loop;
if ifblnk(r_lp_t.parent_table_name,'') = ''
then
then
m_execstr = format(E'select (''{'' %s \r\n || ''}'')::json ;',m_execstr );
else
select string_agg(s.filter_string, ' ')
@@ -1364,7 +1369,7 @@ BEGIN
from exec_json(m_execstr, 'str json') r into m_retval,m_errmsg, m_json;
if m_json is null
then
then
select r.p_retval, r.p_errmsg, r.p_json - > 'str'
from exec_json(m_execstr, 'str json') r into m_retval,m_errmsg, m_json;
@@ -1373,22 +1378,22 @@ BEGIN
m_debug_exestr = nv(m_debug_exestr) || E'\r\n/*'|| nv(r_lp_t.parent_table_name) || ' len:' || nv(length(m_json::text)) ||E'*/ \r\n' || nv(m_execstr) || E'\r\n ';
if m_json_full_complex is null
then
then
m_json_full_complex = jsonb_build_object(r_lp_t.tblid::text,m_json);
end if;
if (m_json_full_complex->r_lp_t.tblid::text) is null
then
then
m_json_full_complex = jsonb_set(m_json_full_complex, format('{%s}',r_lp_t.tblid)::text[], m_json::jsonb,true);
else
m_json_full_complex = jsonb_set(m_json_full_complex, format('{%s}',r_lp_t.tblid)::text[], _jsonb_object_cat(m_json_full_complex->r_lp_t.tblid,m_json::jsonb),true);
end if;
-- perform log_event(m_funcname,format('Complex Tables %s=%s m_json_full_complex=%s' ,p_data_prefix,p_data_rid
-- ,m_json_full_complex::text
-- ),bt_enum('eventlog','local notice')
-- --,(select jsonb_agg(row_to_json(f)::jsonb) from tmp_merge_init_fields f)::text
-- );
-- ,m_json_full_complex::text
-- ),bt_enum('eventlog','local notice')
-- --,(select jsonb_agg(row_to_json(f)::jsonb) from tmp_merge_init_fields f)::text
-- );
m_execfilter = '';
m_execstr = '';
@@ -1399,13 +1404,13 @@ BEGIN
end if;
if nv(m_comma) = '' and length(m_execstr) > 2
then
then
m_comma = ',';
end if;
end loop;
if G_DEBUG
then
then
perform pl_writefile(r_template.debugsql_filename, convert_to(m_debug_exestr,'utf8'));
end if;