5 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
23 changed files with 4222 additions and 180 deletions
+3 -1
View File
@@ -164,4 +164,6 @@ dist
.pnp.*
temp/*
dist/*
dist/*
# Local build of the CLI binary
/pgtidy
+22 -7
View File
@@ -60,13 +60,28 @@ testdata/corpus/ — real-world .pgsql procedures used as the safety/idempoten
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.SemanticallyEqual(src, out)` before writing or returning
formatted output. It re-lexes both sides and compares non-trivia token streams
(case-insensitive for identifiers/keywords, exact otherwise, recursing into dollar-quoted
bodies). If it ever returns false, 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
not just a test assertion (`pkg/format/format_test.go`); it is enforced at runtime so a
formatter bug can never silently drop or alter code.
`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
+4 -4
View File
@@ -63,8 +63,8 @@ func cmdFmt(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
return 2
}
out := format.File(parser.Parse(string(src)), st)
if !format.SemanticallyEqual(string(src), out) {
_, _ = fmt.Fprintln(stderr, "pgtidy: refusing to format stdin: formatter safety check failed (output would change code content)")
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 {
@@ -90,8 +90,8 @@ func cmdFmt(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
continue
}
out := format.File(parser.Parse(string(src)), st)
if !format.SemanticallyEqual(string(src), out) {
_, _ = fmt.Fprintf(stderr, "pgtidy: refusing to format %s: formatter safety check failed (output would change code content)\n", path)
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
}
+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
+1 -1
View File
@@ -1,6 +1,6 @@
# Maintainer: Hein (Warky Devs) <hein@warky.dev>
pkgname=pgtidy-bin
pkgver=0.0.7
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.7
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,
+85 -8
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)
@@ -454,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):]
@@ -468,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.
@@ -589,7 +658,13 @@ func formatBodyStmtLines(lines []bline, baseIndent string, st config.Style) []st
for i, ll := range lines {
text := ll.text
indent := baseIndent
if i > 0 && ll.indent != "" && !isStandaloneBodyKeyword(ll.text, "then", "else", "elsif", "elseif") {
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)
@@ -639,7 +714,9 @@ func reindentBodyDML(lines []bline, baseIndent string, st config.Style) []string
continue
}
if lineDepth == 0 && (kw == "and" || kw == "or") {
out = append(out, baseIndent+st.Indent+strings.TrimSpace(text))
// 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
+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{
+15 -17
View File
@@ -23,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"
@@ -130,8 +130,8 @@ func TestFormatIssue1PLpgSQLIndenting(t *testing.T) {
want := "CREATE FUNCTION f(\n" +
")\n" +
"RETURNS void\n" +
"LANGUAGE plpgsql\n" +
" RETURNS void\n" +
" LANGUAGE plpgsql\n" +
"AS\n" +
"$$\n" +
"DECLARE\n" +
@@ -144,14 +144,14 @@ func TestFormatIssue1PLpgSQLIndenting(t *testing.T) {
" set status = 'done'\n" +
" where\n" +
" u.rid_process = r_lp.rid_process\n" +
" and nv(u.status) <> 'done';\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" +
" and nv(u.status) <> 'open';\n" +
"\n" +
" end if;\n" +
"$$;\n"
@@ -184,18 +184,16 @@ 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 is a test-local alias for the exported safety check.
+201 -3
View File
@@ -1,11 +1,52 @@
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.
@@ -14,9 +55,9 @@ import (
// 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 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.
// 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)
@@ -32,6 +73,14 @@ func SemanticallyEqual(a, b string) bool {
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)
@@ -47,6 +96,155 @@ func SemanticallyEqual(a, b string) bool {
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 {
+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)
}
}
+2 -2
View File
@@ -124,7 +124,7 @@ func (s *server) handle(raw []byte) bool {
s.reply(req.ID, []textEdit{})
return false
}
if !format.SemanticallyEqual(text, formatted) {
if err := format.VerifySafe(text, formatted, s.cfg); err != nil {
s.reply(req.ID, []textEdit{})
return false
}
@@ -228,7 +228,7 @@ func (s *server) rangeFormat(text string, r lspRange) []textEdit {
if formatted == text {
return nil
}
if !format.SemanticallyEqual(text, formatted) {
if err := format.VerifySafe(text, formatted, s.cfg); err != nil {
return nil
}
+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;
+106 -101
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
@@ -1130,10 +1133,10 @@ 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
@@ -1163,7 +1166,7 @@ BEGIN
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,7 +1174,7 @@ BEGIN
where f.source = r_lp_t.source
limit 1
into m_exec_orderstr;
*/
*/
end if;
@@ -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;
@@ -1385,10 +1390,10 @@ BEGIN
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 = '';