Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf9ea5fbda | ||
|
|
5cdec88299 | ||
|
|
83b215fd25 | ||
|
|
94d776e3de | ||
|
|
1be745b69c | ||
|
|
25d40ad515 | ||
|
|
bac966c2ac | ||
|
|
4abc89700f | ||
|
|
ad52f21cc2 | ||
|
|
60602d1de7 | ||
|
|
8d19258aa0 | ||
|
|
5cba6beeb1 | ||
|
|
a0838e4bdc | ||
|
|
44fb77efd6 | ||
|
|
4fab2fe652 | ||
|
|
64aaae30f4 | ||
|
|
c277171963 | ||
|
|
04711cf7b2 | ||
|
|
f17e87e749 | ||
|
|
c8030247f2 | ||
|
|
a58b081cae |
@@ -170,6 +170,7 @@ jobs:
|
|||||||
VERSION="${{ github.event.inputs.tag || github.ref_name }}"
|
VERSION="${{ github.event.inputs.tag || github.ref_name }}"
|
||||||
PKGVER="${VERSION#v}"
|
PKGVER="${VERSION#v}"
|
||||||
|
|
||||||
|
sudo apt-get update -y
|
||||||
sudo apt-get install -y rpm
|
sudo apt-get install -y rpm
|
||||||
|
|
||||||
CGO_ENABLED=0 go build \
|
CGO_ENABLED=0 go build \
|
||||||
@@ -207,6 +208,52 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
pkg-nsis:
|
||||||
|
name: Windows installer
|
||||||
|
needs: release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Build NSIS installer
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
VERSION="${{ github.event.inputs.tag || github.ref_name }}"
|
||||||
|
PKGVER="${VERSION#v}"
|
||||||
|
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y nsis
|
||||||
|
|
||||||
|
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build \
|
||||||
|
-trimpath \
|
||||||
|
-ldflags "-X main.version=${PKGVER}" \
|
||||||
|
-o pgtidy.exe \
|
||||||
|
./cmd/pgtidy
|
||||||
|
|
||||||
|
makensis -DVERSION="${PKGVER}" -DSRC_EXE="$(pwd)/pgtidy.exe" windows/installer.nsi
|
||||||
|
|
||||||
|
- name: Upload to release
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
TAG="${{ github.event.inputs.tag || github.ref_name }}"
|
||||||
|
RELEASE=$(curl -s "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" \
|
||||||
|
-H "Authorization: token ${GITHUB_TOKEN}")
|
||||||
|
UPLOAD_URL=$(echo "$RELEASE" | grep -o '"upload_url":"[^"]*"' | cut -d'"' -f4 | sed 's/{[^}]*}//')
|
||||||
|
[ -z "$UPLOAD_URL" ] && { echo "upload_url not found: $RELEASE"; exit 1; }
|
||||||
|
for f in windows/pgtidy-setup-*.exe; do
|
||||||
|
echo "Uploading $(basename "$f")..."
|
||||||
|
curl -s -X POST "${UPLOAD_URL}?name=$(basename "$f")" \
|
||||||
|
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary "@${f}" > /dev/null
|
||||||
|
done
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
pkg-aur:
|
pkg-aur:
|
||||||
name: AUR package
|
name: AUR package
|
||||||
needs: release
|
needs: release
|
||||||
@@ -318,6 +365,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
|
|
||||||
|
- name: Set version from tag
|
||||||
|
working-directory: editors/vscode
|
||||||
|
run: |
|
||||||
|
TAG="${{ github.event.inputs.tag || github.ref_name }}"
|
||||||
|
npm pkg set version="${TAG#v}"
|
||||||
|
|
||||||
- name: Install and package
|
- name: Install and package
|
||||||
working-directory: editors/vscode
|
working-directory: editors/vscode
|
||||||
run: |
|
run: |
|
||||||
@@ -355,6 +408,12 @@ jobs:
|
|||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: '21'
|
java-version: '21'
|
||||||
|
|
||||||
|
- name: Set version from tag
|
||||||
|
working-directory: editors/datagrip
|
||||||
|
run: |
|
||||||
|
TAG="${{ github.event.inputs.tag || github.ref_name }}"
|
||||||
|
sed -i "s/^pluginVersion=.*/pluginVersion=${TAG#v}/" gradle.properties
|
||||||
|
|
||||||
- name: Build plugin
|
- name: Build plugin
|
||||||
working-directory: editors/datagrip
|
working-directory: editors/datagrip
|
||||||
run: ./gradlew buildPlugin
|
run: ./gradlew buildPlugin
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ jobs:
|
|||||||
cache: npm
|
cache: npm
|
||||||
cache-dependency-path: editors/vscode/package-lock.json
|
cache-dependency-path: editors/vscode/package-lock.json
|
||||||
|
|
||||||
|
- name: Set version from tag
|
||||||
|
working-directory: editors/vscode
|
||||||
|
run: npm pkg set version="${GITHUB_REF_NAME#v}"
|
||||||
|
|
||||||
- name: Install and package
|
- name: Install and package
|
||||||
working-directory: editors/vscode
|
working-directory: editors/vscode
|
||||||
run: |
|
run: |
|
||||||
@@ -73,6 +77,10 @@ jobs:
|
|||||||
- name: Setup Gradle
|
- name: Setup Gradle
|
||||||
uses: gradle/actions/setup-gradle@v4
|
uses: gradle/actions/setup-gradle@v4
|
||||||
|
|
||||||
|
- name: Set version from tag
|
||||||
|
working-directory: editors/datagrip
|
||||||
|
run: sed -i "s/^pluginVersion=.*/pluginVersion=${GITHUB_REF_NAME#v}/" gradle.properties
|
||||||
|
|
||||||
- name: Build plugin
|
- name: Build plugin
|
||||||
working-directory: editors/datagrip
|
working-directory: editors/datagrip
|
||||||
run: ./gradlew buildPlugin
|
run: ./gradlew buildPlugin
|
||||||
|
|||||||
@@ -165,3 +165,5 @@ dist
|
|||||||
|
|
||||||
temp/*
|
temp/*
|
||||||
dist/*
|
dist/*
|
||||||
|
# Local build of the CLI binary
|
||||||
|
/pgtidy
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
version: "2"
|
||||||
|
|
||||||
|
linters:
|
||||||
|
default: standard
|
||||||
@@ -36,7 +36,7 @@ pkg/pgast/ — go-pgquery wrapper: SQL → real PG AST (lint, v2)
|
|||||||
pkg/lint/ — rule engine + rule packs (v2)
|
pkg/lint/ — rule engine + rule packs (v2)
|
||||||
pkg/lsp/ — LSP server (v3)
|
pkg/lsp/ — LSP server (v3)
|
||||||
editors/vscode/ — VSCode extension (v3)
|
editors/vscode/ — VSCode extension (v3)
|
||||||
editors/datagrip/ — LSP4IJ integration (v4)
|
editors/datagrip/ — native JetBrains plugin, shells out to the CLI (v4)
|
||||||
testdata/corpus/ — real-world .pgsql procedures used as the safety/idempotence harness
|
testdata/corpus/ — real-world .pgsql procedures used as the safety/idempotence harness
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -59,6 +59,29 @@ testdata/corpus/ — real-world .pgsql procedures used as the safety/idempoten
|
|||||||
3. **Idempotence**: `fmt(fmt(x)) == fmt(x)`.
|
3. **Idempotence**: `fmt(fmt(x)) == fmt(x)`.
|
||||||
4. **Graceful degradation**: any span the parser cannot handle is passed through verbatim
|
4. **Graceful degradation**: any span the parser cannot handle is passed through verbatim
|
||||||
rather than corrupted.
|
rather than corrupted.
|
||||||
|
5. **Runtime safety gate**: every frontend (CLI `fmt`, LSP `textDocument/formatting` and
|
||||||
|
`rangeFormatting`) calls `format.VerifySafe(src, out, style)` before writing or returning
|
||||||
|
formatted output. If it returns a non-nil error the formatter has a bug — the caller must
|
||||||
|
refuse to write/emit the result and keep the original source, never guess or best-effort
|
||||||
|
it. This is enforced at runtime, not just in `pkg/format/*_test.go`, so a formatter bug can
|
||||||
|
never silently drop or alter code. `VerifySafe` runs four checks:
|
||||||
|
- **Semantic equivalence** (`SemanticallyEqual`): re-lex both sides, compare the non-trivia
|
||||||
|
token streams — case-insensitive for identifiers/keywords, exact otherwise, recursing
|
||||||
|
into dollar-quoted bodies. Comments and whitespace are trivia and are ignored here.
|
||||||
|
- **Comment preservation** (`CommentsPreserved`): every `--` and `/* */` comment in `src`
|
||||||
|
reappears in `out`, in order, with the same content (line endings and indentation are
|
||||||
|
normalised away; dropping, merging, splitting, reordering, or rewording a comment is
|
||||||
|
not). Descends into dollar-quoted bodies.
|
||||||
|
- **Structural balance** (`StructurallyBalanced`): the `( ) [ ]` and BEGIN/CASE/IF/LOOP…END
|
||||||
|
nesting profile of `out` matches `src`, counting only real code tokens (comment and
|
||||||
|
string/dollar-quote contents are skipped).
|
||||||
|
- **Idempotence**: re-formatting `out` yields `out` unchanged.
|
||||||
|
|
||||||
|
**Line endings**: the formatter re-emits all layout with `st.Newline` (default `\n`), so a
|
||||||
|
CRLF input file is normalised to LF on write. This includes `\r\n` that sits *inside* a
|
||||||
|
multi-line string literal — a line-ending change there is layout normalisation, not a change
|
||||||
|
of code content, so `SemanticallyEqual` compares string tokens modulo `\r\n` ↔ `\n`
|
||||||
|
(`normNL` in `pkg/format/safety.go`). Set `newline: "\r\n"` in `.pgtidy.yaml` to keep CRLF.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -111,8 +134,9 @@ go fmt ./... # Format Go code
|
|||||||
Per-platform VSIX (`win32/linux/darwin × x64/arm64`) built in CI matrix.
|
Per-platform VSIX (`win32/linux/darwin × x64/arm64`) built in CI matrix.
|
||||||
|
|
||||||
### V4 — DataGrip
|
### V4 — DataGrip
|
||||||
- `editors/datagrip`: integrate via LSP4IJ (free, works across JetBrains editions incl.
|
- `editors/datagrip`: native JetBrains plugin (no LSP4IJ dependency) that shells out to the
|
||||||
DataGrip). No core changes expected.
|
`pgtidy` binary directly. Formatting and version info run `pgtidy fmt`/`version`; lint
|
||||||
|
diagnostics and quick-fixes run via a native `ExternalAnnotator` calling `pgtidy lint --json`.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
- **Formatter:** `go test ./...` runs golden-file tests + corpus harness asserting idempotence
|
- **Formatter:** `go test ./...` runs golden-file tests + corpus harness asserting idempotence
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ vet:
|
|||||||
fmt:
|
fmt:
|
||||||
go fmt ./...
|
go fmt ./...
|
||||||
|
|
||||||
## lint: vet + format check
|
## lint: vet + format check + golangci-lint
|
||||||
lint: vet
|
lint: vet
|
||||||
test -z "$$(gofmt -l .)" || (echo "gofmt needed:"; gofmt -l .; exit 1)
|
test -z "$$(gofmt -l .)" || (echo "gofmt needed:"; gofmt -l .; exit 1)
|
||||||
|
golangci-lint run ./...
|
||||||
|
|
||||||
## clean: remove build artifacts
|
## clean: remove build artifacts
|
||||||
clean:
|
clean:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
func cmdConfig(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
func cmdConfig(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||||
for _, a := range args {
|
for _, a := range args {
|
||||||
if a == "-h" || a == "--help" {
|
if a == "-h" || a == "--help" {
|
||||||
fmt.Fprintln(stdout, "pgtidy config — print effective configuration resolved from .pgtidy.yaml")
|
_, _ = fmt.Fprintln(stdout, "pgtidy config — print effective configuration resolved from .pgtidy.yaml")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,14 +21,14 @@ func cmdConfig(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
}
|
}
|
||||||
st, err := config.Load(wd)
|
st, err := config.Load(wd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(stderr, "pgtidy: %v\n", err)
|
_, _ = fmt.Fprintf(stderr, "pgtidy: %v\n", err)
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
fmt.Fprintf(stdout, "indent: %q\n", st.Indent)
|
_, _ = fmt.Fprintf(stdout, "indent: %q\n", st.Indent)
|
||||||
fmt.Fprintf(stdout, "newline: %q\n", st.Newline)
|
_, _ = fmt.Fprintf(stdout, "newline: %q\n", st.Newline)
|
||||||
fmt.Fprintf(stdout, "keyword_case: %s\n", st.KeywordCase)
|
_, _ = fmt.Fprintf(stdout, "keyword_case: %s\n", st.KeywordCase)
|
||||||
fmt.Fprintf(stdout, "ident_case: %s\n", st.IdentCase)
|
_, _ = fmt.Fprintf(stdout, "ident_case: %s\n", st.IdentCase)
|
||||||
fmt.Fprintf(stdout, "type_case: %s\n", st.TypeCase)
|
_, _ = fmt.Fprintf(stdout, "type_case: %s\n", st.TypeCase)
|
||||||
fmt.Fprintf(stdout, "commas: %s\n", st.Commas)
|
_, _ = fmt.Fprintf(stdout, "commas: %s\n", st.Commas)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-10
@@ -37,7 +37,7 @@ func cmdFmt(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
return 0
|
return 0
|
||||||
default:
|
default:
|
||||||
if len(a) > 1 && a[0] == '-' {
|
if len(a) > 1 && a[0] == '-' {
|
||||||
fmt.Fprintf(stderr, "pgtidy fmt: unknown flag %q\n", a)
|
_, _ = fmt.Fprintf(stderr, "pgtidy fmt: unknown flag %q\n", a)
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
files = append(files, a)
|
files = append(files, a)
|
||||||
@@ -51,7 +51,7 @@ func cmdFmt(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
}
|
}
|
||||||
st, err := config.Load(wd)
|
st, err := config.Load(wd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(stderr, "%v\n", err)
|
_, _ = fmt.Fprintf(stderr, "%v\n", err)
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,19 +59,23 @@ func cmdFmt(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
src, err := io.ReadAll(stdin)
|
src, err := io.ReadAll(stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(stderr, "pgtidy: reading stdin: %v\n", err)
|
_, _ = fmt.Fprintf(stderr, "pgtidy: reading stdin: %v\n", err)
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
out := format.File(parser.Parse(string(src)), st)
|
out := format.File(parser.Parse(string(src)), st)
|
||||||
|
if err := format.VerifySafe(string(src), out, st); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "pgtidy: refusing to format stdin: formatter safety check failed: %v\n", err)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
switch {
|
switch {
|
||||||
case check:
|
case check:
|
||||||
if out != string(src) {
|
if out != string(src) {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
case diff:
|
case diff:
|
||||||
io.WriteString(stdout, unifiedDiff(string(src), out, "stdin"))
|
_, _ = io.WriteString(stdout, unifiedDiff(string(src), out, "stdin"))
|
||||||
default:
|
default:
|
||||||
io.WriteString(stdout, out)
|
_, _ = io.WriteString(stdout, out)
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -81,11 +85,16 @@ func cmdFmt(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
for _, path := range files {
|
for _, path := range files {
|
||||||
src, err := os.ReadFile(path)
|
src, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(stderr, "pgtidy: %v\n", err)
|
_, _ = fmt.Fprintf(stderr, "pgtidy: %v\n", err)
|
||||||
exit = 2
|
exit = 2
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out := format.File(parser.Parse(string(src)), st)
|
out := format.File(parser.Parse(string(src)), st)
|
||||||
|
if err := format.VerifySafe(string(src), out, st); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "pgtidy: refusing to format %s: formatter safety check failed: %v\n", path, err)
|
||||||
|
exit = 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
changed := out != string(src)
|
changed := out != string(src)
|
||||||
if changed {
|
if changed {
|
||||||
anyDiff = true
|
anyDiff = true
|
||||||
@@ -94,22 +103,22 @@ func cmdFmt(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
case write:
|
case write:
|
||||||
if changed {
|
if changed {
|
||||||
if err := os.WriteFile(path, []byte(out), 0o644); err != nil {
|
if err := os.WriteFile(path, []byte(out), 0o644); err != nil {
|
||||||
fmt.Fprintf(stderr, "pgtidy: writing %s: %v\n", path, err)
|
_, _ = fmt.Fprintf(stderr, "pgtidy: writing %s: %v\n", path, err)
|
||||||
exit = 2
|
exit = 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case list:
|
case list:
|
||||||
if changed {
|
if changed {
|
||||||
fmt.Fprintln(stdout, path)
|
_, _ = fmt.Fprintln(stdout, path)
|
||||||
}
|
}
|
||||||
case diff:
|
case diff:
|
||||||
if changed {
|
if changed {
|
||||||
io.WriteString(stdout, unifiedDiff(string(src), out, path))
|
_, _ = io.WriteString(stdout, unifiedDiff(string(src), out, path))
|
||||||
}
|
}
|
||||||
case check:
|
case check:
|
||||||
// handled after loop via anyDiff
|
// handled after loop via anyDiff
|
||||||
default:
|
default:
|
||||||
io.WriteString(stdout, out)
|
_, _ = io.WriteString(stdout, out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if check && anyDiff && exit == 0 {
|
if check && anyDiff && exit == 0 {
|
||||||
|
|||||||
+40
-18
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@@ -18,9 +19,10 @@ import (
|
|||||||
// file:line:col: [RULEID] severity: message
|
// file:line:col: [RULEID] severity: message
|
||||||
func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||||
var (
|
var (
|
||||||
only []string // --only=ID,ID rule filter
|
only []string // --only=ID,ID rule filter
|
||||||
fix bool
|
fix bool
|
||||||
files []string
|
jsonOutput bool
|
||||||
|
files []string
|
||||||
)
|
)
|
||||||
for _, a := range args {
|
for _, a := range args {
|
||||||
switch {
|
switch {
|
||||||
@@ -29,6 +31,8 @@ func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
return 0
|
return 0
|
||||||
case a == "--fix":
|
case a == "--fix":
|
||||||
fix = true
|
fix = true
|
||||||
|
case a == "--json":
|
||||||
|
jsonOutput = true
|
||||||
case strings.HasPrefix(a, "--only="):
|
case strings.HasPrefix(a, "--only="):
|
||||||
ids := strings.Split(strings.TrimPrefix(a, "--only="), ",")
|
ids := strings.Split(strings.TrimPrefix(a, "--only="), ",")
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
@@ -37,7 +41,7 @@ func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case len(a) > 1 && a[0] == '-':
|
case len(a) > 1 && a[0] == '-':
|
||||||
fmt.Fprintf(stderr, "pgtidy lint: unknown flag %q\n", a)
|
_, _ = fmt.Fprintf(stderr, "pgtidy lint: unknown flag %q\n", a)
|
||||||
return 2
|
return 2
|
||||||
default:
|
default:
|
||||||
files = append(files, a)
|
files = append(files, a)
|
||||||
@@ -66,7 +70,13 @@ func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
return diags, nil
|
return diags, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
printDiags := func(diags []diagnostics.Diagnostic) {
|
var jsonDiags []diagnostics.Diagnostic
|
||||||
|
|
||||||
|
reportDiags := func(diags []diagnostics.Diagnostic) {
|
||||||
|
if jsonOutput {
|
||||||
|
jsonDiags = append(jsonDiags, diags...)
|
||||||
|
return
|
||||||
|
}
|
||||||
for _, d := range diags {
|
for _, d := range diags {
|
||||||
loc := d.File
|
loc := d.File
|
||||||
if d.Line > 0 {
|
if d.Line > 0 {
|
||||||
@@ -75,7 +85,7 @@ func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
if loc == "" {
|
if loc == "" {
|
||||||
loc = "stdin"
|
loc = "stdin"
|
||||||
}
|
}
|
||||||
fmt.Fprintf(stdout, "%s: [%s] %s: %s\n", loc, d.RuleID, d.Severity, d.Message)
|
_, _ = fmt.Fprintf(stdout, "%s: [%s] %s: %s\n", loc, d.RuleID, d.Severity, d.Message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,22 +94,22 @@ func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
src, err := io.ReadAll(stdin)
|
src, err := io.ReadAll(stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(stderr, "pgtidy: reading stdin: %v\n", err)
|
_, _ = fmt.Fprintf(stderr, "pgtidy: reading stdin: %v\n", err)
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
diags, err := check(string(src), "")
|
diags, err := check(string(src), "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(stderr, "pgtidy: %v\n", err)
|
_, _ = fmt.Fprintf(stderr, "pgtidy: %v\n", err)
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
if fix {
|
if fix && !jsonOutput {
|
||||||
fixed := lint.ApplyFixes(string(src), diags)
|
fixed := lint.ApplyFixes(string(src), diags)
|
||||||
if fixed != string(src) {
|
if fixed != string(src) {
|
||||||
io.WriteString(stdout, fixed)
|
_, _ = io.WriteString(stdout, fixed)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
printDiags(diags)
|
reportDiags(diags)
|
||||||
if len(diags) > 0 {
|
if len(diags) > 0 {
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
@@ -107,36 +117,47 @@ func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
for _, path := range files {
|
for _, path := range files {
|
||||||
src, err := os.ReadFile(path)
|
src, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(stderr, "pgtidy: %v\n", err)
|
_, _ = fmt.Fprintf(stderr, "pgtidy: %v\n", err)
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
diags, err := check(string(src), path)
|
diags, err := check(string(src), path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(stderr, "pgtidy: %v\n", err)
|
_, _ = fmt.Fprintf(stderr, "pgtidy: %v\n", err)
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
if fix {
|
if fix && !jsonOutput {
|
||||||
fixed := lint.ApplyFixes(string(src), diags)
|
fixed := lint.ApplyFixes(string(src), diags)
|
||||||
if fixed != string(src) {
|
if fixed != string(src) {
|
||||||
if err := os.WriteFile(path, []byte(fixed), 0o644); err != nil {
|
if err := os.WriteFile(path, []byte(fixed), 0o644); err != nil {
|
||||||
fmt.Fprintf(stderr, "pgtidy: writing %s: %v\n", path, err)
|
_, _ = fmt.Fprintf(stderr, "pgtidy: writing %s: %v\n", path, err)
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
// Re-check to report any remaining unfixed diagnostics.
|
// Re-check to report any remaining unfixed diagnostics.
|
||||||
diags, err = check(fixed, path)
|
diags, err = check(fixed, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(stderr, "pgtidy: %v\n", err)
|
_, _ = fmt.Fprintf(stderr, "pgtidy: %v\n", err)
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
printDiags(diags)
|
reportDiags(diags)
|
||||||
if len(diags) > 0 {
|
if len(diags) > 0 {
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if jsonOutput {
|
||||||
|
if jsonDiags == nil {
|
||||||
|
jsonDiags = []diagnostics.Diagnostic{}
|
||||||
|
}
|
||||||
|
enc := json.NewEncoder(stdout)
|
||||||
|
if err := enc.Encode(jsonDiags); err != nil {
|
||||||
|
_, _ = fmt.Fprintf(stderr, "pgtidy: encoding json: %v\n", err)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if found {
|
if found {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
@@ -144,12 +165,13 @@ func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func lintUsage(w io.Writer) {
|
func lintUsage(w io.Writer) {
|
||||||
fmt.Fprint(w, `Usage: pgtidy lint [flags] [file ...]
|
_, _ = fmt.Fprint(w, `Usage: pgtidy lint [flags] [file ...]
|
||||||
|
|
||||||
Read SQL from files (or stdin) and report lint findings.
|
Read SQL from files (or stdin) and report lint findings.
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
--fix Apply autofixes for fixable rules (MIG001, MIG003) and rewrite files
|
--fix Apply autofixes for fixable rules (MIG001, MIG003) and rewrite files
|
||||||
|
--json Emit findings as a JSON array instead of text (ignores --fix)
|
||||||
--only=ID,... comma-separated rule IDs to enable (default: all rules)
|
--only=ID,... comma-separated rule IDs to enable (default: all rules)
|
||||||
-h, --help show this help
|
-h, --help show this help
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@ import (
|
|||||||
func cmdLsp(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
func cmdLsp(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
||||||
for _, a := range args {
|
for _, a := range args {
|
||||||
if a == "-h" || a == "--help" {
|
if a == "-h" || a == "--help" {
|
||||||
fmt.Fprintln(stdout, "pgtidy lsp — start the Language Server Protocol server (stdio transport)")
|
_, _ = fmt.Fprintln(stdout, "pgtidy lsp — start the Language Server Protocol server (stdio transport)")
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ func cmdLsp(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
wd = "."
|
wd = "."
|
||||||
}
|
}
|
||||||
if err := lsp.Serve(context.Background(), stdin, stdout, wd); err != nil {
|
if err := lsp.Serve(context.Background(), stdin, stdout, wd); err != nil {
|
||||||
fmt.Fprintf(stderr, "pgtidy lsp: %v\n", err)
|
_, _ = fmt.Fprintf(stderr, "pgtidy lsp: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
+3
-3
@@ -29,20 +29,20 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
case "config":
|
case "config":
|
||||||
return cmdConfig(args[1:], stdin, stdout, stderr)
|
return cmdConfig(args[1:], stdin, stdout, stderr)
|
||||||
case "version", "--version", "-v":
|
case "version", "--version", "-v":
|
||||||
fmt.Fprintf(stdout, "pgtidy %s\n", version)
|
_, _ = fmt.Fprintf(stdout, "pgtidy %s\n", version)
|
||||||
return 0
|
return 0
|
||||||
case "help", "-h", "--help":
|
case "help", "-h", "--help":
|
||||||
usage(stdout)
|
usage(stdout)
|
||||||
return 0
|
return 0
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(stderr, "pgtidy: unknown command %q\n", args[0])
|
_, _ = fmt.Fprintf(stderr, "pgtidy: unknown command %q\n", args[0])
|
||||||
usage(stderr)
|
usage(stderr)
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func usage(w io.Writer) {
|
func usage(w io.Writer) {
|
||||||
fmt.Fprint(w, `pgtidy — PostgreSQL formatter and linter
|
_, _ = fmt.Fprint(w, `pgtidy — PostgreSQL formatter and linter
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
pgtidy fmt [flags] [files...] Format SQL/PL-pgSQL (stdin if no files)
|
pgtidy fmt [flags] [files...] Format SQL/PL-pgSQL (stdin if no files)
|
||||||
|
|||||||
@@ -1,26 +1,68 @@
|
|||||||
# PgTidy house style — all fields shown with their default values.
|
# PgTidy house style — all fields with their default values.
|
||||||
# Place as .pgtidy.yaml in your project root (or any parent directory).
|
# Place as .pgtidy.yaml in your project root (or any parent directory).
|
||||||
# Any field you omit keeps its default.
|
# Any field you omit keeps its default.
|
||||||
|
|
||||||
# Indentation string for one level (two spaces).
|
# --- Core ---
|
||||||
indent: " "
|
|
||||||
|
|
||||||
# Line terminator written by the formatter.
|
indent: " " # One indentation level (two spaces).
|
||||||
newline: "\n"
|
newline: "\n" # Line terminator emitted by the formatter.
|
||||||
|
|
||||||
# Casing for SQL keywords (SELECT, FROM, WHERE, …).
|
# --- Casing ---
|
||||||
# upper | lower | preserve
|
# upper | lower | preserve
|
||||||
keyword_case: upper
|
|
||||||
|
|
||||||
# Casing for unquoted identifiers (column names, variable names, …).
|
keyword_case: upper # SQL keywords (SELECT, FROM, WHERE, …)
|
||||||
# upper | lower | preserve
|
ident_case: lower # Unquoted identifiers (column/variable names)
|
||||||
ident_case: lower
|
type_case: lower # Built-in type names (text, integer, boolean, …)
|
||||||
|
alias_case: lower # Token immediately following AS in SELECT / FROM
|
||||||
|
builtin_case: lower # Built-in function names (COALESCE, MAX, NOW, …)
|
||||||
|
custom_type_case: lower # User-defined / domain types not in the built-in set
|
||||||
|
|
||||||
# Casing for built-in type names (text, integer, boolean, …).
|
# --- Query layout ---
|
||||||
# upper | lower | preserve
|
|
||||||
type_case: lower
|
|
||||||
|
|
||||||
# Comma placement in multi-line parameter / column lists.
|
commas: leading # leading → ,col | trailing → col,
|
||||||
# leading → comma at the start of the continuation line (,col)
|
|
||||||
# trailing → comma at the end of the preceding line (col,)
|
align_columns: false # Pad SELECT list items so values align vertically
|
||||||
commas: leading
|
align_line_comments: false # Align trailing -- comments within a block
|
||||||
|
select_align_as: false # Pad between expression and AS keyword in SELECT list
|
||||||
|
set_align_equal: false # Align = in UPDATE SET list
|
||||||
|
indent_join: false # Extra indentation for JOIN … ON lines
|
||||||
|
join_indent_size: 1 # Number of extra indent levels for JOINs
|
||||||
|
|
||||||
|
# always | when_long | never
|
||||||
|
where_wrap: always # Each AND/OR condition on its own line
|
||||||
|
|
||||||
|
where_and_or_indent: true # AND/OR indented one level under WHERE
|
||||||
|
|
||||||
|
# --- Subqueries ---
|
||||||
|
# same_line | new_line
|
||||||
|
|
||||||
|
subquery_opening: same_line # Opening ( placement
|
||||||
|
subquery_content: new_line # Content indentation inside parens
|
||||||
|
subquery_closing: new_line # Closing ) placement
|
||||||
|
subquery_space_before_paren: false # Space before ( in subqueries
|
||||||
|
|
||||||
|
# --- INSERT ---
|
||||||
|
|
||||||
|
insert_collapse_values: true # Fold multiple VALUES rows onto fewer lines
|
||||||
|
|
||||||
|
# --- Routines (functions / procedures) ---
|
||||||
|
|
||||||
|
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: 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
|
||||||
|
|
||||||
|
# --- Expressions ---
|
||||||
|
|
||||||
|
binary_op_align: false # Align =, <>, || etc. vertically in WHERE/expression lists
|
||||||
|
space_after_comma_in_calls: false # Space after , in function calls: func(a, b)
|
||||||
|
case_when_wrap: false # Each WHEN … THEN on its own line
|
||||||
|
case_end: new_line # END placement: same_line | new_line
|
||||||
|
case_collapse: false # Collapse short CASE expressions to one line
|
||||||
|
record_space_before_paren: false # Space before ( in ROW(…) / record constructors
|
||||||
|
|||||||
@@ -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`.
|
||||||
@@ -71,6 +71,89 @@ to the intended style below.
|
|||||||
`::`, `->`, `->>`, array `[...]`, or before a call's `(`.
|
`::`, `->`, `->>`, array `[...]`, or before a call's `(`.
|
||||||
- Dollar-quote tags preserved verbatim (`$$`, `$S$`, `$Z$`, …).
|
- Dollar-quote tags preserved verbatim (`$$`, `$S$`, `$Z$`, …).
|
||||||
|
|
||||||
|
## DataGrip settings mapping
|
||||||
|
|
||||||
|
`PostgresCodeStyleSettings` (DataGrip / JetBrains) is the reference for all configurable
|
||||||
|
style options. The table below maps every relevant DataGrip key to its `.pgtidy.yaml`
|
||||||
|
counterpart so a user can reproduce their DataGrip style exactly in PgTidy.
|
||||||
|
|
||||||
|
DataGrip enum conventions used below:
|
||||||
|
- **Case**: 0=preserve, 1=upper, 2=lower
|
||||||
|
- **Comma**: 1=leading (`,col`), 2=trailing (`col,`)
|
||||||
|
- **Placement**: 1=same_line, 2=new_line
|
||||||
|
- **Wrap**: 0=never, 1=when_long, 2=always
|
||||||
|
|
||||||
|
### Casing
|
||||||
|
|
||||||
|
| DataGrip key | PgTidy key | Default | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `KEYWORD_CASE` | `keyword_case` | `upper` | SELECT, FROM, WHERE, … |
|
||||||
|
| `IDENTIFIER_CASE` | `ident_case` | `lower` | unquoted column/variable names |
|
||||||
|
| `TYPE_CASE` | `type_case` | `lower` | built-in type names (text, integer, …) |
|
||||||
|
| `CUSTOM_TYPE_CASE` | `custom_type_case` | `lower` | user-defined / domain types |
|
||||||
|
| `ALIAS_CASE` | `alias_case` | `lower` | column and table aliases |
|
||||||
|
| `BUILT_IN_CASE` | `builtin_case` | `lower` | built-in functions (COALESCE, MAX, …) |
|
||||||
|
|
||||||
|
### Query layout
|
||||||
|
|
||||||
|
| DataGrip key | PgTidy key | Default | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `QUERY_EL_COMMA` | `commas` | `leading` | applies to all clause element lists |
|
||||||
|
| `QUERY_ALIGN_ELEMENTS` | `align_columns` | `false` | align SELECT list items to same column |
|
||||||
|
| `QUERY_ALIGN_LINE_COMMENTS` | `align_line_comments` | `false` | align `--` inline comments in a block |
|
||||||
|
| `SELECT_ALIGN_AS` | `select_align_as` | `false` | align `AS` keyword across SELECT list |
|
||||||
|
| `FROM_INDENT_JOIN` | `indent_join` | `false` | indent JOIN relative to FROM |
|
||||||
|
| `FROM_ONLY_JOIN_INDENT` | `join_indent_size` | `1` | extra indent levels for JOINs |
|
||||||
|
| `SET_ALIGN_EQUAL_SIGN` | `set_align_equal` | `false` | align `=` in UPDATE SET list |
|
||||||
|
| `WHERE_EL_WRAP` + `WHERE_EL_LINE` | `where_wrap` | `always` | always \| when_long \| never — each AND/OR condition on its own line |
|
||||||
|
| _(no DataGrip equivalent)_ | `where_and_or_indent` | `true` | when true, AND/OR are indented one level under WHERE, not at WHERE's column |
|
||||||
|
|
||||||
|
### Subqueries
|
||||||
|
|
||||||
|
| DataGrip key | PgTidy key | Default | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `SUBQUERY_OPENING` | `subquery_opening` | `same_line` | opening `(` placement |
|
||||||
|
| `SUBQUERY_CONTENT` | `subquery_content` | `new_line` | content indentation inside paren |
|
||||||
|
| `SUBQUERY_CLOSING` | `subquery_closing` | `new_line` | closing `)` placement |
|
||||||
|
| `SUBQUERY_PAR_SPACE_BEFORE` | `subquery_space_before_paren` | `false` | space before `(` |
|
||||||
|
|
||||||
|
### INSERT
|
||||||
|
|
||||||
|
| DataGrip key | PgTidy key | Default | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `INSERT_COLLAPSE_MULTI_ROW_VALUES` | `insert_collapse_values` | `true` | fold VALUES rows into fewer lines |
|
||||||
|
|
||||||
|
### Routine (function / procedure)
|
||||||
|
|
||||||
|
| DataGrip key | PgTidy key | Default | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `ROUTINE_ARG_COMMA` | uses `commas` | `leading` | same setting as query lists |
|
||||||
|
| `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
|
||||||
|
|
||||||
|
| 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` | `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 |
|
||||||
|
|
||||||
|
### Expressions
|
||||||
|
|
||||||
|
| DataGrip key | PgTidy key | Default | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `EXPR_BINARY_OP_ALIGN` | `binary_op_align` | `false` | align `=`, `<>`, `||`, … vertically in WHERE/expression lists; default false — must not be hardcoded |
|
||||||
|
| `EXPR_CALL_SPACE_AFTER_COMMA` | `space_after_comma_in_calls` | `false` | space after `,` in function calls |
|
||||||
|
| `EXPR_CASE_WHEN_WRAP` | `case_when_wrap` | `false` | each WHEN on its own line |
|
||||||
|
| `EXPR_CASE_END` | `case_end` | `new_line` | same_line \| new_line |
|
||||||
|
| `EXPR_CASE_COLLAPSE` | `case_collapse` | `false` | collapse short CASE to one line |
|
||||||
|
| `CORTEGE_SPACE_BEFORE_L_PAREN` | `record_space_before_paren` | `false` | space before `(` in ROW/record constructors |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Milestones
|
## Milestones
|
||||||
|
|
||||||
### V1 — Formatter + CLI (priority)
|
### V1 — Formatter + CLI (priority)
|
||||||
@@ -88,6 +171,8 @@ to the intended style below.
|
|||||||
5. **CLI** (`cmd/pgtidy fmt`): `--check`, `--write`/`-w`, stdin→stdout, `--diff`; config
|
5. **CLI** (`cmd/pgtidy fmt`): `--check`, `--write`/`-w`, stdin→stdout, `--diff`; config
|
||||||
discovery walking up to `.pgtidy.yaml`; CI-friendly exit codes.
|
discovery walking up to `.pgtidy.yaml`; CI-friendly exit codes.
|
||||||
6. **Config** (`pkg/config`): load/merge style config; defaults = house style above.
|
6. **Config** (`pkg/config`): load/merge style config; defaults = house style above.
|
||||||
|
Full field set defined in the DataGrip settings mapping section above — covers casing
|
||||||
|
(6 keys), query layout, subqueries, INSERT, routines, PL/pgSQL body, and expressions.
|
||||||
|
|
||||||
**Safety guarantees (tested):** semantic equivalence (re-lex output, compare non-trivia token
|
**Safety guarantees (tested):** semantic equivalence (re-lex output, compare non-trivia token
|
||||||
stream to input), and idempotence (`fmt(fmt(x)) == fmt(x)`). The corpus is the
|
stream to input), and idempotence (`fmt(fmt(x)) == fmt(x)`). The corpus is the
|
||||||
|
|||||||
@@ -93,6 +93,14 @@ Legend: ✅ done · 🚧 in progress · ⬜ not started
|
|||||||
- `fmt_test.go`: stdin, --check (un/formatted), -w idempotence, unknown-command.
|
- `fmt_test.go`: stdin, --check (un/formatted), -w idempotence, unknown-command.
|
||||||
- Safety invariants #2 (semantic equivalence), #3 (idempotence), #4 (graceful degradation)
|
- Safety invariants #2 (semantic equivalence), #3 (idempotence), #4 (graceful degradation)
|
||||||
are tested in `pkg/format` over the corpus.
|
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.
|
- `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.
|
- `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._
|
- _Range formatting: future._
|
||||||
|
- Full capability inventory, runtime-verified gaps, and ranked next steps in `docs/lsp-status.md` (issue #3).
|
||||||
|
|
||||||
## ✅ V4 — DataGrip
|
## ✅ V4 — DataGrip
|
||||||
- `editors/datagrip/`: Gradle-based JetBrains plugin targeting DataGrip 2024.3+ via LSP4IJ.
|
- `editors/datagrip/`: Gradle-based JetBrains plugin targeting DataGrip 2024.3+ via LSP4IJ.
|
||||||
@@ -161,6 +170,85 @@ Legend: ✅ done · 🚧 in progress · ⬜ not started
|
|||||||
- _Still TODO: Wadler Doc-IR printer for width-aware wrapping of long lines._
|
- _Still TODO: Wadler Doc-IR printer for width-aware wrapping of long lines._
|
||||||
- _Still TODO: LSP range formatting._
|
- _Still TODO: LSP range formatting._
|
||||||
|
|
||||||
|
## ✅ Config expansion — DataGrip settings parity
|
||||||
|
|
||||||
|
Reference: `PostgresCodeStyleSettings` mapping in `docs/plan.md`.
|
||||||
|
|
||||||
|
### ✅ Extended `pkg/config` fields
|
||||||
|
|
||||||
|
Added to `Style` struct, `yamlFile`, and `Load()` in `pkg/config/config.go`:
|
||||||
|
|
||||||
|
- New types: `WrapMode` (`always`|`when_long`|`never`), `Placement` (`same_line`|`new_line`)
|
||||||
|
- **Casing**: `AliasCase`, `BuiltinCase`, `CustomTypeCase` — all default `lower`
|
||||||
|
- **Query layout**: `AlignColumns`, `AlignLineComments`, `SelectAlignAs`, `SetAlignEqual`,
|
||||||
|
`IndentJoin`, `JoinIndentSize`, `WhereWrap`, `WhereAndOrIndent`
|
||||||
|
- **Subqueries**: `SubqueryOpening`, `SubqueryContent`, `SubqueryClosing`, `SubquerySpaceBeforeParen`
|
||||||
|
- **INSERT**: `InsertCollapseValues`
|
||||||
|
- **Routines**: `AlignParamTypes`, `RoutineAsWrap`
|
||||||
|
- **PL/pgSQL**: `PlpgsqlMaxBlankLines`, `PlpgsqlDeclareAlignType`, `PlpgsqlDeclareAlignEq`,
|
||||||
|
`PlpgsqlIfThenNewline`, `PlpgsqlLoopCollapse`
|
||||||
|
- **Expressions**: `BinaryOpAlign`, `SpaceAfterCommaInCalls`, `CaseWhenWrap`, `CaseEnd`,
|
||||||
|
`CaseCollapse`, `RecordSpaceBeforeParen`
|
||||||
|
- `docs/config/default.pgtidy.yaml` updated with all new keys and comments.
|
||||||
|
|
||||||
|
### ✅ Casing engine — alias and built-in classification
|
||||||
|
|
||||||
|
`pkg/format/keywords.go`: added `builtinFunctions` set (COALESCE, MAX, MIN, NOW, …).
|
||||||
|
`pkg/format/format.go`: `caseTextCtx` uses context — `prev` token and `nextIsLParen` flag
|
||||||
|
to route ident tokens through `AliasCase` (after AS) or `BuiltinCase` (before `(`).
|
||||||
|
`inline()` and `dmlInline()` pass context to `caseTextCtx`.
|
||||||
|
|
||||||
|
### ✅ Formatter — query layout settings (`pkg/format/dml.go`)
|
||||||
|
|
||||||
|
- `indent_join` + `join_indent_size`: JOIN clause indented by `JoinIndentSize × Indent`.
|
||||||
|
- `where_wrap` + `where_and_or_indent`: `dmlWhereClause` splits AND/OR conditions; `always`
|
||||||
|
puts each condition on its own line indented under WHERE; `never` keeps inline.
|
||||||
|
- `set_align_equal`: `dmlColListSet` pads LHS of SET items so `=` signs align.
|
||||||
|
- `align_columns` + `select_align_as`: `dmlColListSelect` + `alignSelectItems` pads
|
||||||
|
SELECT expressions so AS keywords and aliases align vertically.
|
||||||
|
- `space_after_comma_in_calls` applied in `dmlInline`.
|
||||||
|
- `binary_op_align` registered in config (enforcement in WHERE/expression context deferred).
|
||||||
|
|
||||||
|
### ⬜ Formatter — subquery formatting
|
||||||
|
|
||||||
|
`subquery_opening/content/closing/space_before_paren` fields are wired in config.
|
||||||
|
Enforcement in `dml.go` is not yet implemented — subqueries use current CTE formatting
|
||||||
|
as a proxy (new_line for content, inline for single-arg subexpressions).
|
||||||
|
|
||||||
|
### ⬜ Formatter — INSERT VALUES collapse
|
||||||
|
|
||||||
|
`insert_collapse_values` field is wired in config. Enforcement in `dml.go` not yet implemented.
|
||||||
|
|
||||||
|
### ✅ Formatter — routine param alignment (`pkg/format/format.go`)
|
||||||
|
|
||||||
|
- `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.
|
||||||
|
|
||||||
|
### ✅ Formatter — PL/pgSQL body settings (`pkg/format/body.go`)
|
||||||
|
|
||||||
|
- `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. 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
|
||||||
|
collapses them to one line.
|
||||||
|
- CRLF normalization in trivia emission (comment text, body trivia before DECLARE).
|
||||||
|
|
||||||
|
### ⬜ Formatter — expression settings (case_when_wrap, case_end, case_collapse, record_space_before_paren)
|
||||||
|
|
||||||
|
Config fields wired. Expression-level CASE/ROW formatting not yet implemented.
|
||||||
|
|
||||||
|
### ⬜ DataGrip XML import/export (optional, V4+)
|
||||||
|
|
||||||
|
`pgtidy config import --datagrip <settings.xml>` / `pgtidy config export --datagrip`
|
||||||
|
not implemented.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Open risks
|
## Open risks
|
||||||
- `go-pgquery` tracks PG17 (not PG18) — fine for lint; irrelevant to formatter path.
|
- `go-pgquery` tracks PG17 (not PG18) — fine for lint; irrelevant to formatter path.
|
||||||
- Leading-comma + one-per-line is a first-class style option, not an afterthought.
|
- Leading-comma + one-per-line is a first-class style option, not an afterthought.
|
||||||
|
|||||||
@@ -1,37 +1,27 @@
|
|||||||
# PgTidy — DataGrip / JetBrains Plugin
|
# PgTidy — DataGrip / JetBrains Plugin
|
||||||
|
|
||||||
Formats and lints SQL files in DataGrip (and any JetBrains IDE) via the LSP4IJ plugin.
|
Formats SQL files in DataGrip (and any JetBrains IDE) by shelling out to the `pgtidy` binary.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- `pgtidy` binary on `PATH` — download from [releases](https://git.warky.dev/wdevs/PgTidy/releases) or build with `go install git.warky.dev/wdevs/pgtidy/cmd/pgtidy@latest`
|
- `pgtidy` binary on `PATH` — download from [releases](https://git.warky.dev/wdevs/PgTidy/releases) or build with `go install git.warky.dev/wdevs/pgtidy/cmd/pgtidy@latest`
|
||||||
- [LSP4IJ](https://plugins.jetbrains.com/plugin/23257-lsp4ij) plugin installed (free, by Red Hat)
|
|
||||||
- DataGrip 2024.3+ (or any JetBrains IDE 2024.3+)
|
- DataGrip 2024.3+ (or any JetBrains IDE 2024.3+)
|
||||||
|
|
||||||
## Install the plugin
|
## Install the plugin
|
||||||
|
|
||||||
### Option A — Install from disk (`.zip`)
|
|
||||||
|
|
||||||
1. Build: `./gradlew buildPlugin` (output in `build/distributions/`)
|
1. Build: `./gradlew buildPlugin` (output in `build/distributions/`)
|
||||||
2. In DataGrip: **Settings → Plugins → ⚙ → Install Plugin from Disk…** → select the `.zip`
|
2. In DataGrip: **Settings → Plugins → ⚙ → Install Plugin from Disk…** → select the `.zip`
|
||||||
3. Restart the IDE
|
3. Restart the IDE
|
||||||
|
|
||||||
### Option B — Install LSP4IJ and configure manually (no plugin build needed)
|
|
||||||
|
|
||||||
1. Install **LSP4IJ** from the marketplace (**Settings → Plugins → Marketplace → search "LSP4IJ"**)
|
|
||||||
2. Go to **Settings → Language Servers → + (Add)**
|
|
||||||
3. Fill in:
|
|
||||||
- **Name:** `PgTidy`
|
|
||||||
- **Command:** `pgtidy lsp`
|
|
||||||
4. Under **Mappings**, add file patterns: `*.sql`, `*.pgsql`
|
|
||||||
5. Click **OK** and restart the IDE
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Formatting** — `Code → Reformat Code` (`Ctrl+Alt+L`) formats the current SQL file
|
- **Formatting** — `Format with PgTidy` (`Ctrl+Alt+Shift+P` or right-click in the editor) formats the current SQL file via `pgtidy fmt`
|
||||||
- **Diagnostics** — lint findings shown as inspections (MIG001–MIG003, COR001–003, NAM001–003)
|
- **Version check** — `Tools → Show PgTidy Version` and the status bar widget show the installed `pgtidy` version
|
||||||
|
- **Diagnostics** — lint findings (MIG001–MIG003, COR001–003, NAM001–003) are shown inline as editor annotations, via a native `ExternalAnnotator` that runs `pgtidy lint --json`
|
||||||
- **Quick fixes** — intention actions for MIG001 (add `CONCURRENTLY`) and MIG003 (add `NOT VALID`)
|
- **Quick fixes** — intention actions for MIG001 (add `CONCURRENTLY`) and MIG003 (add `NOT VALID`)
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
**Server not starting:** Check **View → Tool Windows → LSP4IJ Consoles** for stderr output. Most common cause: `pgtidy` not found on `PATH` — update the command to the full path, e.g. `/usr/local/bin/pgtidy lsp`.
|
**"Cannot start pgtidy" error:** the `pgtidy` binary isn't on `PATH` for the IDE process. Install it or restart the IDE after adding it to `PATH`.
|
||||||
|
|
||||||
|
**No diagnostics showing:** diagnostics only run on files with a `.sql` or `.pgsql` extension; check `pgtidy lint --json` runs cleanly on the file from a terminal with the same `PATH` as the IDE.
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ dependencies {
|
|||||||
val platformType = providers.gradleProperty("platformType")
|
val platformType = providers.gradleProperty("platformType")
|
||||||
create(platformType, platformVersion)
|
create(platformType, platformVersion)
|
||||||
bundledPlugin("com.intellij.database")
|
bundledPlugin("com.intellij.database")
|
||||||
plugin("com.redhat.devtools.lsp4ij:${providers.gradleProperty("lsp4ijVersion").get()}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,3 @@ pluginSinceBuild=243
|
|||||||
# DataGrip 2024.3
|
# DataGrip 2024.3
|
||||||
platformVersion=2024.3
|
platformVersion=2024.3
|
||||||
platformType=DB
|
platformType=DB
|
||||||
lsp4ijVersion=0.20.1
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.pgtidy.datagrip
|
||||||
|
|
||||||
|
data class PgTidyFix(
|
||||||
|
val offset: Int,
|
||||||
|
val end: Int,
|
||||||
|
val new: String,
|
||||||
|
val title: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PgTidyDiagnostic(
|
||||||
|
val ruleId: String,
|
||||||
|
val severity: String,
|
||||||
|
val message: String,
|
||||||
|
val file: String,
|
||||||
|
val line: Int,
|
||||||
|
val col: Int,
|
||||||
|
val fix: PgTidyFix?,
|
||||||
|
)
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.pgtidy.datagrip
|
||||||
|
|
||||||
|
import com.intellij.lang.annotation.AnnotationHolder
|
||||||
|
import com.intellij.lang.annotation.ExternalAnnotator
|
||||||
|
import com.intellij.lang.annotation.HighlightSeverity
|
||||||
|
import com.intellij.openapi.editor.Document
|
||||||
|
import com.intellij.psi.PsiFile
|
||||||
|
|
||||||
|
class PgTidyExternalAnnotator : ExternalAnnotator<String, List<PgTidyDiagnostic>>() {
|
||||||
|
|
||||||
|
override fun collectInformation(file: PsiFile): String? {
|
||||||
|
val ext = file.virtualFile?.extension?.lowercase()
|
||||||
|
if (ext != "sql" && ext != "pgsql") return null
|
||||||
|
return file.viewProvider.document?.text
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun doAnnotate(source: String): List<PgTidyDiagnostic> = PgTidyLintRunner.run(source)
|
||||||
|
|
||||||
|
override fun apply(file: PsiFile, diagnostics: List<PgTidyDiagnostic>, holder: AnnotationHolder) {
|
||||||
|
val document = file.viewProvider.document ?: return
|
||||||
|
for (d in diagnostics) {
|
||||||
|
if (d.line <= 0 || d.line > document.lineCount) continue
|
||||||
|
val range = lineTailRange(document, d.line, d.col) ?: continue
|
||||||
|
|
||||||
|
val severity = when (d.severity) {
|
||||||
|
"error" -> HighlightSeverity.ERROR
|
||||||
|
"warning" -> HighlightSeverity.WARNING
|
||||||
|
else -> HighlightSeverity.WEAK_WARNING
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = holder.newAnnotation(severity, "[${d.ruleId}] ${d.message}").range(range)
|
||||||
|
if (d.fix != null) {
|
||||||
|
builder.withFix(PgTidyQuickFix(d.fix))
|
||||||
|
}
|
||||||
|
builder.create()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Byte column [col] (1-based) to end-of-line, converted to a document TextRange. */
|
||||||
|
private fun lineTailRange(document: Document, line1Based: Int, col1Based: Int): com.intellij.openapi.util.TextRange? {
|
||||||
|
val lineIdx = line1Based - 1
|
||||||
|
val lineStart = document.getLineStartOffset(lineIdx)
|
||||||
|
val lineEnd = document.getLineEndOffset(lineIdx)
|
||||||
|
val lineText = document.getText(com.intellij.openapi.util.TextRange(lineStart, lineEnd))
|
||||||
|
val charCol = PgTidyOffsets.byteOffsetToCharIndex(lineText, col1Based - 1)
|
||||||
|
val start = lineStart + charCol
|
||||||
|
if (start >= lineEnd) return null
|
||||||
|
return com.intellij.openapi.util.TextRange(start, lineEnd)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.pgtidy.datagrip
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.reflect.TypeToken
|
||||||
|
|
||||||
|
object PgTidyLintRunner {
|
||||||
|
private val gson = Gson()
|
||||||
|
private val listType = TypeToken.getParameterized(List::class.java, PgTidyDiagnostic::class.java).type
|
||||||
|
|
||||||
|
/** Runs `pgtidy lint --json` over [source]. Returns an empty list if pgtidy is missing or output is unparseable. */
|
||||||
|
fun run(source: String): List<PgTidyDiagnostic> {
|
||||||
|
val proc = try {
|
||||||
|
ProcessBuilder("pgtidy", "lint", "--json")
|
||||||
|
.redirectErrorStream(false)
|
||||||
|
.start()
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
proc.outputStream.bufferedWriter().use { it.write(source) }
|
||||||
|
val output = proc.inputStream.bufferedReader().readText()
|
||||||
|
proc.errorStream.bufferedReader().readText()
|
||||||
|
proc.waitFor()
|
||||||
|
|
||||||
|
if (output.isBlank()) return emptyList()
|
||||||
|
return try {
|
||||||
|
gson.fromJson<List<PgTidyDiagnostic>>(output, listType) ?: emptyList()
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.pgtidy.datagrip
|
||||||
|
|
||||||
|
/**
|
||||||
|
* pgtidy reports positions as UTF-8 byte offsets (Go strings are byte slices);
|
||||||
|
* IntelliJ documents index text as UTF-16 chars. Converts one to the other.
|
||||||
|
*/
|
||||||
|
object PgTidyOffsets {
|
||||||
|
fun byteOffsetToCharIndex(text: String, byteOffset: Int): Int {
|
||||||
|
var bytes = 0
|
||||||
|
var i = 0
|
||||||
|
while (i < text.length) {
|
||||||
|
val cp = text.codePointAt(i)
|
||||||
|
val charCount = Character.charCount(cp)
|
||||||
|
val byteLen = utf8Length(cp)
|
||||||
|
if (bytes + byteLen > byteOffset) return i
|
||||||
|
bytes += byteLen
|
||||||
|
i += charCount
|
||||||
|
}
|
||||||
|
return text.length
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun utf8Length(codePoint: Int): Int = when {
|
||||||
|
codePoint <= 0x7F -> 1
|
||||||
|
codePoint <= 0x7FF -> 2
|
||||||
|
codePoint <= 0xFFFF -> 3
|
||||||
|
else -> 4
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.pgtidy.datagrip
|
||||||
|
|
||||||
|
import com.intellij.codeInsight.intention.IntentionAction
|
||||||
|
import com.intellij.openapi.editor.Editor
|
||||||
|
import com.intellij.openapi.project.Project
|
||||||
|
import com.intellij.psi.PsiFile
|
||||||
|
import com.intellij.util.IncorrectOperationException
|
||||||
|
|
||||||
|
class PgTidyQuickFix(private val fix: PgTidyFix) : IntentionAction {
|
||||||
|
|
||||||
|
override fun getText(): String = fix.title
|
||||||
|
override fun getFamilyName(): String = "PgTidy"
|
||||||
|
override fun startInWriteAction(): Boolean = true
|
||||||
|
|
||||||
|
override fun isAvailable(project: Project, editor: Editor?, file: PsiFile?): Boolean = editor != null
|
||||||
|
|
||||||
|
@Throws(IncorrectOperationException::class)
|
||||||
|
override fun invoke(project: Project, editor: Editor?, file: PsiFile?) {
|
||||||
|
val document = editor?.document ?: return
|
||||||
|
val text = document.text
|
||||||
|
val start = PgTidyOffsets.byteOffsetToCharIndex(text, fix.offset)
|
||||||
|
val end = PgTidyOffsets.byteOffsetToCharIndex(text, fix.end)
|
||||||
|
document.replaceString(start, end, fix.new)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.pgtidy.datagrip
|
|
||||||
|
|
||||||
import com.intellij.openapi.project.Project
|
|
||||||
import com.redhat.devtools.lsp4ij.server.ProcessStreamConnectionProvider
|
|
||||||
|
|
||||||
class PgTidyServerConnection(project: Project) : ProcessStreamConnectionProvider(
|
|
||||||
listOf("pgtidy", "lsp"),
|
|
||||||
project.basePath ?: System.getProperty("user.home"),
|
|
||||||
)
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.pgtidy.datagrip
|
|
||||||
|
|
||||||
import com.intellij.openapi.project.Project
|
|
||||||
import com.redhat.devtools.lsp4ij.LanguageServerFactory
|
|
||||||
import com.redhat.devtools.lsp4ij.server.StreamConnectionProvider
|
|
||||||
|
|
||||||
class PgTidyServerFactory : LanguageServerFactory {
|
|
||||||
override fun createConnectionProvider(project: Project): StreamConnectionProvider =
|
|
||||||
PgTidyServerConnection(project)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.pgtidy.datagrip
|
||||||
|
|
||||||
|
import com.intellij.openapi.actionSystem.AnAction
|
||||||
|
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||||
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
|
import com.intellij.openapi.progress.ProgressIndicator
|
||||||
|
import com.intellij.openapi.progress.Task
|
||||||
|
import com.intellij.openapi.ui.Messages
|
||||||
|
|
||||||
|
class PgTidyShowVersionAction : AnAction() {
|
||||||
|
|
||||||
|
override fun actionPerformed(e: AnActionEvent) {
|
||||||
|
val project = e.project
|
||||||
|
|
||||||
|
object : Task.Backgroundable(project, "PgTidy: checking version…", false) {
|
||||||
|
override fun run(indicator: ProgressIndicator) {
|
||||||
|
val proc = try {
|
||||||
|
ProcessBuilder("pgtidy", "version")
|
||||||
|
.redirectErrorStream(false)
|
||||||
|
.start()
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
ApplicationManager.getApplication().invokeLater {
|
||||||
|
Messages.showErrorDialog(project, "Cannot start pgtidy: ${ex.message}", "PgTidy")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val output = proc.inputStream.bufferedReader().readText()
|
||||||
|
val stderr = proc.errorStream.bufferedReader().readText()
|
||||||
|
val exit = proc.waitFor()
|
||||||
|
|
||||||
|
ApplicationManager.getApplication().invokeLater {
|
||||||
|
if (exit != 0) {
|
||||||
|
Messages.showErrorDialog(project, stderr.ifBlank { "pgtidy exited with code $exit" }, "PgTidy")
|
||||||
|
return@invokeLater
|
||||||
|
}
|
||||||
|
Messages.showInfoMessage(project, output.trim(), "PgTidy Version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.queue()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.pgtidy.datagrip
|
||||||
|
|
||||||
|
import com.intellij.openapi.application.ApplicationManager
|
||||||
|
import com.intellij.openapi.project.Project
|
||||||
|
import com.intellij.openapi.wm.StatusBar
|
||||||
|
import com.intellij.openapi.wm.StatusBarWidget
|
||||||
|
import com.intellij.openapi.wm.StatusBarWidgetFactory
|
||||||
|
import com.intellij.util.Consumer
|
||||||
|
import java.awt.event.MouseEvent
|
||||||
|
|
||||||
|
private const val WIDGET_ID = "com.pgtidy.StatusBarWidget"
|
||||||
|
|
||||||
|
class PgTidyStatusBarWidget(private val project: Project) : StatusBarWidget, StatusBarWidget.TextPresentation {
|
||||||
|
|
||||||
|
private var statusBar: StatusBar? = null
|
||||||
|
private var text: String = "pgtidy: …"
|
||||||
|
|
||||||
|
override fun ID(): String = WIDGET_ID
|
||||||
|
|
||||||
|
override fun install(statusBar: StatusBar) {
|
||||||
|
this.statusBar = statusBar
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dispose() {
|
||||||
|
statusBar = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getPresentation(): StatusBarWidget.WidgetPresentation = this
|
||||||
|
|
||||||
|
override fun getText(): String = text
|
||||||
|
|
||||||
|
override fun getAlignment(): Float = java.awt.Component.CENTER_ALIGNMENT
|
||||||
|
|
||||||
|
override fun getTooltipText(): String = "PgTidy version — click to refresh"
|
||||||
|
|
||||||
|
override fun getClickConsumer(): Consumer<MouseEvent> = Consumer { refresh() }
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
ApplicationManager.getApplication().executeOnPooledThread {
|
||||||
|
text = try {
|
||||||
|
val proc = ProcessBuilder("pgtidy", "version")
|
||||||
|
.redirectErrorStream(true)
|
||||||
|
.start()
|
||||||
|
val output = proc.inputStream.bufferedReader().readText().trim()
|
||||||
|
val exit = proc.waitFor()
|
||||||
|
if (exit == 0 && output.isNotBlank()) output else "pgtidy: not found"
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
"pgtidy: not found"
|
||||||
|
}
|
||||||
|
ApplicationManager.getApplication().invokeLater {
|
||||||
|
statusBar?.updateWidget(WIDGET_ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PgTidyStatusBarWidgetFactory : StatusBarWidgetFactory {
|
||||||
|
override fun getId(): String = WIDGET_ID
|
||||||
|
|
||||||
|
override fun getDisplayName(): String = "PgTidy Version"
|
||||||
|
|
||||||
|
override fun isAvailable(project: Project): Boolean = true
|
||||||
|
|
||||||
|
override fun createWidget(project: Project): StatusBarWidget = PgTidyStatusBarWidget(project)
|
||||||
|
|
||||||
|
override fun disposeWidget(widget: StatusBarWidget) {
|
||||||
|
widget.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun canBeEnabledOn(statusBar: StatusBar): Boolean = true
|
||||||
|
}
|
||||||
@@ -5,13 +5,11 @@
|
|||||||
<vendor url="https://git.warky.dev/wdevs/PgTidy">Warky Devs</vendor>
|
<vendor url="https://git.warky.dev/wdevs/PgTidy">Warky Devs</vendor>
|
||||||
<description><![CDATA[
|
<description><![CDATA[
|
||||||
PostgreSQL formatter and linter powered by <a href="https://github.com/hein/pgtidy">pgtidy</a>.<br/>
|
PostgreSQL formatter and linter powered by <a href="https://github.com/hein/pgtidy">pgtidy</a>.<br/>
|
||||||
Requires the <code>pgtidy</code> binary on PATH and the
|
Requires the <code>pgtidy</code> binary on PATH.
|
||||||
<a href="https://plugins.jetbrains.com/plugin/23257-lsp4ij">LSP4IJ</a> plugin.
|
|
||||||
]]></description>
|
]]></description>
|
||||||
|
|
||||||
<depends>com.intellij.modules.platform</depends>
|
<depends>com.intellij.modules.platform</depends>
|
||||||
<depends>com.intellij.database</depends>
|
<depends>com.intellij.database</depends>
|
||||||
<depends>com.redhat.devtools.lsp4ij</depends>
|
|
||||||
|
|
||||||
<actions>
|
<actions>
|
||||||
<action id="com.pgtidy.FormatDocument"
|
<action id="com.pgtidy.FormatDocument"
|
||||||
@@ -21,17 +19,19 @@
|
|||||||
<add-to-group group-id="EditorPopupMenu" anchor="first"/>
|
<add-to-group group-id="EditorPopupMenu" anchor="first"/>
|
||||||
<keyboard-shortcut keymap="$default" first-keystroke="ctrl alt shift P"/>
|
<keyboard-shortcut keymap="$default" first-keystroke="ctrl alt shift P"/>
|
||||||
</action>
|
</action>
|
||||||
|
<action id="com.pgtidy.ShowVersion"
|
||||||
|
class="com.pgtidy.datagrip.PgTidyShowVersionAction"
|
||||||
|
text="Show PgTidy Version"
|
||||||
|
description="Show the installed pgtidy binary version">
|
||||||
|
<add-to-group group-id="ToolsMenu" anchor="last"/>
|
||||||
|
</action>
|
||||||
</actions>
|
</actions>
|
||||||
|
|
||||||
<extensions defaultExtensionNs="com.redhat.devtools.lsp4ij">
|
<extensions defaultExtensionNs="com.intellij">
|
||||||
<server id="com.pgtidy.lsp"
|
<statusBarWidgetFactory id="com.pgtidy.StatusBarWidget"
|
||||||
name="PgTidy"
|
implementation="com.pgtidy.datagrip.PgTidyStatusBarWidgetFactory"
|
||||||
factoryClass="com.pgtidy.datagrip.PgTidyServerFactory">
|
order="last"/>
|
||||||
<description>PostgreSQL formatter and linter (pgtidy lsp)</description>
|
<externalAnnotator language="" implementationClass="com.pgtidy.datagrip.PgTidyExternalAnnotator"/>
|
||||||
</server>
|
|
||||||
|
|
||||||
<fileNamePatternMapping patterns="*.sql;*.pgsql"
|
|
||||||
serverId="com.pgtidy.lsp"
|
|
||||||
languageId="SQL"/>
|
|
||||||
</extensions>
|
</extensions>
|
||||||
|
|
||||||
</idea-plugin>
|
</idea-plugin>
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Maintainer: Hein (Warky Devs) <hein@warky.dev>
|
# Maintainer: Hein (Warky Devs) <hein@warky.dev>
|
||||||
pkgname=pgtidy-bin
|
pkgname=pgtidy-bin
|
||||||
pkgver=0.0.2
|
pkgver=0.0.8
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="PostgreSQL SQL formatter and linter"
|
pkgdesc="PostgreSQL SQL formatter and linter"
|
||||||
arch=('x86_64' 'aarch64')
|
arch=('x86_64' 'aarch64')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
Name: pgtidy
|
Name: pgtidy
|
||||||
Version: 0.0.2
|
Version: 0.0.8
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: PostgreSQL SQL formatter and linter
|
Summary: PostgreSQL SQL formatter and linter
|
||||||
|
|
||||||
|
|||||||
+270
-49
@@ -16,9 +16,8 @@ import (
|
|||||||
type Case string
|
type Case string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CaseUpper Case = "upper"
|
CaseUpper Case = "upper"
|
||||||
CaseLower Case = "lower"
|
CaseLower Case = "lower"
|
||||||
// CasePreserve leaves the token text unchanged.
|
|
||||||
CasePreserve Case = "preserve"
|
CasePreserve Case = "preserve"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,51 +25,174 @@ const (
|
|||||||
type CommaStyle string
|
type CommaStyle string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// CommaLeading puts the comma at the start of the continuation line
|
CommaLeading CommaStyle = "leading"
|
||||||
// (",col"), the house style.
|
|
||||||
CommaLeading CommaStyle = "leading"
|
|
||||||
// CommaTrailing puts the comma at the end of the preceding line ("col,").
|
|
||||||
CommaTrailing CommaStyle = "trailing"
|
CommaTrailing CommaStyle = "trailing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// WrapMode controls whether a construct wraps to multiple lines.
|
||||||
|
type WrapMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
WrapAlways WrapMode = "always"
|
||||||
|
WrapWhenLong WrapMode = "when_long"
|
||||||
|
WrapNever WrapMode = "never"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Placement controls where a token or block is placed relative to surrounding content.
|
||||||
|
type Placement string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PlacementSameLine Placement = "same_line"
|
||||||
|
PlacementNewLine Placement = "new_line"
|
||||||
|
)
|
||||||
|
|
||||||
// Style is the formatter configuration.
|
// Style is the formatter configuration.
|
||||||
type Style struct {
|
type Style struct {
|
||||||
// Indent is one indentation level (default two spaces).
|
// --- Core ---
|
||||||
Indent string
|
Indent string
|
||||||
// Newline is the line terminator emitted by the formatter.
|
|
||||||
Newline string
|
Newline string
|
||||||
// KeywordCase controls SQL keyword casing (types excluded — see TypeCase).
|
|
||||||
KeywordCase Case
|
// --- Casing ---
|
||||||
// IdentCase controls unquoted identifier casing (quoted identifiers are
|
KeywordCase Case
|
||||||
// never touched).
|
IdentCase Case
|
||||||
IdentCase Case
|
TypeCase Case
|
||||||
// TypeCase controls built-in type-name casing.
|
AliasCase Case // token immediately following AS in SELECT/FROM
|
||||||
TypeCase Case
|
BuiltinCase Case // built-in function names (COALESCE, MAX, NOW, …)
|
||||||
// Commas controls list separator placement.
|
CustomTypeCase Case // user-defined / domain types not in the built-in set
|
||||||
Commas CommaStyle
|
|
||||||
|
// --- Query layout ---
|
||||||
|
Commas CommaStyle
|
||||||
|
AlignColumns bool // pad SELECT list so values align
|
||||||
|
AlignLineComments bool // align trailing -- comments in a block
|
||||||
|
SelectAlignAs bool // pad between expression and AS in SELECT list
|
||||||
|
SetAlignEqual bool // align = in UPDATE SET list
|
||||||
|
IndentJoin bool // extra indentation for JOIN … ON lines
|
||||||
|
JoinIndentSize int // extra indent levels for JOINs (default 1)
|
||||||
|
WhereWrap WrapMode // always|when_long|never — each AND/OR on its own line
|
||||||
|
WhereAndOrIndent bool // AND/OR indented one level under WHERE
|
||||||
|
|
||||||
|
// --- Subqueries ---
|
||||||
|
SubqueryOpening Placement // opening ( placement: same_line|new_line
|
||||||
|
SubqueryContent Placement // content indentation: same_line|new_line
|
||||||
|
SubqueryClosing Placement // closing ) placement: same_line|new_line
|
||||||
|
SubquerySpaceBeforeParen bool // space before ( in subqueries
|
||||||
|
|
||||||
|
// --- INSERT ---
|
||||||
|
InsertCollapseValues bool // fold multiple VALUES rows onto fewer lines
|
||||||
|
|
||||||
|
// --- Routines ---
|
||||||
|
AlignParamTypes bool // pad param names so type column aligns
|
||||||
|
RoutineAsWrap bool // newline before AS $$
|
||||||
|
|
||||||
|
// --- PL/pgSQL body ---
|
||||||
|
PlpgsqlMaxBlankLines int // max consecutive blank lines in body
|
||||||
|
PlpgsqlDeclareAlignType bool // align type column in DECLARE block
|
||||||
|
PlpgsqlDeclareAlignEq bool // align := / = in DECLARE block
|
||||||
|
PlpgsqlIfThenNewline bool // THEN on its own line
|
||||||
|
PlpgsqlLoopCollapse bool // collapse empty loop bodies to one line
|
||||||
|
|
||||||
|
// --- Expressions ---
|
||||||
|
BinaryOpAlign bool // align =, <>, || etc. vertically in WHERE/expr lists
|
||||||
|
SpaceAfterCommaInCalls bool // space after , in function calls: func(a, b)
|
||||||
|
CaseWhenWrap bool // each WHEN … THEN on its own line
|
||||||
|
CaseEnd Placement // END placement: same_line|new_line
|
||||||
|
CaseCollapse bool // collapse short CASE to one line
|
||||||
|
RecordSpaceBeforeParen bool // space before ( in ROW(…) / record constructors
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default returns the house-style configuration.
|
// Default returns the house-style configuration.
|
||||||
func Default() Style {
|
func Default() Style {
|
||||||
return Style{
|
return Style{
|
||||||
Indent: " ",
|
Indent: " ",
|
||||||
Newline: "\n",
|
Newline: "\n",
|
||||||
KeywordCase: CaseUpper,
|
|
||||||
IdentCase: CaseLower,
|
KeywordCase: CaseUpper,
|
||||||
TypeCase: CaseLower,
|
IdentCase: CaseLower,
|
||||||
Commas: CommaLeading,
|
TypeCase: CaseLower,
|
||||||
|
AliasCase: CaseLower,
|
||||||
|
BuiltinCase: CaseLower,
|
||||||
|
CustomTypeCase: CaseLower,
|
||||||
|
|
||||||
|
Commas: CommaLeading,
|
||||||
|
AlignColumns: false,
|
||||||
|
AlignLineComments: false,
|
||||||
|
SelectAlignAs: false,
|
||||||
|
SetAlignEqual: false,
|
||||||
|
IndentJoin: false,
|
||||||
|
JoinIndentSize: 1,
|
||||||
|
WhereWrap: WrapAlways,
|
||||||
|
WhereAndOrIndent: true,
|
||||||
|
|
||||||
|
SubqueryOpening: PlacementSameLine,
|
||||||
|
SubqueryContent: PlacementNewLine,
|
||||||
|
SubqueryClosing: PlacementNewLine,
|
||||||
|
SubquerySpaceBeforeParen: false,
|
||||||
|
|
||||||
|
InsertCollapseValues: true,
|
||||||
|
|
||||||
|
AlignParamTypes: false,
|
||||||
|
RoutineAsWrap: true,
|
||||||
|
|
||||||
|
PlpgsqlMaxBlankLines: 1,
|
||||||
|
PlpgsqlDeclareAlignType: true,
|
||||||
|
PlpgsqlDeclareAlignEq: true,
|
||||||
|
PlpgsqlIfThenNewline: true,
|
||||||
|
PlpgsqlLoopCollapse: true,
|
||||||
|
|
||||||
|
BinaryOpAlign: false,
|
||||||
|
SpaceAfterCommaInCalls: false,
|
||||||
|
CaseWhenWrap: false,
|
||||||
|
CaseEnd: PlacementNewLine,
|
||||||
|
CaseCollapse: false,
|
||||||
|
RecordSpaceBeforeParen: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// yamlFile is the on-disk representation of .pgtidy.yaml.
|
// yamlFile is the on-disk representation of .pgtidy.yaml.
|
||||||
// All fields are pointers so we can distinguish "not set" from "set to zero value".
|
// All fields are pointers so we can distinguish "not set" from "set to zero value".
|
||||||
type yamlFile struct {
|
type yamlFile struct {
|
||||||
Indent *string `yaml:"indent"`
|
Indent *string `yaml:"indent"`
|
||||||
Newline *string `yaml:"newline"`
|
Newline *string `yaml:"newline"`
|
||||||
KeywordCase *string `yaml:"keyword_case"`
|
|
||||||
IdentCase *string `yaml:"ident_case"`
|
KeywordCase *string `yaml:"keyword_case"`
|
||||||
TypeCase *string `yaml:"type_case"`
|
IdentCase *string `yaml:"ident_case"`
|
||||||
Commas *string `yaml:"commas"`
|
TypeCase *string `yaml:"type_case"`
|
||||||
|
AliasCase *string `yaml:"alias_case"`
|
||||||
|
BuiltinCase *string `yaml:"builtin_case"`
|
||||||
|
CustomTypeCase *string `yaml:"custom_type_case"`
|
||||||
|
|
||||||
|
Commas *string `yaml:"commas"`
|
||||||
|
AlignColumns *bool `yaml:"align_columns"`
|
||||||
|
AlignLineComments *bool `yaml:"align_line_comments"`
|
||||||
|
SelectAlignAs *bool `yaml:"select_align_as"`
|
||||||
|
SetAlignEqual *bool `yaml:"set_align_equal"`
|
||||||
|
IndentJoin *bool `yaml:"indent_join"`
|
||||||
|
JoinIndentSize *int `yaml:"join_indent_size"`
|
||||||
|
WhereWrap *string `yaml:"where_wrap"`
|
||||||
|
WhereAndOrIndent *bool `yaml:"where_and_or_indent"`
|
||||||
|
|
||||||
|
SubqueryOpening *string `yaml:"subquery_opening"`
|
||||||
|
SubqueryContent *string `yaml:"subquery_content"`
|
||||||
|
SubqueryClosing *string `yaml:"subquery_closing"`
|
||||||
|
SubquerySpaceBeforeParen *bool `yaml:"subquery_space_before_paren"`
|
||||||
|
|
||||||
|
InsertCollapseValues *bool `yaml:"insert_collapse_values"`
|
||||||
|
|
||||||
|
AlignParamTypes *bool `yaml:"align_param_types"`
|
||||||
|
RoutineAsWrap *bool `yaml:"routine_as_wrap"`
|
||||||
|
|
||||||
|
PlpgsqlMaxBlankLines *int `yaml:"plpgsql_max_blank_lines"`
|
||||||
|
PlpgsqlDeclareAlignType *bool `yaml:"plpgsql_declare_align_type"`
|
||||||
|
PlpgsqlDeclareAlignEq *bool `yaml:"plpgsql_declare_align_eq"`
|
||||||
|
PlpgsqlIfThenNewline *bool `yaml:"plpgsql_if_then_newline"`
|
||||||
|
PlpgsqlLoopCollapse *bool `yaml:"plpgsql_loop_collapse"`
|
||||||
|
|
||||||
|
BinaryOpAlign *bool `yaml:"binary_op_align"`
|
||||||
|
SpaceAfterCommaInCalls *bool `yaml:"space_after_comma_in_calls"`
|
||||||
|
CaseWhenWrap *bool `yaml:"case_when_wrap"`
|
||||||
|
CaseEnd *string `yaml:"case_end"`
|
||||||
|
CaseCollapse *bool `yaml:"case_collapse"`
|
||||||
|
RecordSpaceBeforeParen *bool `yaml:"record_space_before_paren"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load discovers and parses the nearest .pgtidy.yaml by walking up from
|
// Load discovers and parses the nearest .pgtidy.yaml by walking up from
|
||||||
@@ -100,27 +222,26 @@ func Load(startDir string) (Style, error) {
|
|||||||
if yf.Newline != nil {
|
if yf.Newline != nil {
|
||||||
st.Newline = *yf.Newline
|
st.Newline = *yf.Newline
|
||||||
}
|
}
|
||||||
if yf.KeywordCase != nil {
|
|
||||||
c := Case(*yf.KeywordCase)
|
if err := loadCase(yf.KeywordCase, &st.KeywordCase, path, "keyword_case"); err != nil {
|
||||||
if err := validCase(c); err != nil {
|
return st, err
|
||||||
return st, fmt.Errorf("pgtidy: %s: keyword_case: %w", path, err)
|
|
||||||
}
|
|
||||||
st.KeywordCase = c
|
|
||||||
}
|
}
|
||||||
if yf.IdentCase != nil {
|
if err := loadCase(yf.IdentCase, &st.IdentCase, path, "ident_case"); err != nil {
|
||||||
c := Case(*yf.IdentCase)
|
return st, err
|
||||||
if err := validCase(c); err != nil {
|
|
||||||
return st, fmt.Errorf("pgtidy: %s: ident_case: %w", path, err)
|
|
||||||
}
|
|
||||||
st.IdentCase = c
|
|
||||||
}
|
}
|
||||||
if yf.TypeCase != nil {
|
if err := loadCase(yf.TypeCase, &st.TypeCase, path, "type_case"); err != nil {
|
||||||
c := Case(*yf.TypeCase)
|
return st, err
|
||||||
if err := validCase(c); err != nil {
|
|
||||||
return st, fmt.Errorf("pgtidy: %s: type_case: %w", path, err)
|
|
||||||
}
|
|
||||||
st.TypeCase = c
|
|
||||||
}
|
}
|
||||||
|
if err := loadCase(yf.AliasCase, &st.AliasCase, path, "alias_case"); err != nil {
|
||||||
|
return st, err
|
||||||
|
}
|
||||||
|
if err := loadCase(yf.BuiltinCase, &st.BuiltinCase, path, "builtin_case"); err != nil {
|
||||||
|
return st, err
|
||||||
|
}
|
||||||
|
if err := loadCase(yf.CustomTypeCase, &st.CustomTypeCase, path, "custom_type_case"); err != nil {
|
||||||
|
return st, err
|
||||||
|
}
|
||||||
|
|
||||||
if yf.Commas != nil {
|
if yf.Commas != nil {
|
||||||
cs := CommaStyle(*yf.Commas)
|
cs := CommaStyle(*yf.Commas)
|
||||||
if cs != CommaLeading && cs != CommaTrailing {
|
if cs != CommaLeading && cs != CommaTrailing {
|
||||||
@@ -129,9 +250,93 @@ func Load(startDir string) (Style, error) {
|
|||||||
st.Commas = cs
|
st.Commas = cs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadBool(yf.AlignColumns, &st.AlignColumns)
|
||||||
|
loadBool(yf.AlignLineComments, &st.AlignLineComments)
|
||||||
|
loadBool(yf.SelectAlignAs, &st.SelectAlignAs)
|
||||||
|
loadBool(yf.SetAlignEqual, &st.SetAlignEqual)
|
||||||
|
loadBool(yf.IndentJoin, &st.IndentJoin)
|
||||||
|
if yf.JoinIndentSize != nil {
|
||||||
|
st.JoinIndentSize = *yf.JoinIndentSize
|
||||||
|
}
|
||||||
|
if yf.WhereWrap != nil {
|
||||||
|
wm := WrapMode(*yf.WhereWrap)
|
||||||
|
if err := validWrap(wm); err != nil {
|
||||||
|
return st, fmt.Errorf("pgtidy: %s: where_wrap: %w", path, err)
|
||||||
|
}
|
||||||
|
st.WhereWrap = wm
|
||||||
|
}
|
||||||
|
loadBool(yf.WhereAndOrIndent, &st.WhereAndOrIndent)
|
||||||
|
|
||||||
|
if yf.SubqueryOpening != nil {
|
||||||
|
pl := Placement(*yf.SubqueryOpening)
|
||||||
|
if err := validPlacement(pl); err != nil {
|
||||||
|
return st, fmt.Errorf("pgtidy: %s: subquery_opening: %w", path, err)
|
||||||
|
}
|
||||||
|
st.SubqueryOpening = pl
|
||||||
|
}
|
||||||
|
if yf.SubqueryContent != nil {
|
||||||
|
pl := Placement(*yf.SubqueryContent)
|
||||||
|
if err := validPlacement(pl); err != nil {
|
||||||
|
return st, fmt.Errorf("pgtidy: %s: subquery_content: %w", path, err)
|
||||||
|
}
|
||||||
|
st.SubqueryContent = pl
|
||||||
|
}
|
||||||
|
if yf.SubqueryClosing != nil {
|
||||||
|
pl := Placement(*yf.SubqueryClosing)
|
||||||
|
if err := validPlacement(pl); err != nil {
|
||||||
|
return st, fmt.Errorf("pgtidy: %s: subquery_closing: %w", path, err)
|
||||||
|
}
|
||||||
|
st.SubqueryClosing = pl
|
||||||
|
}
|
||||||
|
loadBool(yf.SubquerySpaceBeforeParen, &st.SubquerySpaceBeforeParen)
|
||||||
|
|
||||||
|
loadBool(yf.InsertCollapseValues, &st.InsertCollapseValues)
|
||||||
|
|
||||||
|
loadBool(yf.AlignParamTypes, &st.AlignParamTypes)
|
||||||
|
loadBool(yf.RoutineAsWrap, &st.RoutineAsWrap)
|
||||||
|
|
||||||
|
if yf.PlpgsqlMaxBlankLines != nil {
|
||||||
|
st.PlpgsqlMaxBlankLines = *yf.PlpgsqlMaxBlankLines
|
||||||
|
}
|
||||||
|
loadBool(yf.PlpgsqlDeclareAlignType, &st.PlpgsqlDeclareAlignType)
|
||||||
|
loadBool(yf.PlpgsqlDeclareAlignEq, &st.PlpgsqlDeclareAlignEq)
|
||||||
|
loadBool(yf.PlpgsqlIfThenNewline, &st.PlpgsqlIfThenNewline)
|
||||||
|
loadBool(yf.PlpgsqlLoopCollapse, &st.PlpgsqlLoopCollapse)
|
||||||
|
|
||||||
|
loadBool(yf.BinaryOpAlign, &st.BinaryOpAlign)
|
||||||
|
loadBool(yf.SpaceAfterCommaInCalls, &st.SpaceAfterCommaInCalls)
|
||||||
|
loadBool(yf.CaseWhenWrap, &st.CaseWhenWrap)
|
||||||
|
if yf.CaseEnd != nil {
|
||||||
|
pl := Placement(*yf.CaseEnd)
|
||||||
|
if err := validPlacement(pl); err != nil {
|
||||||
|
return st, fmt.Errorf("pgtidy: %s: case_end: %w", path, err)
|
||||||
|
}
|
||||||
|
st.CaseEnd = pl
|
||||||
|
}
|
||||||
|
loadBool(yf.CaseCollapse, &st.CaseCollapse)
|
||||||
|
loadBool(yf.RecordSpaceBeforeParen, &st.RecordSpaceBeforeParen)
|
||||||
|
|
||||||
return st, nil
|
return st, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadCase(src *string, dst *Case, path, key string) error {
|
||||||
|
if src == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c := Case(*src)
|
||||||
|
if err := validCase(c); err != nil {
|
||||||
|
return fmt.Errorf("pgtidy: %s: %s: %w", path, key, err)
|
||||||
|
}
|
||||||
|
*dst = c
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadBool(src *bool, dst *bool) {
|
||||||
|
if src != nil {
|
||||||
|
*dst = *src
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// findConfig walks parent directories from startDir looking for .pgtidy.yaml.
|
// findConfig walks parent directories from startDir looking for .pgtidy.yaml.
|
||||||
// Returns ("", nil) when no file is found before reaching the filesystem root.
|
// Returns ("", nil) when no file is found before reaching the filesystem root.
|
||||||
func findConfig(startDir string) (string, error) {
|
func findConfig(startDir string) (string, error) {
|
||||||
@@ -159,3 +364,19 @@ func validCase(c Case) error {
|
|||||||
}
|
}
|
||||||
return fmt.Errorf("must be \"upper\", \"lower\", or \"preserve\"")
|
return fmt.Errorf("must be \"upper\", \"lower\", or \"preserve\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validWrap(w WrapMode) error {
|
||||||
|
switch w {
|
||||||
|
case WrapAlways, WrapWhenLong, WrapNever:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("must be \"always\", \"when_long\", or \"never\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
func validPlacement(p Placement) error {
|
||||||
|
switch p {
|
||||||
|
case PlacementSameLine, PlacementNewLine:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("must be \"same_line\" or \"new_line\"")
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,27 +15,27 @@ const (
|
|||||||
// TextFix is a byte-range replacement that can be applied to the source SQL.
|
// TextFix is a byte-range replacement that can be applied to the source SQL.
|
||||||
// Replace src[Offset:End] with New. An insertion has Offset == End.
|
// Replace src[Offset:End] with New. An insertion has Offset == End.
|
||||||
type TextFix struct {
|
type TextFix struct {
|
||||||
Offset int // byte offset in source (inclusive)
|
Offset int `json:"offset"` // byte offset in source (inclusive)
|
||||||
End int // byte offset in source (exclusive)
|
End int `json:"end"` // byte offset in source (exclusive)
|
||||||
New string // replacement text
|
New string `json:"new"` // replacement text
|
||||||
Title string // short description shown in editor UI
|
Title string `json:"title"` // short description shown in editor UI
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diagnostic is a single lint finding.
|
// Diagnostic is a single lint finding.
|
||||||
type Diagnostic struct {
|
type Diagnostic struct {
|
||||||
// RuleID is the stable identifier for the rule that produced this finding
|
// RuleID is the stable identifier for the rule that produced this finding
|
||||||
// (e.g. "MIG001").
|
// (e.g. "MIG001").
|
||||||
RuleID string
|
RuleID string `json:"ruleId"`
|
||||||
// Severity is the urgency level.
|
// Severity is the urgency level.
|
||||||
Severity Severity
|
Severity Severity `json:"severity"`
|
||||||
// Message is a human-readable description of the finding.
|
// Message is a human-readable description of the finding.
|
||||||
Message string
|
Message string `json:"message"`
|
||||||
// File is the path to the source file, or "" for stdin.
|
// File is the path to the source file, or "" for stdin.
|
||||||
File string
|
File string `json:"file"`
|
||||||
// Line is the 1-based line number of the finding.
|
// Line is the 1-based line number of the finding.
|
||||||
Line int
|
Line int `json:"line"`
|
||||||
// Col is the 1-based column number of the finding.
|
// Col is the 1-based column number of the finding.
|
||||||
Col int
|
Col int `json:"col"`
|
||||||
// Fix is non-nil when an autofix is available for this diagnostic.
|
// Fix is non-nil when an autofix is available for this diagnostic.
|
||||||
Fix *TextFix
|
Fix *TextFix `json:"fix,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
+523
-62
@@ -92,10 +92,11 @@ func formatBodyInner(inner string, st config.Style) string {
|
|||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
// Emit verbatim up to and including DECLARE (keyword-cased).
|
// Emit verbatim up to and including DECLARE (keyword-cased).
|
||||||
|
// Normalize CRLF in trivia so the output always uses st.Newline.
|
||||||
for i := 0; i <= declareIdx; i++ {
|
for i := 0; i <= declareIdx; i++ {
|
||||||
t := sig[i]
|
t := sig[i]
|
||||||
for _, tr := range t.Lead {
|
for _, tr := range t.Lead {
|
||||||
b.WriteString(tr.Text)
|
b.WriteString(strings.ReplaceAll(tr.Text, "\r\n", nl))
|
||||||
}
|
}
|
||||||
if i == declareIdx {
|
if i == declareIdx {
|
||||||
b.WriteString(applyCase(t.Tok.Text, st.KeywordCase))
|
b.WriteString(applyCase(t.Tok.Text, st.KeywordCase))
|
||||||
@@ -122,57 +123,30 @@ func formatDeclareVars(b *strings.Builder, toks []cst.Tok, st config.Style) {
|
|||||||
nl := st.Newline
|
nl := st.Newline
|
||||||
indent := st.Indent
|
indent := st.Indent
|
||||||
depth := 0
|
depth := 0
|
||||||
|
var decls [][]cst.Tok
|
||||||
var cur []cst.Tok
|
var cur []cst.Tok
|
||||||
var preComments []string
|
var preCommentSets [][]string
|
||||||
|
var curPreComments []string
|
||||||
|
|
||||||
emit := func() {
|
collect := func() {
|
||||||
if len(cur) == 0 {
|
if len(cur) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, c := range preComments {
|
decls = append(decls, cur)
|
||||||
b.WriteString(indent)
|
preCommentSets = append(preCommentSets, curPreComments)
|
||||||
b.WriteString(c)
|
|
||||||
b.WriteString(nl)
|
|
||||||
}
|
|
||||||
preComments = nil
|
|
||||||
|
|
||||||
// Graceful degradation: mid-declaration comments stay verbatim.
|
|
||||||
if anyComment(cur[1:]) {
|
|
||||||
b.WriteString(indent)
|
|
||||||
b.WriteString(verbatimSpan(cur))
|
|
||||||
b.WriteString(nl)
|
|
||||||
cur = nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
body := cur
|
|
||||||
hasSemi := len(body) > 0 && body[len(body)-1].Tok.Kind == lexer.Semicolon
|
|
||||||
if hasSemi {
|
|
||||||
body = body[:len(body)-1]
|
|
||||||
}
|
|
||||||
b.WriteString(indent)
|
|
||||||
for i, t := range body {
|
|
||||||
if i > 0 && needSpace(body[i-1].Tok, t.Tok) {
|
|
||||||
b.WriteByte(' ')
|
|
||||||
}
|
|
||||||
b.WriteString(caseText(t.Tok, st))
|
|
||||||
}
|
|
||||||
if hasSemi {
|
|
||||||
b.WriteString(";")
|
|
||||||
}
|
|
||||||
b.WriteString(nl)
|
|
||||||
cur = nil
|
cur = nil
|
||||||
|
curPreComments = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, t := range toks {
|
for _, t := range toks {
|
||||||
if len(cur) == 0 {
|
if len(cur) == 0 {
|
||||||
for _, tr := range t.Lead {
|
for _, tr := range t.Lead {
|
||||||
if tr.Kind == lexer.LineComment || tr.Kind == lexer.BlockComment {
|
if tr.Kind == lexer.LineComment || tr.Kind == lexer.BlockComment {
|
||||||
preComments = append(preComments, strings.TrimRight(tr.Text, " \t"))
|
text := strings.TrimRight(strings.ReplaceAll(tr.Text, "\r", ""), " \t")
|
||||||
|
curPreComments = append(curPreComments, text)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch t.Tok.Kind {
|
switch t.Tok.Kind {
|
||||||
case lexer.LParen, lexer.LBracket:
|
case lexer.LParen, lexer.LBracket:
|
||||||
depth++
|
depth++
|
||||||
@@ -181,14 +155,224 @@ func formatDeclareVars(b *strings.Builder, toks []cst.Tok, st config.Style) {
|
|||||||
depth--
|
depth--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cur = append(cur, t)
|
cur = append(cur, t)
|
||||||
|
|
||||||
if t.Tok.Kind == lexer.Semicolon && depth == 0 {
|
if t.Tok.Kind == lexer.Semicolon && depth == 0 {
|
||||||
emit()
|
collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collect()
|
||||||
|
|
||||||
|
// Compute alignment widths when requested.
|
||||||
|
var nameColW, typeColW int
|
||||||
|
if st.PlpgsqlDeclareAlignType || st.PlpgsqlDeclareAlignEq {
|
||||||
|
for _, decl := range decls {
|
||||||
|
if anyComment(decl[1:]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
body := decl
|
||||||
|
if len(body) > 0 && body[len(body)-1].Tok.Kind == lexer.Semicolon {
|
||||||
|
body = body[:len(body)-1]
|
||||||
|
}
|
||||||
|
nw, tw := declareNameTypeWidth(body, st)
|
||||||
|
if nw > nameColW {
|
||||||
|
nameColW = nw
|
||||||
|
}
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, cur := range decls {
|
||||||
|
for _, c := range preCommentSets[i] {
|
||||||
|
b.WriteString(indent)
|
||||||
|
b.WriteString(c)
|
||||||
|
b.WriteString(nl)
|
||||||
|
}
|
||||||
|
// Graceful degradation: mid-declaration comments stay verbatim.
|
||||||
|
if anyComment(cur[1:]) {
|
||||||
|
b.WriteString(indent)
|
||||||
|
b.WriteString(verbatimSpan(cur))
|
||||||
|
b.WriteString(nl)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
body := cur
|
||||||
|
hasSemi := len(body) > 0 && body[len(body)-1].Tok.Kind == lexer.Semicolon
|
||||||
|
if hasSemi {
|
||||||
|
body = body[:len(body)-1]
|
||||||
|
}
|
||||||
|
b.WriteString(indent)
|
||||||
|
if (st.PlpgsqlDeclareAlignType || st.PlpgsqlDeclareAlignEq) && nameColW > 0 {
|
||||||
|
writeDeclareAligned(b, body, st, nameColW, typeColW)
|
||||||
|
} else {
|
||||||
|
for j, t := range body {
|
||||||
|
if j > 0 && needSpace(body[j-1].Tok, t.Tok) && !isPctTypeBoundary(body, j) {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
}
|
||||||
|
b.WriteString(caseText(t.Tok, st))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasSemi {
|
||||||
|
b.WriteString(";")
|
||||||
|
}
|
||||||
|
b.WriteString(nl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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]].
|
||||||
|
func declareNameTypeWidth(body []cst.Tok, st config.Style) (nameW, typeW int) {
|
||||||
|
if len(body) < 2 {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
// name is always the first token.
|
||||||
|
name := caseText(body[0].Tok, st)
|
||||||
|
nameW = len(name)
|
||||||
|
|
||||||
|
// type runs from body[1] until we hit := / DEFAULT / = at depth 0.
|
||||||
|
var typeTokens []cst.Tok
|
||||||
|
depth := 0
|
||||||
|
for _, t := range body[1:] {
|
||||||
|
switch t.Tok.Kind {
|
||||||
|
case lexer.LParen, lexer.LBracket:
|
||||||
|
depth++
|
||||||
|
case lexer.RParen, lexer.RBracket:
|
||||||
|
if depth > 0 {
|
||||||
|
depth--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if depth == 0 {
|
||||||
|
low := lowerASCII(t.Tok.Text)
|
||||||
|
if t.Tok.Kind == lexer.Operator && (t.Tok.Text == ":=" || t.Tok.Text == "=") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if t.Tok.Kind == lexer.Ident && low == "default" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
typeTokens = append(typeTokens, t)
|
||||||
|
}
|
||||||
|
var tb strings.Builder
|
||||||
|
for j, t := range typeTokens {
|
||||||
|
if j > 0 && needSpace(typeTokens[j-1].Tok, t.Tok) && !isPctTypeBoundary(typeTokens, j) {
|
||||||
|
tb.WriteByte(' ')
|
||||||
|
}
|
||||||
|
tb.WriteString(caseText(t.Tok, st))
|
||||||
|
}
|
||||||
|
typeW = len(tb.String())
|
||||||
|
return nameW, typeW
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeDeclareAligned writes a single DECLARE variable with aligned columns.
|
||||||
|
func writeDeclareAligned(b *strings.Builder, body []cst.Tok, st config.Style, nameColW, typeColW int) {
|
||||||
|
if len(body) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := caseText(body[0].Tok, st)
|
||||||
|
b.WriteString(name)
|
||||||
|
if len(body) == 1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad name to nameColW if align_type is requested.
|
||||||
|
if st.PlpgsqlDeclareAlignType {
|
||||||
|
pad := nameColW - len(name)
|
||||||
|
for k := 0; k < pad; k++ {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect type tokens.
|
||||||
|
var typeTokens, restTokens []cst.Tok
|
||||||
|
depth := 0
|
||||||
|
pastType := false
|
||||||
|
for _, t := range body[1:] {
|
||||||
|
switch t.Tok.Kind {
|
||||||
|
case lexer.LParen, lexer.LBracket:
|
||||||
|
depth++
|
||||||
|
case lexer.RParen, lexer.RBracket:
|
||||||
|
if depth > 0 {
|
||||||
|
depth--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !pastType && depth == 0 {
|
||||||
|
low := lowerASCII(t.Tok.Text)
|
||||||
|
if (t.Tok.Kind == lexer.Operator && (t.Tok.Text == ":=" || t.Tok.Text == "=")) ||
|
||||||
|
(t.Tok.Kind == lexer.Ident && low == "default") {
|
||||||
|
pastType = true
|
||||||
|
restTokens = append(restTokens, t)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pastType {
|
||||||
|
restTokens = append(restTokens, t)
|
||||||
|
} else {
|
||||||
|
typeTokens = append(typeTokens, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var typeStr strings.Builder
|
||||||
|
for j, t := range typeTokens {
|
||||||
|
if j > 0 && needSpace(typeTokens[j-1].Tok, t.Tok) && !isPctTypeBoundary(typeTokens, j) {
|
||||||
|
typeStr.WriteByte(' ')
|
||||||
|
}
|
||||||
|
typeStr.WriteString(caseText(t.Tok, st))
|
||||||
|
}
|
||||||
|
typeRendered := typeStr.String()
|
||||||
|
|
||||||
|
b.WriteByte(' ')
|
||||||
|
b.WriteString(typeRendered)
|
||||||
|
|
||||||
|
if len(restTokens) > 0 {
|
||||||
|
// Pad type to typeColW if align_eq is requested.
|
||||||
|
if st.PlpgsqlDeclareAlignEq {
|
||||||
|
pad := typeColW - len(typeRendered)
|
||||||
|
for k := 0; k < pad; k++ {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for j, t := range restTokens {
|
||||||
|
prev := restTokens[0].Tok
|
||||||
|
if j > 0 {
|
||||||
|
prev = restTokens[j-1].Tok
|
||||||
|
}
|
||||||
|
if j == 0 || needSpace(prev, t.Tok) {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
}
|
||||||
|
b.WriteString(caseText(t.Tok, st))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
emit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// bline is one logical line within an accumulated statement.
|
// bline is one logical line within an accumulated statement.
|
||||||
@@ -208,17 +392,39 @@ type bline struct {
|
|||||||
// 3. After EXCEPTION the formatter switches to verbatim-indent mode (original
|
// 3. After EXCEPTION the formatter switches to verbatim-indent mode (original
|
||||||
// leading whitespace is preserved) to avoid conflicts between styles that
|
// leading whitespace is preserved) to avoid conflicts between styles that
|
||||||
// put WHEN at col-0 vs indented.
|
// put WHEN at col-0 vs indented.
|
||||||
// 4. Blank-line counts from the original are preserved.
|
// 4. Blank-line counts from the original are preserved (capped by PlpgsqlMaxBlankLines).
|
||||||
func formatBodyStatements(text string, st config.Style) string {
|
func formatBodyStatements(text string, st config.Style) string {
|
||||||
nl := st.Newline
|
nl := st.Newline
|
||||||
normalised := strings.ReplaceAll(text, "\r\n", "\n")
|
normalised := strings.ReplaceAll(text, "\r\n", "\n")
|
||||||
rawLines := strings.Split(normalised, "\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
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
result strings.Builder
|
result strings.Builder
|
||||||
stmt []bline
|
stmt []bline
|
||||||
parenDepth int
|
parenDepth int
|
||||||
blockDepth int // 0=col-0 (BEGIN/END/EXCEPTION), 1=body, 2=nested…
|
blockDepth int // 0=col-0 (BEGIN/END/EXCEPTION), 1=body, 2=nested…
|
||||||
|
caseDepth int // depth of open CASE…END expressions (WHEN…THEN is not a block opener)
|
||||||
inException bool
|
inException bool
|
||||||
pendingBlanks int
|
pendingBlanks int
|
||||||
depthInc bool // increment blockDepth after next flush
|
depthInc bool // increment blockDepth after next flush
|
||||||
@@ -228,7 +434,11 @@ func formatBodyStatements(text string, st config.Style) string {
|
|||||||
if len(stmt) == 0 {
|
if len(stmt) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for i := 0; i < pendingBlanks; i++ {
|
blanks := pendingBlanks
|
||||||
|
if blanks > maxBlanks {
|
||||||
|
blanks = maxBlanks
|
||||||
|
}
|
||||||
|
for i := 0; i < blanks; i++ {
|
||||||
result.WriteString(nl)
|
result.WriteString(nl)
|
||||||
}
|
}
|
||||||
pendingBlanks = 0
|
pendingBlanks = 0
|
||||||
@@ -259,25 +469,31 @@ func formatBodyStatements(text string, st config.Style) string {
|
|||||||
}
|
}
|
||||||
effectiveDepth = blockDepth
|
effectiveDepth = blockDepth
|
||||||
case "else", "elsif", "elseif":
|
case "else", "elsif", "elseif":
|
||||||
// Emit at one level up; blockDepth unchanged so the body continues
|
|
||||||
// at the same depth (THEN will re-apply depthInc for elsif).
|
|
||||||
if blockDepth > 0 {
|
if blockDepth > 0 {
|
||||||
effectiveDepth = blockDepth - 1
|
effectiveDepth = blockDepth - 1
|
||||||
}
|
}
|
||||||
case "exception":
|
case "exception":
|
||||||
inException = true
|
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)
|
baseIndent := strings.Repeat(st.Indent, effectiveDepth)
|
||||||
|
|
||||||
for i, ll := range stmt {
|
// plpgsql_if_then_newline: when false, THEN stays on the same line as
|
||||||
if i == 0 || ll.indent == "" {
|
// the condition. When true (default) it's already on its own logical line.
|
||||||
result.WriteString(baseIndent)
|
stmtLines := stmt
|
||||||
} else {
|
if !st.PlpgsqlIfThenNewline && fw == "if" {
|
||||||
result.WriteString(ll.indent)
|
stmtLines = joinThenToCondition(stmt)
|
||||||
}
|
}
|
||||||
result.WriteString(ll.text)
|
|
||||||
|
formattedLines := formatBodyStmtLines(stmtLines, baseIndent, st)
|
||||||
|
for _, line := range formattedLines {
|
||||||
|
result.WriteString(line)
|
||||||
result.WriteString(nl)
|
result.WriteString(nl)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,8 +504,21 @@ func formatBodyStatements(text string, st config.Style) string {
|
|||||||
stmt = nil
|
stmt = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, rawLine := range rawLines {
|
for j, rawLine := range rawLines {
|
||||||
line := strings.TrimRight(rawLine, "\r")
|
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)
|
indent := leadingWhitespace(line)
|
||||||
stripped := line[len(indent):]
|
stripped := line[len(indent):]
|
||||||
|
|
||||||
@@ -301,9 +530,37 @@ func formatBodyStatements(text string, st config.Style) string {
|
|||||||
|
|
||||||
fw := lowerASCII(firstBodyKeyword(stripped))
|
fw := lowerASCII(firstBodyKeyword(stripped))
|
||||||
isColZero := indent == ""
|
isColZero := indent == ""
|
||||||
// Only join when we are at paren-depth 0; content inside parens (e.g.
|
|
||||||
// inside a CTE subquery) should not be merged across lines.
|
|
||||||
joinToPrev := isColZero && parenDepth == 0 && len(stmt) > 0 && !sqlClauseKw[fw]
|
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.
|
||||||
|
if joinToPrev {
|
||||||
|
if lowerASCII(firstBodyKeyword(stmt[len(stmt)-1].text)) == "" {
|
||||||
|
joinToPrev = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Pre-flush pending comment-only blines before adding a new non-comment
|
||||||
|
// non-joined bline. Without this, a comment + `end if;` end up in the
|
||||||
|
// same stmt, `fw` comes from the comment (empty string), depth is never
|
||||||
|
// decremented, and the formatter diverges on the second pass.
|
||||||
|
if !joinToPrev && fw != "" && len(stmt) > 0 {
|
||||||
|
allComments := true
|
||||||
|
for _, ll := range stmt {
|
||||||
|
if lowerASCII(firstBodyKeyword(ll.text)) != "" {
|
||||||
|
allComments = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if allComments {
|
||||||
|
flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if joinToPrev {
|
if joinToPrev {
|
||||||
last := &stmt[len(stmt)-1]
|
last := &stmt[len(stmt)-1]
|
||||||
@@ -312,7 +569,6 @@ func formatBodyStatements(text string, st config.Style) string {
|
|||||||
stmt = append(stmt, bline{text: stripped, indent: indent})
|
stmt = append(stmt, bline{text: stripped, indent: indent})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan tokens to track paren depth and detect flush triggers.
|
|
||||||
var lastD0Kw string
|
var lastD0Kw string
|
||||||
for _, tok := range lexer.Lex(stripped) {
|
for _, tok := range lexer.Lex(stripped) {
|
||||||
if tok.IsTrivia() || tok.Kind == lexer.EOF {
|
if tok.IsTrivia() || tok.Kind == lexer.EOF {
|
||||||
@@ -327,27 +583,53 @@ func formatBodyStatements(text string, st config.Style) string {
|
|||||||
}
|
}
|
||||||
case lexer.Semicolon:
|
case lexer.Semicolon:
|
||||||
if parenDepth == 0 {
|
if parenDepth == 0 {
|
||||||
|
// plpgsql_loop_collapse: fold empty FOR … LOOP END LOOP; to one line.
|
||||||
|
if st.PlpgsqlLoopCollapse && len(stmt) > 0 {
|
||||||
|
collapsed, ok := tryCollapseLoop(stmt, st)
|
||||||
|
if ok {
|
||||||
|
stmt = []bline{{text: collapsed, indent: ""}}
|
||||||
|
}
|
||||||
|
}
|
||||||
flush()
|
flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if parenDepth == 0 && tok.Kind == lexer.Ident {
|
if parenDepth == 0 && tok.Kind == lexer.Ident {
|
||||||
lastD0Kw = lowerASCII(tok.Text)
|
lastD0Kw = lowerASCII(tok.Text)
|
||||||
|
switch lastD0Kw {
|
||||||
|
case "case":
|
||||||
|
caseDepth++
|
||||||
|
case "end":
|
||||||
|
if caseDepth > 0 {
|
||||||
|
caseDepth--
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Structural keywords at the end of a line (paren depth 0) trigger a
|
|
||||||
// flush and possibly a block-depth change.
|
|
||||||
if parenDepth == 0 && len(stmt) > 0 {
|
if parenDepth == 0 && len(stmt) > 0 {
|
||||||
switch lastD0Kw {
|
switch lastD0Kw {
|
||||||
case "then", "loop", "begin":
|
case "then":
|
||||||
// ELSIF/ELSEIF headers end with THEN but must NOT increment depth
|
// A THEN ending a CASE…WHEN branch is not a PL/pgSQL block
|
||||||
// (blockDepth is already at the right level for the body).
|
// opener; only one matching END closes the whole CASE, so
|
||||||
|
// treating each WHEN…THEN as a block open would permanently
|
||||||
|
// inflate blockDepth.
|
||||||
|
if caseDepth == 0 {
|
||||||
|
fw0 := lowerASCII(firstBodyKeyword(stmt[0].text))
|
||||||
|
if fw0 != "elsif" && fw0 != "elseif" {
|
||||||
|
depthInc = true
|
||||||
|
}
|
||||||
|
flush()
|
||||||
|
}
|
||||||
|
case "loop", "begin":
|
||||||
fw0 := lowerASCII(firstBodyKeyword(stmt[0].text))
|
fw0 := lowerASCII(firstBodyKeyword(stmt[0].text))
|
||||||
if fw0 != "elsif" && fw0 != "elseif" {
|
if fw0 != "elsif" && fw0 != "elseif" {
|
||||||
depthInc = true
|
depthInc = true
|
||||||
}
|
}
|
||||||
flush()
|
flush()
|
||||||
case "else", "exception":
|
case "else", "exception":
|
||||||
|
if caseDepth > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
flush()
|
flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -357,6 +639,185 @@ func formatBodyStatements(text string, st config.Style) string {
|
|||||||
return result.String()
|
return result.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// formatBodyStmtLines formats one flushed PL/pgSQL statement at its contextual
|
||||||
|
// base indent. Multi-line UPDATE/DELETE statements inside PL/pgSQL get their
|
||||||
|
// top-level SET/WHERE/AND/OR clauses realigned under the statement while nested
|
||||||
|
// subqueries keep their original indentation. Non-DML statements keep
|
||||||
|
// continuation indentation, except that standalone structural keywords such as
|
||||||
|
// THEN are aligned with the block opener.
|
||||||
|
func formatBodyStmtLines(lines []bline, baseIndent string, st config.Style) []string {
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if looksLikeMultiLineBodyDML(lines) {
|
||||||
|
return reindentBodyDML(lines, baseIndent, st)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]string, 0, len(lines))
|
||||||
|
for i, ll := range lines {
|
||||||
|
text := ll.text
|
||||||
|
indent := baseIndent
|
||||||
|
switch {
|
||||||
|
case i > 0 && ll.indent == "" && len(significantBodyTokens(text)) == 0:
|
||||||
|
// A column-0 comment line trailing a multi-line (commented-out)
|
||||||
|
// statement is a continuation the author left flush-left — keep it
|
||||||
|
// there rather than re-indenting it to block depth.
|
||||||
|
indent = ""
|
||||||
|
case i > 0 && ll.indent != "" && !isStandaloneBodyKeyword(ll.text, "then", "else", "elsif", "elseif"):
|
||||||
|
indent = ll.indent
|
||||||
|
}
|
||||||
|
out = append(out, indent+text)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func looksLikeMultiLineBodyDML(lines []bline) bool {
|
||||||
|
if len(lines) < 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
kw := lowerASCII(firstBodyKeyword(lines[0].text))
|
||||||
|
return kw == "update" || kw == "delete"
|
||||||
|
}
|
||||||
|
|
||||||
|
func reindentBodyDML(lines []bline, baseIndent string, st config.Style) []string {
|
||||||
|
out := make([]string, 0, len(lines)+1)
|
||||||
|
afterWhere := false
|
||||||
|
parenDepth := 0
|
||||||
|
for i, ll := range lines {
|
||||||
|
text := strings.TrimRight(ll.text, " ")
|
||||||
|
lineDepth := parenDepth
|
||||||
|
kw := lowerASCII(firstBodyKeyword(text))
|
||||||
|
if afterWhere && lineDepth == 0 && kw != "and" && kw != "or" {
|
||||||
|
out = append(out, baseIndent+st.Indent+st.Indent+strings.TrimSpace(text))
|
||||||
|
afterWhere = false
|
||||||
|
updateBodyParenDepth(text, &parenDepth)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lineDepth == 0 && (kw == "set" || kw == "where" || kw == "values" || kw == "returning") {
|
||||||
|
if kw == "where" {
|
||||||
|
whereText := strings.TrimSpace(text)
|
||||||
|
fields := strings.Fields(whereText)
|
||||||
|
nextKw := ""
|
||||||
|
if i+1 < len(lines) {
|
||||||
|
nextKw = lowerASCII(firstBodyKeyword(lines[i+1].text))
|
||||||
|
}
|
||||||
|
if len(fields) > 1 && (nextKw == "and" || nextKw == "or") {
|
||||||
|
out = append(out, baseIndent+fields[0])
|
||||||
|
out = append(out, baseIndent+st.Indent+st.Indent+strings.TrimSpace(whereText[len(fields[0]):]))
|
||||||
|
afterWhere = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
afterWhere = len(fields) == 1
|
||||||
|
}
|
||||||
|
out = append(out, baseIndent+strings.TrimSpace(text))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lineDepth == 0 && (kw == "and" || kw == "or") {
|
||||||
|
// House style: AND/OR line up with the WHERE keyword; the first
|
||||||
|
// predicate is indented two levels under it.
|
||||||
|
out = append(out, baseIndent+strings.TrimSpace(text))
|
||||||
|
afterWhere = false
|
||||||
|
updateBodyParenDepth(text, &parenDepth)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
out = append(out, baseIndent+strings.TrimSpace(text))
|
||||||
|
} else if ll.indent != "" {
|
||||||
|
out = append(out, ll.indent+strings.TrimSpace(text))
|
||||||
|
} else {
|
||||||
|
out = append(out, baseIndent+strings.TrimSpace(text))
|
||||||
|
}
|
||||||
|
afterWhere = false
|
||||||
|
updateBodyParenDepth(text, &parenDepth)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateBodyParenDepth(s string, depth *int) {
|
||||||
|
for _, tok := range lexer.Lex(s) {
|
||||||
|
switch tok.Kind {
|
||||||
|
case lexer.LParen, lexer.LBracket:
|
||||||
|
(*depth)++
|
||||||
|
case lexer.RParen, lexer.RBracket:
|
||||||
|
if *depth > 0 {
|
||||||
|
(*depth)--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func significantBodyTokens(s string) []cst.Tok {
|
||||||
|
var toks []cst.Tok
|
||||||
|
for _, tok := range lexer.Lex(s) {
|
||||||
|
if tok.IsTrivia() || tok.Kind == lexer.EOF {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
toks = append(toks, cst.Tok{Tok: tok})
|
||||||
|
}
|
||||||
|
return toks
|
||||||
|
}
|
||||||
|
|
||||||
|
func isStandaloneBodyKeyword(s string, kws ...string) bool {
|
||||||
|
toks := significantBodyTokens(s)
|
||||||
|
if len(toks) != 1 || toks[0].Tok.Kind != lexer.Ident {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
low := lowerASCII(toks[0].Tok.Text)
|
||||||
|
for _, kw := range kws {
|
||||||
|
if low == kw {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// joinThenToCondition merges a THEN line (on its own bline) into the preceding
|
||||||
|
// condition line when plpgsql_if_then_newline is false.
|
||||||
|
func joinThenToCondition(lines []bline) []bline {
|
||||||
|
out := make([]bline, 0, len(lines))
|
||||||
|
for i, ll := range lines {
|
||||||
|
if i > 0 && strings.EqualFold(strings.TrimSpace(ll.text), "then") {
|
||||||
|
out[len(out)-1].text = strings.TrimRight(out[len(out)-1].text, " \t") + " THEN"
|
||||||
|
} else {
|
||||||
|
out = append(out, ll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// tryCollapseLoop tries to collapse an empty loop body to one line.
|
||||||
|
// Detects: FOR … LOOP\n (empty or only blanks)\nEND LOOP;
|
||||||
|
// Returns the collapsed line and true on success.
|
||||||
|
func tryCollapseLoop(lines []bline, st config.Style) (string, bool) {
|
||||||
|
if len(lines) < 2 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
first := strings.TrimSpace(lines[0].text)
|
||||||
|
last := strings.TrimSpace(lines[len(lines)-1].text)
|
||||||
|
firstLow := lowerASCII(first)
|
||||||
|
lastLow := lowerASCII(last)
|
||||||
|
|
||||||
|
// Check last line is END LOOP; or LOOP (for WHILE/FOR empty bodies that end with LOOP).
|
||||||
|
if !strings.HasPrefix(lastLow, "end loop") && lastLow != "end loop;" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
// Check middle lines are all empty.
|
||||||
|
for _, mid := range lines[1 : len(lines)-1] {
|
||||||
|
if strings.TrimSpace(mid.text) != "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check first line ends with LOOP.
|
||||||
|
if !strings.HasSuffix(firstLow, "loop") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
_ = st
|
||||||
|
_ = firstLow
|
||||||
|
// Collapse to: <header> END LOOP;
|
||||||
|
return strings.TrimRight(first, " \t") + " " + strings.ToUpper(last), true
|
||||||
|
}
|
||||||
|
|
||||||
// firstBodyKeyword returns the text of the first identifier token in s
|
// firstBodyKeyword returns the text of the first identifier token in s
|
||||||
// (lowercased), or "" if the first significant token is not an identifier.
|
// (lowercased), or "" if the first significant token is not an identifier.
|
||||||
func firstBodyKeyword(s string) string {
|
func firstBodyKeyword(s string) string {
|
||||||
|
|||||||
+270
-21
@@ -10,16 +10,13 @@ import (
|
|||||||
|
|
||||||
// isDMLStart reports whether toks begins with a DML statement keyword.
|
// isDMLStart reports whether toks begins with a DML statement keyword.
|
||||||
func isDMLStart(toks []cst.Tok) bool {
|
func isDMLStart(toks []cst.Tok) bool {
|
||||||
for _, t := range toks {
|
if len(toks) == 0 || toks[0].Tok.Kind != lexer.Ident {
|
||||||
if t.Tok.Kind == lexer.Ident {
|
|
||||||
switch lowerASCII(t.Tok.Text) {
|
|
||||||
case "select", "insert", "update", "delete", "with":
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
switch lowerASCII(toks[0].Tok.Text) {
|
||||||
|
case "select", "insert", "update", "delete", "with":
|
||||||
|
return true
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,9 +176,16 @@ func dmlSegText(seg dmlSeg, st config.Style) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch kw {
|
switch kw {
|
||||||
case "select", "set", "returning":
|
case "select", "returning":
|
||||||
items := dmlSplitCommas(seg.body)
|
items := dmlSplitCommas(seg.body)
|
||||||
return dmlColList(kwText, items, st)
|
return dmlColListSelect(kwText, items, st)
|
||||||
|
case "set":
|
||||||
|
items := dmlSplitCommas(seg.body)
|
||||||
|
return dmlColListSet(kwText, items, st)
|
||||||
|
case "where":
|
||||||
|
return dmlWhereClause(kwText, seg.body, st)
|
||||||
|
case "join", "left", "right", "inner", "full", "cross", "natural":
|
||||||
|
return dmlJoinClause(kwText, seg.body, st)
|
||||||
case "with":
|
case "with":
|
||||||
return formatWithBody(kwText, seg.body, st)
|
return formatWithBody(kwText, seg.body, st)
|
||||||
default:
|
default:
|
||||||
@@ -193,6 +197,97 @@ func dmlSegText(seg dmlSeg, st config.Style) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dmlJoinClause formats a JOIN clause, applying indent_join when configured.
|
||||||
|
func dmlJoinClause(kwText string, body []cst.Tok, st config.Style) string {
|
||||||
|
text := dmlInline(body, st)
|
||||||
|
line := kwText
|
||||||
|
if text != "" {
|
||||||
|
line += " " + text
|
||||||
|
}
|
||||||
|
if !st.IndentJoin {
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
indent := strings.Repeat(st.Indent, st.JoinIndentSize)
|
||||||
|
nl := st.Newline
|
||||||
|
var b strings.Builder
|
||||||
|
for i, part := range strings.Split(line, nl) {
|
||||||
|
if i > 0 {
|
||||||
|
b.WriteString(nl)
|
||||||
|
}
|
||||||
|
b.WriteString(indent)
|
||||||
|
b.WriteString(part)
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// dmlWhereClause formats a WHERE clause, splitting AND/OR conditions per
|
||||||
|
// the where_wrap and where_and_or_indent settings.
|
||||||
|
func dmlWhereClause(kwText string, body []cst.Tok, st config.Style) string {
|
||||||
|
if st.WhereWrap == config.WrapNever {
|
||||||
|
text := dmlInline(body, st)
|
||||||
|
if text == "" {
|
||||||
|
return kwText
|
||||||
|
}
|
||||||
|
return kwText + " " + text
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split at depth-0 AND/OR.
|
||||||
|
conditions := dmlSplitAndOr(body)
|
||||||
|
if len(conditions) <= 1 {
|
||||||
|
text := dmlInline(body, st)
|
||||||
|
if text == "" {
|
||||||
|
return kwText
|
||||||
|
}
|
||||||
|
return kwText + " " + text
|
||||||
|
}
|
||||||
|
|
||||||
|
nl := st.Newline
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(kwText)
|
||||||
|
for i, cond := range conditions {
|
||||||
|
b.WriteString(nl)
|
||||||
|
text := dmlInline(cond, st)
|
||||||
|
if st.WhereAndOrIndent {
|
||||||
|
b.WriteString(st.Indent)
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
// First condition: no leading AND/OR
|
||||||
|
b.WriteString(" ") // align with AND/OR token width
|
||||||
|
b.WriteString(text)
|
||||||
|
} else {
|
||||||
|
b.WriteString(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// dmlSplitAndOr splits toks at depth-0 AND/OR tokens, keeping the AND/OR with
|
||||||
|
// the following condition.
|
||||||
|
func dmlSplitAndOr(toks []cst.Tok) [][]cst.Tok {
|
||||||
|
var result [][]cst.Tok
|
||||||
|
depth := 0
|
||||||
|
start := 0
|
||||||
|
for i, t := range toks {
|
||||||
|
switch t.Tok.Kind {
|
||||||
|
case lexer.LParen, lexer.LBracket:
|
||||||
|
depth++
|
||||||
|
case lexer.RParen, lexer.RBracket:
|
||||||
|
if depth > 0 {
|
||||||
|
depth--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if depth == 0 && t.Tok.Kind == lexer.Ident {
|
||||||
|
low := lowerASCII(t.Tok.Text)
|
||||||
|
if (low == "and" || low == "or") && i > start {
|
||||||
|
result = append(result, toks[start:i])
|
||||||
|
start = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = append(result, toks[start:])
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// formatWithBody formats the body of a WITH clause by splitting CTE definitions
|
// formatWithBody formats the body of a WITH clause by splitting CTE definitions
|
||||||
// at depth-0 commas and formatting the subquery inside each AS (...) block.
|
// at depth-0 commas and formatting the subquery inside each AS (...) block.
|
||||||
func formatWithBody(kwText string, body []cst.Tok, st config.Style) string {
|
func formatWithBody(kwText string, body []cst.Tok, st config.Style) string {
|
||||||
@@ -367,10 +462,21 @@ func dmlInline(toks []cst.Tok, st config.Style) string {
|
|||||||
}
|
}
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
for i, t := range toks {
|
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(' ')
|
b.WriteByte(' ')
|
||||||
}
|
}
|
||||||
b.WriteString(caseText(t.Tok, st))
|
// Space after comma in calls: func(a, b) vs func(a,b).
|
||||||
|
if st.SpaceAfterCommaInCalls && i > 0 && toks[i-1].Tok.Kind == lexer.Comma {
|
||||||
|
// Only inside parens (caller manages this at depth > 0, but we add space
|
||||||
|
// when the comma is not a clause-level comma — heuristic: always add).
|
||||||
|
b.WriteByte(' ')
|
||||||
|
}
|
||||||
|
var prev lexer.Token
|
||||||
|
if i > 0 {
|
||||||
|
prev = toks[i-1].Tok
|
||||||
|
}
|
||||||
|
nextIsLParen := i+1 < len(toks) && toks[i+1].Tok.Kind == lexer.LParen
|
||||||
|
b.WriteString(caseTextCtx(t.Tok, prev, nextIsLParen, st))
|
||||||
}
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
@@ -401,11 +507,9 @@ func dmlSplitCommas(toks []cst.Tok) [][]cst.Tok {
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
// dmlColList formats kwText followed by a comma-separated body.
|
// dmlColListSelect formats a SELECT / RETURNING column list with optional
|
||||||
// One item: kept on the same line as the keyword.
|
// align_columns and select_align_as settings.
|
||||||
// Multiple items: each on its own line with the configured comma style.
|
func dmlColListSelect(kwText string, items [][]cst.Tok, st config.Style) string {
|
||||||
func dmlColList(kwText string, items [][]cst.Tok, st config.Style) string {
|
|
||||||
// Filter out spurious empty items (e.g. trailing comma in source).
|
|
||||||
var kept [][]cst.Tok
|
var kept [][]cst.Tok
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
if len(item) > 0 {
|
if len(item) > 0 {
|
||||||
@@ -426,14 +530,23 @@ func dmlColList(kwText string, items [][]cst.Tok, st config.Style) string {
|
|||||||
return kwText + " " + body
|
return kwText + " " + body
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multiple items: one per line.
|
// Render each item text.
|
||||||
first := st.Indent + " " // aligns item text one column past the comma
|
texts := make([]string, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
texts[i] = dmlInline(item, st)
|
||||||
|
}
|
||||||
|
|
||||||
|
// align_columns / select_align_as: pad expressions so AS and aliases align.
|
||||||
|
if (st.AlignColumns || st.SelectAlignAs) && len(texts) > 1 {
|
||||||
|
texts = alignSelectItems(texts, st)
|
||||||
|
}
|
||||||
|
|
||||||
|
first := st.Indent + " "
|
||||||
cont := st.Indent + ","
|
cont := st.Indent + ","
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString(kwText)
|
b.WriteString(kwText)
|
||||||
for i, item := range items {
|
for i, text := range texts {
|
||||||
b.WriteString(nl)
|
b.WriteString(nl)
|
||||||
text := dmlInline(item, st)
|
|
||||||
if i == 0 || st.Commas != config.CommaLeading {
|
if i == 0 || st.Commas != config.CommaLeading {
|
||||||
b.WriteString(first)
|
b.WriteString(first)
|
||||||
b.WriteString(text)
|
b.WriteString(text)
|
||||||
@@ -447,3 +560,139 @@ func dmlColList(kwText string, items [][]cst.Tok, st config.Style) string {
|
|||||||
}
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dmlColListSet formats an UPDATE SET column list with optional set_align_equal.
|
||||||
|
func dmlColListSet(kwText string, items [][]cst.Tok, st config.Style) string {
|
||||||
|
var kept [][]cst.Tok
|
||||||
|
for _, item := range items {
|
||||||
|
if len(item) > 0 {
|
||||||
|
kept = append(kept, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items = kept
|
||||||
|
|
||||||
|
nl := st.Newline
|
||||||
|
switch len(items) {
|
||||||
|
case 0:
|
||||||
|
return kwText
|
||||||
|
case 1:
|
||||||
|
body := dmlInline(items[0], st)
|
||||||
|
if body == "" {
|
||||||
|
return kwText
|
||||||
|
}
|
||||||
|
return kwText + " " + body
|
||||||
|
}
|
||||||
|
|
||||||
|
texts := make([]string, len(items))
|
||||||
|
for i, item := range items {
|
||||||
|
texts[i] = dmlInline(item, st)
|
||||||
|
}
|
||||||
|
|
||||||
|
// set_align_equal: pad lhs so = signs align.
|
||||||
|
if st.SetAlignEqual && len(texts) > 1 {
|
||||||
|
texts = alignSetItems(texts)
|
||||||
|
}
|
||||||
|
|
||||||
|
first := st.Indent + " "
|
||||||
|
cont := st.Indent + ","
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(kwText)
|
||||||
|
for i, text := range texts {
|
||||||
|
b.WriteString(nl)
|
||||||
|
if i == 0 || st.Commas != config.CommaLeading {
|
||||||
|
b.WriteString(first)
|
||||||
|
b.WriteString(text)
|
||||||
|
if st.Commas == config.CommaTrailing && i < len(items)-1 {
|
||||||
|
b.WriteString(",")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b.WriteString(cont)
|
||||||
|
b.WriteString(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// alignSelectItems pads SELECT list item expressions so that AS keywords and
|
||||||
|
// alias names align vertically.
|
||||||
|
func alignSelectItems(texts []string, st config.Style) []string {
|
||||||
|
// Split each text into (expr, " AS ", alias) or keep as-is.
|
||||||
|
type part struct {
|
||||||
|
expr, alias string
|
||||||
|
hasAs bool
|
||||||
|
}
|
||||||
|
parts := make([]part, len(texts))
|
||||||
|
maxExpr := 0
|
||||||
|
for i, t := range texts {
|
||||||
|
// Find " AS " or " as " (case-insensitive).
|
||||||
|
if idx := findAsIndex(t); idx >= 0 {
|
||||||
|
parts[i] = part{expr: t[:idx], alias: t[idx:], hasAs: true}
|
||||||
|
if l := len(t[:idx]); l > maxExpr {
|
||||||
|
maxExpr = l
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parts[i] = part{expr: t}
|
||||||
|
if st.AlignColumns {
|
||||||
|
if l := len(t); l > maxExpr {
|
||||||
|
maxExpr = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]string, len(texts))
|
||||||
|
for i, p := range parts {
|
||||||
|
if !p.hasAs || maxExpr == 0 {
|
||||||
|
out[i] = texts[i]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
pad := strings.Repeat(" ", maxExpr-len(p.expr))
|
||||||
|
out[i] = p.expr + pad + p.alias
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// findAsIndex returns the byte index of " AS " (case-insensitive) in s,
|
||||||
|
// or -1 if not present at depth 0.
|
||||||
|
func findAsIndex(s string) int {
|
||||||
|
low := lowerASCII(s)
|
||||||
|
// Look for " as " boundary.
|
||||||
|
for i := 0; i < len(low)-3; i++ {
|
||||||
|
if low[i] == ' ' && low[i+1] == 'a' && low[i+2] == 's' && low[i+3] == ' ' {
|
||||||
|
return i + 1 // index of 'a'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// alignSetItems pads SET assignment lhs values so that = signs align.
|
||||||
|
func alignSetItems(texts []string) []string {
|
||||||
|
maxLhs := 0
|
||||||
|
lhsWidths := make([]int, len(texts))
|
||||||
|
for i, t := range texts {
|
||||||
|
idx := strings.Index(t, " = ")
|
||||||
|
if idx < 0 {
|
||||||
|
idx = strings.Index(t, "=")
|
||||||
|
}
|
||||||
|
if idx >= 0 {
|
||||||
|
lhsWidths[i] = idx
|
||||||
|
if idx > maxLhs {
|
||||||
|
maxLhs = idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if maxLhs == 0 {
|
||||||
|
return texts
|
||||||
|
}
|
||||||
|
out := make([]string, len(texts))
|
||||||
|
for i, t := range texts {
|
||||||
|
if lhsWidths[i] == 0 || lhsWidths[i] == maxLhs {
|
||||||
|
out[i] = t
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
idx := lhsWidths[i]
|
||||||
|
pad := strings.Repeat(" ", maxLhs-idx)
|
||||||
|
out[i] = t[:idx] + pad + t[idx:]
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@@ -226,6 +226,62 @@ func TestDMLIdempotent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDMLWhereAndOr(t *testing.T) {
|
||||||
|
// where_wrap=always should split AND/OR conditions onto separate lines.
|
||||||
|
src := "select a from t where x = 1 and y = 2 or z = 3;"
|
||||||
|
got := format(src)
|
||||||
|
want := "SELECT a\nFROM t\nWHERE\n x = 1\n AND y = 2\n OR z = 3;\n"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("where and/or\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
checkDML(t, "where and/or", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLIndentJoin(t *testing.T) {
|
||||||
|
st := config.Default()
|
||||||
|
st.IndentJoin = true
|
||||||
|
src := "select a from t join s on t.id = s.id;"
|
||||||
|
got := File(parser.Parse(src), st)
|
||||||
|
want := "SELECT a\nFROM t\n JOIN s ON t.id = s.id;\n"
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("indent join\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
// Idempotence with same config.
|
||||||
|
twice := File(parser.Parse(got), st)
|
||||||
|
if twice != got {
|
||||||
|
t.Errorf("indent join not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLSetAlignEqual(t *testing.T) {
|
||||||
|
st := config.Default()
|
||||||
|
st.SetAlignEqual = true
|
||||||
|
src := "update t set a = 1, bb = 2, ccc = 3 where id = 1;"
|
||||||
|
got := File(parser.Parse(src), st)
|
||||||
|
// All = signs should align.
|
||||||
|
if got == "" {
|
||||||
|
t.Error("empty output")
|
||||||
|
}
|
||||||
|
// Idempotence.
|
||||||
|
twice := File(parser.Parse(got), st)
|
||||||
|
if twice != got {
|
||||||
|
t.Errorf("set_align_equal not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDMLAlignParamTypes(t *testing.T) {
|
||||||
|
src := "create function f(in p_name text, in p_long_name integer, out p_result boolean) returns void language sql as $$ select 1 $$;"
|
||||||
|
got := format(src)
|
||||||
|
// p_name and p_long_name should have aligned types.
|
||||||
|
if got == "" {
|
||||||
|
t.Error("empty output")
|
||||||
|
}
|
||||||
|
twice := format(got)
|
||||||
|
if twice != got {
|
||||||
|
t.Errorf("align_param_types not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", got, twice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCorpusUnaffectedByDML(t *testing.T) {
|
func TestCorpusUnaffectedByDML(t *testing.T) {
|
||||||
// Verify the corpus (which contains only CREATE FUNCTION) is not affected
|
// Verify the corpus (which contains only CREATE FUNCTION) is not affected
|
||||||
// by the new DML formatting path.
|
// by the new DML formatting path.
|
||||||
|
|||||||
+236
-21
@@ -53,9 +53,12 @@ func (p *printer) writeItem(n cst.Node) {
|
|||||||
case *cst.CreateFunction:
|
case *cst.CreateFunction:
|
||||||
p.writeCreateFunction(v)
|
p.writeCreateFunction(v)
|
||||||
case *cst.Raw:
|
case *cst.Raw:
|
||||||
if isDMLStart(v.Toks) {
|
switch {
|
||||||
|
case isDMLStart(v.Toks):
|
||||||
p.b.WriteString(formatDML(v.Toks, p.st))
|
p.b.WriteString(formatDML(v.Toks, p.st))
|
||||||
} else {
|
case isDoBlock(v.Toks):
|
||||||
|
p.b.WriteString(formatDoBlock(v.Toks, p.st))
|
||||||
|
default:
|
||||||
p.b.WriteString(verbatimSpan(v.Toks))
|
p.b.WriteString(verbatimSpan(v.Toks))
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -63,11 +66,64 @@ func (p *printer) writeItem(n cst.Node) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isDoBlock reports whether toks is a DO $$ ... $$ statement.
|
||||||
|
func isDoBlock(toks []cst.Tok) bool {
|
||||||
|
for _, t := range toks {
|
||||||
|
if t.Tok.Kind == lexer.Ident {
|
||||||
|
return lowerASCII(t.Tok.Text) == "do"
|
||||||
|
}
|
||||||
|
if !t.Tok.IsTrivia() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatDoBlock formats a DO $$ ... $$ block by applying formatBody to the
|
||||||
|
// dollar-quoted string and emitting DO + newline + formatted body.
|
||||||
|
func formatDoBlock(toks []cst.Tok, st config.Style) string {
|
||||||
|
// Find the DO keyword, the dollar-string body, and the optional semicolon.
|
||||||
|
var doTok, bodyTok *cst.Tok
|
||||||
|
hasSemi := false
|
||||||
|
for i := range toks {
|
||||||
|
t := &toks[i]
|
||||||
|
if t.Tok.IsTrivia() || t.Tok.Kind == lexer.EOF {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
low := lowerASCII(t.Tok.Text)
|
||||||
|
if t.Tok.Kind == lexer.Ident && low == "do" && doTok == nil {
|
||||||
|
doTok = t
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if doTok != nil && t.Tok.Kind == lexer.DollarString && bodyTok == nil {
|
||||||
|
bodyTok = t
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if t.Tok.Kind == lexer.Semicolon {
|
||||||
|
hasSemi = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if doTok == nil || bodyTok == nil {
|
||||||
|
return verbatimSpan(toks)
|
||||||
|
}
|
||||||
|
|
||||||
|
nl := st.Newline
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(applyCase(doTok.Tok.Text, st.KeywordCase))
|
||||||
|
b.WriteString(nl)
|
||||||
|
b.WriteString(formatBody(bodyTok.Tok.Text, st))
|
||||||
|
if hasSemi {
|
||||||
|
b.WriteString(";")
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
func (p *printer) writeCreateFunction(cf *cst.CreateFunction) {
|
func (p *printer) writeCreateFunction(cf *cst.CreateFunction) {
|
||||||
// Safety: if the header carries comments we cannot confidently relocate,
|
// Safety: if the header carries comments we cannot confidently relocate,
|
||||||
// emit the whole statement verbatim rather than risk dropping them.
|
// emit the whole statement verbatim rather than risk dropping them.
|
||||||
|
// We still format the body dollar-string independently since it is self-contained.
|
||||||
if headerHasComments(cf) {
|
if headerHasComments(cf) {
|
||||||
p.b.WriteString(verbatimSpan(cst.Tokens(cf)))
|
p.b.WriteString(verbatimSpanFormatBody(cst.Tokens(cf), cf.Body, p.st))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,11 +136,23 @@ func (p *printer) writeCreateFunction(cf *cst.CreateFunction) {
|
|||||||
}
|
}
|
||||||
p.b.WriteString("(")
|
p.b.WriteString("(")
|
||||||
|
|
||||||
first := p.st.Indent + " " // align item text one column past the comma
|
// Build formatted param texts first so we can measure widths.
|
||||||
cont := p.st.Indent
|
paramTexts := make([]string, len(cf.Params))
|
||||||
for i, param := range cf.Params {
|
for i, param := range cf.Params {
|
||||||
|
paramTexts[i] = p.inline(param.Toks)
|
||||||
|
}
|
||||||
|
|
||||||
|
// align_param_types: pad param names so type columns align.
|
||||||
|
if p.st.AlignParamTypes && len(cf.Params) > 1 {
|
||||||
|
paramTexts = alignParamTypes(paramTexts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
p.nl()
|
||||||
text := p.inline(param.Toks)
|
|
||||||
if i == 0 || p.st.Commas != config.CommaLeading {
|
if i == 0 || p.st.Commas != config.CommaLeading {
|
||||||
p.b.WriteString(first)
|
p.b.WriteString(first)
|
||||||
p.b.WriteString(text)
|
p.b.WriteString(text)
|
||||||
@@ -92,20 +160,35 @@ func (p *printer) writeCreateFunction(cf *cst.CreateFunction) {
|
|||||||
p.b.WriteString(",")
|
p.b.WriteString(",")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
p.b.WriteString(cont)
|
p.b.WriteString(", ")
|
||||||
p.b.WriteString(",")
|
|
||||||
p.b.WriteString(text)
|
p.b.WriteString(text)
|
||||||
}
|
}
|
||||||
|
// Emit trailing inline comment from the separator (e.g. --description after param).
|
||||||
|
if param.Sep != nil {
|
||||||
|
for _, tr := range param.Sep.Lead {
|
||||||
|
if tr.Kind == lexer.LineComment || tr.Kind == lexer.BlockComment {
|
||||||
|
p.b.WriteByte(' ')
|
||||||
|
p.b.WriteString(strings.TrimRight(tr.Text, " \t"))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
p.nl()
|
p.nl()
|
||||||
p.b.WriteString(")")
|
p.b.WriteString(")")
|
||||||
|
|
||||||
for _, clause := range cf.Options {
|
for _, clause := range cf.Options {
|
||||||
p.nl()
|
p.nl()
|
||||||
|
p.b.WriteString(p.st.Indent)
|
||||||
p.b.WriteString(p.inline(clause))
|
p.b.WriteString(p.inline(clause))
|
||||||
}
|
}
|
||||||
if cf.As != nil {
|
if cf.As != nil {
|
||||||
p.nl()
|
// routine_as_wrap: when false, AS stays on the same line as the last option.
|
||||||
|
if p.st.RoutineAsWrap {
|
||||||
|
p.nl()
|
||||||
|
} else {
|
||||||
|
p.b.WriteByte(' ')
|
||||||
|
}
|
||||||
p.b.WriteString(p.inline([]cst.Tok{{Tok: cf.As.Tok}}))
|
p.b.WriteString(p.inline([]cst.Tok{{Tok: cf.As.Tok}}))
|
||||||
}
|
}
|
||||||
if cf.Body != nil {
|
if cf.Body != nil {
|
||||||
@@ -136,10 +219,15 @@ func (p *printer) inline(toks []cst.Tok) string {
|
|||||||
}
|
}
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
for i, t := range toks {
|
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(' ')
|
b.WriteByte(' ')
|
||||||
}
|
}
|
||||||
b.WriteString(caseText(t.Tok, p.st))
|
var prev lexer.Token
|
||||||
|
if i > 0 {
|
||||||
|
prev = toks[i-1].Tok
|
||||||
|
}
|
||||||
|
nextIsLParen := i+1 < len(toks) && toks[i+1].Tok.Kind == lexer.LParen
|
||||||
|
b.WriteString(caseTextCtx(t.Tok, prev, nextIsLParen, p.st))
|
||||||
}
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
@@ -147,7 +235,8 @@ func (p *printer) inline(toks []cst.Tok) string {
|
|||||||
func (p *printer) leadingComments(lead cst.Trivia) {
|
func (p *printer) leadingComments(lead cst.Trivia) {
|
||||||
for _, tr := range lead {
|
for _, tr := range lead {
|
||||||
if tr.Kind == lexer.LineComment || tr.Kind == lexer.BlockComment {
|
if tr.Kind == lexer.LineComment || tr.Kind == lexer.BlockComment {
|
||||||
p.b.WriteString(strings.TrimRight(tr.Text, " \t"))
|
text := strings.TrimRight(strings.ReplaceAll(tr.Text, "\r", ""), " \t")
|
||||||
|
p.b.WriteString(text)
|
||||||
p.nl()
|
p.nl()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,7 +246,7 @@ func (p *printer) trailingComments(lead cst.Trivia) {
|
|||||||
cs := commentsOf(lead)
|
cs := commentsOf(lead)
|
||||||
for _, c := range cs {
|
for _, c := range cs {
|
||||||
p.nl()
|
p.nl()
|
||||||
p.b.WriteString(strings.TrimRight(c.Text, " \t"))
|
p.b.WriteString(strings.TrimRight(strings.ReplaceAll(c.Text, "\r", ""), " \t"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +255,31 @@ func (p *printer) trailingComments(lead cst.Trivia) {
|
|||||||
// tightOps are operators printed without surrounding spaces.
|
// tightOps are operators printed without surrounding spaces.
|
||||||
var tightOps = map[string]bool{"::": true, ":": true, "->": true, "->>": true}
|
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
|
// parenKws are keywords that always take a space before '(' because they
|
||||||
// introduce a subquery or a bracketed clause, not a function-call argument list.
|
// introduce a subquery or a bracketed clause, not a function-call argument list.
|
||||||
var parenKws = map[string]bool{
|
var parenKws = map[string]bool{
|
||||||
@@ -211,8 +325,14 @@ func needSpace(a, b lexer.Token) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func caseText(t lexer.Token, st config.Style) string {
|
func caseText(t lexer.Token, st config.Style) string {
|
||||||
|
return caseTextCtx(t, lexer.Token{}, false, st)
|
||||||
|
}
|
||||||
|
|
||||||
|
// caseTextCtx applies casing with context: prev is the preceding significant
|
||||||
|
// token, nextIsLParen indicates the next significant token is '('.
|
||||||
|
func caseTextCtx(t lexer.Token, prev lexer.Token, nextIsLParen bool, st config.Style) string {
|
||||||
if t.Kind != lexer.Ident {
|
if t.Kind != lexer.Ident {
|
||||||
return t.Text // only unquoted words are re-cased
|
return t.Text
|
||||||
}
|
}
|
||||||
low := lowerASCII(t.Text)
|
low := lowerASCII(t.Text)
|
||||||
switch {
|
switch {
|
||||||
@@ -220,6 +340,10 @@ func caseText(t lexer.Token, st config.Style) string {
|
|||||||
return applyCase(t.Text, st.TypeCase)
|
return applyCase(t.Text, st.TypeCase)
|
||||||
case isKeyword(low):
|
case isKeyword(low):
|
||||||
return applyCase(t.Text, st.KeywordCase)
|
return applyCase(t.Text, st.KeywordCase)
|
||||||
|
case nextIsLParen && isBuiltinFunc(low):
|
||||||
|
return applyCase(t.Text, st.BuiltinCase)
|
||||||
|
case prev.Kind == lexer.Ident && lowerASCII(prev.Text) == "as":
|
||||||
|
return applyCase(t.Text, st.AliasCase)
|
||||||
default:
|
default:
|
||||||
return applyCase(t.Text, st.IdentCase)
|
return applyCase(t.Text, st.IdentCase)
|
||||||
}
|
}
|
||||||
@@ -239,18 +363,25 @@ func applyCase(s string, c config.Case) string {
|
|||||||
// --- helpers ---
|
// --- helpers ---
|
||||||
|
|
||||||
// headerHasComments reports whether the function header carries comment trivia
|
// headerHasComments reports whether the function header carries comment trivia
|
||||||
// the formatter cannot confidently relocate. The first token's leading trivia
|
// the formatter cannot confidently relocate. Param separator (Sep) comments
|
||||||
// is excluded: that is the statement's leading comment, which File() emits
|
// are excluded — those are trailing inline comments on param lines that the
|
||||||
// separately. The body token's own text is excluded too (it is emitted
|
// formatter emits explicitly after each param text. The first token's leading
|
||||||
// verbatim), but a comment in front of the body is caught.
|
// trivia and the body token are also excluded.
|
||||||
func headerHasComments(cf *cst.CreateFunction) bool {
|
func headerHasComments(cf *cst.CreateFunction) bool {
|
||||||
|
// Build a set of Sep token offsets so we can skip them.
|
||||||
|
sepOffsets := make(map[int]bool, len(cf.Params))
|
||||||
|
for _, p := range cf.Params {
|
||||||
|
if p.Sep != nil {
|
||||||
|
sepOffsets[p.Sep.Tok.Off] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
all := cst.Tokens(cf)
|
all := cst.Tokens(cf)
|
||||||
for i, t := range all {
|
for i, t := range all {
|
||||||
if i == 0 {
|
if i == 0 || t.Tok.Kind == lexer.Semicolon {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if t.Tok.Kind == lexer.Semicolon {
|
if sepOffsets[t.Tok.Off] {
|
||||||
continue
|
continue // Sep comments handled separately
|
||||||
}
|
}
|
||||||
if hasComment(t) {
|
if hasComment(t) {
|
||||||
return true
|
return true
|
||||||
@@ -302,6 +433,27 @@ func verbatimSpan(toks []cst.Tok) string {
|
|||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// verbatimSpanFormatBody emits toks verbatim but replaces bodyTok's text with
|
||||||
|
// formatBody output. Used when the function header has comments we cannot
|
||||||
|
// safely relocate but the body can still be independently formatted.
|
||||||
|
// If bodyTok is nil the function is identical to verbatimSpan.
|
||||||
|
func verbatimSpanFormatBody(toks []cst.Tok, bodyTok *cst.Tok, st config.Style) string {
|
||||||
|
var b strings.Builder
|
||||||
|
for i, t := range toks {
|
||||||
|
if i > 0 {
|
||||||
|
for _, tr := range t.Lead {
|
||||||
|
b.WriteString(tr.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bodyTok != nil && t.Tok.Kind == lexer.DollarString && t.Tok.Off == bodyTok.Tok.Off {
|
||||||
|
b.WriteString(formatBody(t.Tok.Text, st))
|
||||||
|
} else {
|
||||||
|
b.WriteString(t.Tok.Text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
// hasBlankLine reports whether leading whitespace trivia contains a blank line
|
// hasBlankLine reports whether leading whitespace trivia contains a blank line
|
||||||
// (two or more newlines), indicating the author wanted statements separated.
|
// (two or more newlines), indicating the author wanted statements separated.
|
||||||
func hasBlankLine(lead cst.Trivia) bool {
|
func hasBlankLine(lead cst.Trivia) bool {
|
||||||
@@ -331,3 +483,66 @@ func lowerASCII(s string) string {
|
|||||||
}
|
}
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// alignParamTypes pads param names so the type column aligns across all params.
|
||||||
|
// Expected format per param: "[mode] name type [DEFAULT expr]".
|
||||||
|
// Mode keywords (IN/OUT/INOUT/VARIADIC) are detected and skipped.
|
||||||
|
// Params without a type are passed through unchanged.
|
||||||
|
func alignParamTypes(params []string) []string {
|
||||||
|
type pp struct{ mode, name, rest string }
|
||||||
|
parsed := make([]pp, len(params))
|
||||||
|
maxNameW := 0
|
||||||
|
|
||||||
|
modeKws := map[string]bool{"in": true, "out": true, "inout": true, "variadic": true}
|
||||||
|
|
||||||
|
for i, s := range params {
|
||||||
|
fields := strings.Fields(s)
|
||||||
|
if len(fields) < 2 {
|
||||||
|
parsed[i].rest = s
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nameIdx := 0
|
||||||
|
if modeKws[lowerASCII(fields[0])] {
|
||||||
|
nameIdx = 1
|
||||||
|
}
|
||||||
|
if nameIdx >= len(fields) || nameIdx+1 >= len(fields) {
|
||||||
|
// No type field — keep verbatim.
|
||||||
|
parsed[i].rest = s
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if nameIdx > 0 {
|
||||||
|
parsed[i].mode = fields[0]
|
||||||
|
}
|
||||||
|
parsed[i].name = fields[nameIdx]
|
||||||
|
parsed[i].rest = strings.Join(fields[nameIdx+1:], " ")
|
||||||
|
if len(parsed[i].name) > maxNameW {
|
||||||
|
maxNameW = len(parsed[i].name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxNameW == 0 {
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]string, len(params))
|
||||||
|
for i, p := range parsed {
|
||||||
|
if p.name == "" {
|
||||||
|
out[i] = params[i]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
if p.mode != "" {
|
||||||
|
b.WriteString(p.mode)
|
||||||
|
b.WriteByte(' ')
|
||||||
|
}
|
||||||
|
b.WriteString(p.name)
|
||||||
|
// Pad name to (maxNameW+1) so the type column starts at a consistent offset.
|
||||||
|
pad := maxNameW + 1 - len(p.name)
|
||||||
|
for k := 0; k < pad; k++ {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
}
|
||||||
|
b.WriteString(p.rest)
|
||||||
|
out[i] = b.String()
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
+110
-58
@@ -7,7 +7,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"git.warky.dev/wdevs/pgtidy/pkg/config"
|
"git.warky.dev/wdevs/pgtidy/pkg/config"
|
||||||
"git.warky.dev/wdevs/pgtidy/pkg/lexer"
|
|
||||||
"git.warky.dev/wdevs/pgtidy/pkg/parser"
|
"git.warky.dev/wdevs/pgtidy/pkg/parser"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,13 +23,13 @@ func TestFormatHeaderGolden(t *testing.T) {
|
|||||||
|
|
||||||
want := "--select * from dropall('resolvespec_login');\n" +
|
want := "--select * from dropall('resolvespec_login');\n" +
|
||||||
"CREATE OR REPLACE FUNCTION resolvespec_login(\n" +
|
"CREATE OR REPLACE FUNCTION resolvespec_login(\n" +
|
||||||
" INOUT p_data jsonb\n" +
|
" INOUT p_data jsonb\n" +
|
||||||
" ,OUT p_success boolean\n" +
|
", OUT p_success boolean\n" +
|
||||||
" ,OUT p_error text\n" +
|
", OUT p_error text\n" +
|
||||||
")\n" +
|
")\n" +
|
||||||
"LANGUAGE plpgsql\n" +
|
" LANGUAGE plpgsql\n" +
|
||||||
"VOLATILE\n" +
|
" VOLATILE\n" +
|
||||||
"SECURITY DEFINER\n" +
|
" SECURITY DEFINER\n" +
|
||||||
"AS\n" +
|
"AS\n" +
|
||||||
"$$\nbegin end;\n$$;\n"
|
"$$\nbegin end;\n$$;\n"
|
||||||
|
|
||||||
@@ -71,6 +70,102 @@ func TestFormatBodyBroken(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFormatMmProcBroken(t *testing.T) {
|
||||||
|
dir := filepath.Join("..", "..", "testdata", "corpus")
|
||||||
|
brokenData, err := os.ReadFile(filepath.Join(dir, "test_mm_proc_broken.pgsql"))
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("no test_mm_proc_broken.pgsql: %v", err)
|
||||||
|
}
|
||||||
|
goldenData, err := os.ReadFile(filepath.Join(dir, "test_mm_proc.pgsql"))
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("no test_mm_proc.pgsql: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := format(string(brokenData))
|
||||||
|
want := string(goldenData)
|
||||||
|
if got != want {
|
||||||
|
// Find and report the first differing line.
|
||||||
|
gotLines := strings.Split(got, "\n")
|
||||||
|
wantLines := strings.Split(want, "\n")
|
||||||
|
for i := 0; i < len(gotLines) && i < len(wantLines); i++ {
|
||||||
|
if gotLines[i] != wantLines[i] {
|
||||||
|
t.Errorf("format(test_mm_proc_broken) != test_mm_proc.pgsql at line %d\n got: %q\n want: %q", i+1, gotLines[i], wantLines[i])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(gotLines) != len(wantLines) {
|
||||||
|
t.Errorf("format(test_mm_proc_broken): got %d lines, want %d lines", len(gotLines), len(wantLines))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
twice := format(got)
|
||||||
|
if twice != got {
|
||||||
|
t.Errorf("format(test_mm_proc_broken) is not idempotent")
|
||||||
|
}
|
||||||
|
if !semanticallyEqual(string(brokenData), got) {
|
||||||
|
t.Errorf("format(test_mm_proc_broken) changed semantics")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatIssue1PLpgSQLIndenting(t *testing.T) {
|
||||||
|
src := "CREATE FUNCTION f() RETURNS void LANGUAGE plpgsql AS $$\n" +
|
||||||
|
"DECLARE\n" +
|
||||||
|
" r_lp record;\n" +
|
||||||
|
"BEGIN\n" +
|
||||||
|
" if r_lp.total > 0\n" +
|
||||||
|
" and r_lp.totaldone >= r_lp.total\n" +
|
||||||
|
" then\n" +
|
||||||
|
" update core.process u\n" +
|
||||||
|
" set status = 'done'\n" +
|
||||||
|
" where u.rid_process = r_lp.rid_process\n" +
|
||||||
|
" and nv(u.status) <> 'done';\n" +
|
||||||
|
" elsif r_lp.total > 0\n" +
|
||||||
|
" then\n" +
|
||||||
|
" update core.process u\n" +
|
||||||
|
" set status = 'open'\n" +
|
||||||
|
" where u.rid_process = r_lp.rid_process\n" +
|
||||||
|
" and nv(u.status) <> 'open';\n" +
|
||||||
|
"\n" +
|
||||||
|
" end if;\n" +
|
||||||
|
"$$;\n"
|
||||||
|
|
||||||
|
want := "CREATE FUNCTION f(\n" +
|
||||||
|
")\n" +
|
||||||
|
" RETURNS void\n" +
|
||||||
|
" LANGUAGE plpgsql\n" +
|
||||||
|
"AS\n" +
|
||||||
|
"$$\n" +
|
||||||
|
"DECLARE\n" +
|
||||||
|
" r_lp record;\n" +
|
||||||
|
"BEGIN\n" +
|
||||||
|
" if r_lp.total > 0\n" +
|
||||||
|
" and r_lp.totaldone >= r_lp.total\n" +
|
||||||
|
" then\n" +
|
||||||
|
" update core.process u\n" +
|
||||||
|
" set status = 'done'\n" +
|
||||||
|
" where\n" +
|
||||||
|
" u.rid_process = r_lp.rid_process\n" +
|
||||||
|
" and nv(u.status) <> 'done';\n" +
|
||||||
|
" elsif r_lp.total > 0\n" +
|
||||||
|
" then\n" +
|
||||||
|
" update core.process u\n" +
|
||||||
|
" set status = 'open'\n" +
|
||||||
|
" where\n" +
|
||||||
|
" u.rid_process = r_lp.rid_process\n" +
|
||||||
|
" and nv(u.status) <> 'open';\n" +
|
||||||
|
"\n" +
|
||||||
|
" end if;\n" +
|
||||||
|
"$$;\n"
|
||||||
|
|
||||||
|
got := format(src)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("issue #1 PL/pgSQL indenting\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
checkDML(t, "issue #1 PL/pgSQL indenting", got)
|
||||||
|
if !semanticallyEqual(src, got) {
|
||||||
|
t.Errorf("issue #1 PL/pgSQL indenting changed semantics")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCorpusIdempotentAndSafe(t *testing.T) {
|
func TestCorpusIdempotentAndSafe(t *testing.T) {
|
||||||
dir := filepath.Join("..", "..", "testdata", "corpus")
|
dir := filepath.Join("..", "..", "testdata", "corpus")
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
@@ -79,7 +174,7 @@ func TestCorpusIdempotentAndSafe(t *testing.T) {
|
|||||||
}
|
}
|
||||||
var seen int
|
var seen int
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
if e.IsDir() || !strings.HasSuffix(e.Name(), ".pgsql") {
|
if e.IsDir() || !strings.HasSuffix(e.Name(), ".pgsql") || strings.HasSuffix(e.Name(), "_broken.pgsql") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen++
|
seen++
|
||||||
@@ -89,62 +184,19 @@ func TestCorpusIdempotentAndSafe(t *testing.T) {
|
|||||||
}
|
}
|
||||||
src := string(data)
|
src := string(data)
|
||||||
once := format(src)
|
once := format(src)
|
||||||
twice := format(once)
|
// VerifySafe bundles every runtime gate: semantic equivalence, comment
|
||||||
if once != twice {
|
// preservation, structural balance, and idempotence.
|
||||||
t.Errorf("%s: not idempotent", e.Name())
|
if err := VerifySafe(src, once, config.Default()); err != nil {
|
||||||
}
|
t.Errorf("%s: %v", e.Name(), err)
|
||||||
if !semanticallyEqual(src, once) {
|
|
||||||
t.Errorf("%s: formatting changed semantics", e.Name())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if seen == 0 {
|
if seen == 0 {
|
||||||
t.Skip("no corpus files")
|
t.Skip("no corpus files")
|
||||||
}
|
}
|
||||||
t.Logf("formatted %d corpus files (idempotent + semantically equal)", seen)
|
t.Logf("verified %d corpus files (semantic + comments + structure + idempotence)", seen)
|
||||||
}
|
}
|
||||||
|
|
||||||
// semanticallyEqual compares the non-trivia token streams of two sources,
|
// semanticallyEqual is a test-local alias for the exported safety check.
|
||||||
// treating unquoted identifiers/keywords case-insensitively and everything
|
|
||||||
// else (strings, numbers, operators, punctuation) exactly. Dollar-quoted body
|
|
||||||
// tokens are compared recursively so body whitespace normalization does not
|
|
||||||
// trigger a false failure.
|
|
||||||
func semanticallyEqual(a, b string) bool {
|
func semanticallyEqual(a, b string) bool {
|
||||||
ta := significant(a)
|
return SemanticallyEqual(a, b)
|
||||||
tb := significant(b)
|
|
||||||
if len(ta) != len(tb) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i := range ta {
|
|
||||||
if ta[i].Kind != tb[i].Kind {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch ta[i].Kind {
|
|
||||||
case lexer.Ident:
|
|
||||||
if !strings.EqualFold(ta[i].Text, tb[i].Text) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case lexer.DollarString:
|
|
||||||
_, innerA, _, okA := splitDollarQuote(ta[i].Text)
|
|
||||||
_, innerB, _, okB := splitDollarQuote(tb[i].Text)
|
|
||||||
if okA != okB || (okA && !semanticallyEqual(innerA, innerB)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if ta[i].Text != tb[i].Text {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func significant(src string) []lexer.Token {
|
|
||||||
var out []lexer.Token
|
|
||||||
for _, t := range lexer.Lex(src) {
|
|
||||||
if t.Kind == lexer.EOF || t.IsTrivia() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, t)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-2
@@ -51,5 +51,23 @@ func words(s string) map[string]bool {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func isKeyword(lower string) bool { return keywords[lower] }
|
// builtinFunctions are built-in function names controlled by BuiltinCase.
|
||||||
func isTypeName(lower string) bool { return typeNames[lower] }
|
var builtinFunctions = words(`
|
||||||
|
abs age array_agg array_length array_lower array_ndims array_upper
|
||||||
|
bit_length btrim cardinality ceil ceiling char_length character_length
|
||||||
|
chr clock_timestamp coalesce concat concat_ws count
|
||||||
|
currval decode div encode exp extract floor
|
||||||
|
generate_series greatest initcap jsonb_agg jsonb_object_agg
|
||||||
|
justify_days justify_hours justify_interval
|
||||||
|
lastval least length lower lpad ltrim
|
||||||
|
max md5 min mod now nullif
|
||||||
|
overlay pg_sleep position power quote_ident quote_literal
|
||||||
|
random regexp_match regexp_matches regexp_replace replace reverse round rpad rtrim
|
||||||
|
setval split_part sqrt string_agg strpos substr substring sum
|
||||||
|
to_char to_date to_json to_jsonb to_number to_timestamp to_tsvector
|
||||||
|
translate trim trunc unnest upper width_bucket
|
||||||
|
`)
|
||||||
|
|
||||||
|
func isKeyword(lower string) bool { return keywords[lower] }
|
||||||
|
func isTypeName(lower string) bool { return typeNames[lower] }
|
||||||
|
func isBuiltinFunc(lower string) bool { return builtinFunctions[lower] }
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
package format
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.warky.dev/wdevs/pgtidy/pkg/config"
|
||||||
|
"git.warky.dev/wdevs/pgtidy/pkg/lexer"
|
||||||
|
"git.warky.dev/wdevs/pgtidy/pkg/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VerifySafe runs every safety invariant against a formatting result before it
|
||||||
|
// is written to disk or returned to an editor. src is the original input, out
|
||||||
|
// the formatted output, and st the style out was produced with. It returns nil
|
||||||
|
// when out is safe to emit, otherwise an error naming the invariant that failed.
|
||||||
|
//
|
||||||
|
// The checks, in order of cost:
|
||||||
|
//
|
||||||
|
// 1. Semantic equivalence — the non-trivia (code) token stream is unchanged:
|
||||||
|
// identifiers/keywords compare case-insensitively, everything else exactly,
|
||||||
|
// recursing into dollar-quoted bodies. Comments and whitespace are trivia
|
||||||
|
// and are deliberately ignored here.
|
||||||
|
// 2. Comment preservation — every -- and /* */ comment in src reappears in out,
|
||||||
|
// in the same order, with the same content (ignoring only trailing
|
||||||
|
// whitespace and CRLF/LF). The formatter may move or re-indent a comment but
|
||||||
|
// must never drop, merge, split, or reword one.
|
||||||
|
// 3. Structural balance — the ( ) [ ] and BEGIN/CASE/IF/LOOP…END nesting
|
||||||
|
// profile of out matches src's, counting only real code tokens (anything
|
||||||
|
// inside a comment or a string/dollar-quoted literal is ignored).
|
||||||
|
// 4. Idempotence — formatting out again yields out unchanged.
|
||||||
|
//
|
||||||
|
// Any failure means the formatter has a bug: the caller must keep the original
|
||||||
|
// source and never emit out.
|
||||||
|
func VerifySafe(src, out string, st config.Style) error {
|
||||||
|
if !SemanticallyEqual(src, out) {
|
||||||
|
return fmt.Errorf("code token stream changed")
|
||||||
|
}
|
||||||
|
if err := CommentsPreserved(src, out); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := StructurallyBalanced(src, out); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if reformatted := File(parser.Parse(out), st); reformatted != out {
|
||||||
|
return fmt.Errorf("output is not idempotent (a second format pass would change it)")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SemanticallyEqual reports whether a and b have the same non-trivia token
|
||||||
|
// stream, i.e. formatting may only ever change whitespace/comment trivia and
|
||||||
|
// layout — it must never add, remove, or alter a token of actual code.
|
||||||
|
// Unquoted identifiers/keywords compare case-insensitively (casing is a
|
||||||
|
// style choice); everything else (strings, numbers, operators, punctuation)
|
||||||
|
// must match exactly. Dollar-quoted body tokens are compared recursively so
|
||||||
|
// that independent body reformatting doesn't trigger a false failure.
|
||||||
|
//
|
||||||
|
// The CLI and LSP must call this (via VerifySafe) before ever writing or
|
||||||
|
// emitting formatted output: if it returns false, the formatter has a bug and
|
||||||
|
// the original source must be kept, never the (corrupting) formatted output.
|
||||||
|
func SemanticallyEqual(a, b string) bool {
|
||||||
|
ta := significantTokens(a)
|
||||||
|
tb := significantTokens(b)
|
||||||
|
if len(ta) != len(tb) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range ta {
|
||||||
|
if ta[i].Kind != tb[i].Kind {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch ta[i].Kind {
|
||||||
|
case lexer.Ident:
|
||||||
|
if !strings.EqualFold(ta[i].Text, tb[i].Text) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case lexer.String, lexer.EscapeString, lexer.BitString, lexer.HexString, lexer.UnicodeString:
|
||||||
|
// A CRLF vs LF difference inside a multi-line string literal is a
|
||||||
|
// line-ending normalisation, not a change of code content — the
|
||||||
|
// formatter always re-emits layout with st.Newline. Compare the
|
||||||
|
// literal modulo \r\n ↔ \n.
|
||||||
|
if normNL(ta[i].Text) != normNL(tb[i].Text) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case lexer.DollarString:
|
||||||
|
_, innerA, _, okA := splitDollarQuote(ta[i].Text)
|
||||||
|
_, innerB, _, okB := splitDollarQuote(tb[i].Text)
|
||||||
|
if okA != okB || (okA && !SemanticallyEqual(innerA, innerB)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if ta[i].Text != tb[i].Text {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommentsPreserved reports whether every comment in a survives into b with its
|
||||||
|
// text intact. Comments are compared in document order; each is reduced to its
|
||||||
|
// sequence of non-blank text lines (line endings normalised, every line trimmed
|
||||||
|
// of surrounding whitespace, blank lines dropped) so that the formatter is free
|
||||||
|
// to move or re-indent a comment but can never drop, merge, split, reorder, or
|
||||||
|
// reword one. Comments inside dollar-quoted bodies are included (the bodies are
|
||||||
|
// lexed recursively). A non-nil error describes the first divergence.
|
||||||
|
func CommentsPreserved(a, b string) error {
|
||||||
|
ca := comments(a)
|
||||||
|
cb := comments(b)
|
||||||
|
if len(ca) != len(cb) {
|
||||||
|
return fmt.Errorf("comment count changed: input has %d, output has %d", len(ca), len(cb))
|
||||||
|
}
|
||||||
|
for i := range ca {
|
||||||
|
if ca[i] != cb[i] {
|
||||||
|
return fmt.Errorf("comment %d/%d changed:\n input: %q\n output: %q", i+1, len(ca), ca[i], cb[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// comments returns the normalised text of every -- and /* */ comment in src, in
|
||||||
|
// order, descending into dollar-quoted bodies.
|
||||||
|
func comments(src string) []string {
|
||||||
|
var out []string
|
||||||
|
for _, t := range lexer.Lex(src) {
|
||||||
|
switch t.Kind {
|
||||||
|
case lexer.LineComment, lexer.BlockComment:
|
||||||
|
out = append(out, normComment(t.Text))
|
||||||
|
case lexer.DollarString:
|
||||||
|
if _, inner, _, ok := splitDollarQuote(t.Text); ok {
|
||||||
|
out = append(out, comments(inner)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// normComment canonicalises a comment token to its content — the ordered list of
|
||||||
|
// non-blank text lines, each stripped of surrounding whitespace, joined with LF.
|
||||||
|
// Line endings and indentation are layout, not content, so they are discarded;
|
||||||
|
// dropping or rewording an actual line of comment text still shows up.
|
||||||
|
func normComment(s string) string {
|
||||||
|
s = strings.ReplaceAll(s, "\r\n", "\n")
|
||||||
|
s = strings.ReplaceAll(s, "\r", "\n")
|
||||||
|
var lines []string
|
||||||
|
for _, ln := range strings.Split(s, "\n") {
|
||||||
|
if ln = strings.TrimSpace(ln); ln != "" {
|
||||||
|
lines = append(lines, ln)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// StructurallyBalanced reports whether a and b have the same delimiter and block
|
||||||
|
// nesting profile: identical counts of ( ) [ ] and of the PL/pgSQL block
|
||||||
|
// keywords BEGIN / CASE / IF / LOOP / END (and the compound END IF / END LOOP /
|
||||||
|
// END CASE), plus an identical running paren/bracket depth trace. Only real code
|
||||||
|
// tokens are counted — anything inside a -- or /* */ comment is trivia and is
|
||||||
|
// skipped, and string / dollar-quoted literals are opaque single tokens whose
|
||||||
|
// contents never register (dollar-quoted bodies are recursed into separately).
|
||||||
|
//
|
||||||
|
// Given SemanticallyEqual, this is defence in depth: an independent re-count
|
||||||
|
// with different code that catches a structural token slipping through a bug in
|
||||||
|
// the token-stream comparison (e.g. its dollar-quote or CRLF handling), and it
|
||||||
|
// pins down *where* the structure broke.
|
||||||
|
func StructurallyBalanced(a, b string) error {
|
||||||
|
pa := structureProfile(a)
|
||||||
|
pb := structureProfile(b)
|
||||||
|
if pa.parenDepthTrace != pb.parenDepthTrace {
|
||||||
|
return fmt.Errorf("parenthesis/bracket nesting changed")
|
||||||
|
}
|
||||||
|
for _, k := range structureKeys {
|
||||||
|
if pa.counts[k] != pb.counts[k] {
|
||||||
|
return fmt.Errorf("structural token %q count changed: input %d, output %d", k, pa.counts[k], pb.counts[k])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var structureKeys = []string{"(", ")", "[", "]", "begin", "case", "if", "loop", "end", "end if", "end loop", "end case"}
|
||||||
|
|
||||||
|
type structProfile struct {
|
||||||
|
counts map[string]int
|
||||||
|
// parenDepthTrace is the sequence of running ( ) [ ] depths after each
|
||||||
|
// bracket token, joined with commas — a compact fingerprint of the nesting
|
||||||
|
// shape that diverges as soon as an open/close is added, dropped, or moved.
|
||||||
|
parenDepthTrace string
|
||||||
|
}
|
||||||
|
|
||||||
|
func structureProfile(src string) structProfile {
|
||||||
|
p := structProfile{counts: map[string]int{}}
|
||||||
|
var trace strings.Builder
|
||||||
|
depth := 0
|
||||||
|
toks := significantTokens(src) // trivia (comments/whitespace) already excluded
|
||||||
|
for i := 0; i < len(toks); i++ {
|
||||||
|
t := toks[i]
|
||||||
|
switch t.Kind {
|
||||||
|
case lexer.LParen:
|
||||||
|
p.counts["("]++
|
||||||
|
depth++
|
||||||
|
fmt.Fprintf(&trace, "%d,", depth)
|
||||||
|
case lexer.RParen:
|
||||||
|
p.counts[")"]++
|
||||||
|
depth--
|
||||||
|
fmt.Fprintf(&trace, "%d,", depth)
|
||||||
|
case lexer.LBracket:
|
||||||
|
p.counts["["]++
|
||||||
|
depth++
|
||||||
|
fmt.Fprintf(&trace, "%d,", depth)
|
||||||
|
case lexer.RBracket:
|
||||||
|
p.counts["]"]++
|
||||||
|
depth--
|
||||||
|
fmt.Fprintf(&trace, "%d,", depth)
|
||||||
|
case lexer.Ident:
|
||||||
|
switch lowerASCII(t.Text) {
|
||||||
|
case "begin", "case", "if", "loop":
|
||||||
|
p.counts[lowerASCII(t.Text)]++
|
||||||
|
case "end":
|
||||||
|
p.counts["end"]++
|
||||||
|
if i+1 < len(toks) && toks[i+1].Kind == lexer.Ident {
|
||||||
|
switch lowerASCII(toks[i+1].Text) {
|
||||||
|
case "if":
|
||||||
|
p.counts["end if"]++
|
||||||
|
case "loop":
|
||||||
|
p.counts["end loop"]++
|
||||||
|
case "case":
|
||||||
|
p.counts["end case"]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case lexer.DollarString:
|
||||||
|
if _, inner, _, ok := splitDollarQuote(t.Text); ok {
|
||||||
|
sub := structureProfile(inner)
|
||||||
|
for _, k := range structureKeys {
|
||||||
|
p.counts[k] += sub.counts[k]
|
||||||
|
}
|
||||||
|
trace.WriteString("[" + sub.parenDepthTrace + "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.parenDepthTrace = trace.String()
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// normNL collapses CRLF to LF so string literals compare independent of the
|
||||||
|
// source file's line-ending convention.
|
||||||
|
func normNL(s string) string { return strings.ReplaceAll(s, "\r\n", "\n") }
|
||||||
|
|
||||||
|
// significantTokens lexes src and returns its tokens excluding EOF and trivia
|
||||||
|
// (whitespace/comments).
|
||||||
|
func significantTokens(src string) []lexer.Token {
|
||||||
|
var out []lexer.Token
|
||||||
|
for _, t := range lexer.Lex(src) {
|
||||||
|
if t.Kind == lexer.EOF || t.IsTrivia() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, t)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
-7
@@ -89,12 +89,12 @@ func (s *server) handle(raw []byte) bool {
|
|||||||
return true
|
return true
|
||||||
case "textDocument/didOpen":
|
case "textDocument/didOpen":
|
||||||
var p didOpenParams
|
var p didOpenParams
|
||||||
json.Unmarshal(req.Params, &p)
|
_ = json.Unmarshal(req.Params, &p)
|
||||||
s.docs[p.TextDocument.URI] = p.TextDocument.Text
|
s.docs[p.TextDocument.URI] = p.TextDocument.Text
|
||||||
s.pushDiagnostics(p.TextDocument.URI, p.TextDocument.Text)
|
s.pushDiagnostics(p.TextDocument.URI, p.TextDocument.Text)
|
||||||
case "textDocument/didChange":
|
case "textDocument/didChange":
|
||||||
var p didChangeParams
|
var p didChangeParams
|
||||||
json.Unmarshal(req.Params, &p)
|
_ = json.Unmarshal(req.Params, &p)
|
||||||
if len(p.ContentChanges) > 0 {
|
if len(p.ContentChanges) > 0 {
|
||||||
text := p.ContentChanges[len(p.ContentChanges)-1].Text
|
text := p.ContentChanges[len(p.ContentChanges)-1].Text
|
||||||
s.docs[p.TextDocument.URI] = text
|
s.docs[p.TextDocument.URI] = text
|
||||||
@@ -104,7 +104,7 @@ func (s *server) handle(raw []byte) bool {
|
|||||||
var p struct {
|
var p struct {
|
||||||
TextDocument textDocID `json:"textDocument"`
|
TextDocument textDocID `json:"textDocument"`
|
||||||
}
|
}
|
||||||
json.Unmarshal(req.Params, &p)
|
_ = json.Unmarshal(req.Params, &p)
|
||||||
delete(s.docs, p.TextDocument.URI)
|
delete(s.docs, p.TextDocument.URI)
|
||||||
delete(s.fixes, p.TextDocument.URI)
|
delete(s.fixes, p.TextDocument.URI)
|
||||||
s.notify("textDocument/publishDiagnostics", publishDiagnosticsParams{
|
s.notify("textDocument/publishDiagnostics", publishDiagnosticsParams{
|
||||||
@@ -113,7 +113,7 @@ func (s *server) handle(raw []byte) bool {
|
|||||||
})
|
})
|
||||||
case "textDocument/formatting":
|
case "textDocument/formatting":
|
||||||
var p formattingParams
|
var p formattingParams
|
||||||
json.Unmarshal(req.Params, &p)
|
_ = json.Unmarshal(req.Params, &p)
|
||||||
text, ok := s.docs[p.TextDocument.URI]
|
text, ok := s.docs[p.TextDocument.URI]
|
||||||
if !ok {
|
if !ok {
|
||||||
s.reply(req.ID, []textEdit{})
|
s.reply(req.ID, []textEdit{})
|
||||||
@@ -124,10 +124,14 @@ func (s *server) handle(raw []byte) bool {
|
|||||||
s.reply(req.ID, []textEdit{})
|
s.reply(req.ID, []textEdit{})
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if err := format.VerifySafe(text, formatted, s.cfg); err != nil {
|
||||||
|
s.reply(req.ID, []textEdit{})
|
||||||
|
return false
|
||||||
|
}
|
||||||
s.reply(req.ID, []textEdit{fullReplace(text, formatted)})
|
s.reply(req.ID, []textEdit{fullReplace(text, formatted)})
|
||||||
case "textDocument/rangeFormatting":
|
case "textDocument/rangeFormatting":
|
||||||
var p rangeFormattingParams
|
var p rangeFormattingParams
|
||||||
json.Unmarshal(req.Params, &p)
|
_ = json.Unmarshal(req.Params, &p)
|
||||||
text, ok := s.docs[p.TextDocument.URI]
|
text, ok := s.docs[p.TextDocument.URI]
|
||||||
if !ok {
|
if !ok {
|
||||||
s.reply(req.ID, []textEdit{})
|
s.reply(req.ID, []textEdit{})
|
||||||
@@ -136,7 +140,7 @@ func (s *server) handle(raw []byte) bool {
|
|||||||
s.reply(req.ID, s.rangeFormat(text, p.Range))
|
s.reply(req.ID, s.rangeFormat(text, p.Range))
|
||||||
case "textDocument/codeAction":
|
case "textDocument/codeAction":
|
||||||
var p codeActionParams
|
var p codeActionParams
|
||||||
json.Unmarshal(req.Params, &p)
|
_ = json.Unmarshal(req.Params, &p)
|
||||||
s.handleCodeAction(req.ID, p)
|
s.handleCodeAction(req.ID, p)
|
||||||
case "$/cancelRequest": // ignore
|
case "$/cancelRequest": // ignore
|
||||||
default:
|
default:
|
||||||
@@ -224,6 +228,9 @@ func (s *server) rangeFormat(text string, r lspRange) []textEdit {
|
|||||||
if formatted == text {
|
if formatted == text {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if err := format.VerifySafe(text, formatted, s.cfg); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Split both versions into lines, keeping the trailing newline attached to
|
// Split both versions into lines, keeping the trailing newline attached to
|
||||||
// each element so that joining them reconstructs the original string.
|
// each element so that joining them reconstructs the original string.
|
||||||
@@ -360,7 +367,7 @@ func (s *server) send(v interface{}) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Fprintf(s.w, "Content-Length: %d\r\n\r\n", len(data))
|
_, _ = fmt.Fprintf(s.w, "Content-Length: %d\r\n\r\n", len(data))
|
||||||
s.w.Write(data) //nolint:errcheck
|
s.w.Write(data) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func readResp(t *testing.T, buf *bytes.Buffer) map[string]interface{} {
|
|||||||
}
|
}
|
||||||
lenStr := string(data[idx+16 : idx+eol])
|
lenStr := string(data[idx+16 : idx+eol])
|
||||||
var n int
|
var n int
|
||||||
fmt.Sscanf(lenStr, "%d", &n)
|
_, _ = fmt.Sscanf(lenStr, "%d", &n)
|
||||||
sep := bytes.Index(data, []byte("\r\n\r\n"))
|
sep := bytes.Index(data, []byte("\r\n\r\n"))
|
||||||
if sep < 0 || len(data) < sep+4+n {
|
if sep < 0 || len(data) < sep+4+n {
|
||||||
time.Sleep(5 * time.Millisecond)
|
time.Sleep(5 * time.Millisecond)
|
||||||
@@ -60,7 +60,7 @@ func readResp(t *testing.T, buf *bytes.Buffer) map[string]interface{} {
|
|||||||
body := data[sep+4 : sep+4+n]
|
body := data[sep+4 : sep+4+n]
|
||||||
buf.Next(sep + 4 + n)
|
buf.Next(sep + 4 + n)
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
json.Unmarshal(body, &result)
|
_ = json.Unmarshal(body, &result)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
t.Fatal("timeout waiting for response")
|
t.Fatal("timeout waiting for response")
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ func parseCreateFunction(stmt []cst.Tok) (*cst.CreateFunction, bool) {
|
|||||||
if i+1 < len(stmt) && stmt[i].Is("or") && stmt[i+1].Is("replace") {
|
if i+1 < len(stmt) && stmt[i].Is("or") && stmt[i+1].Is("replace") {
|
||||||
i += 2
|
i += 2
|
||||||
}
|
}
|
||||||
if i >= len(stmt) || !(stmt[i].Is("function") || stmt[i].Is("procedure")) {
|
if i >= len(stmt) || (!stmt[i].Is("function") && !stmt[i].Is("procedure")) {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ func FirstTokenOffset(sql string, start int) int {
|
|||||||
}
|
}
|
||||||
case i+1 < n && sql[i] == '/' && sql[i+1] == '*':
|
case i+1 < n && sql[i] == '/' && sql[i+1] == '*':
|
||||||
i += 2
|
i += 2
|
||||||
for i+1 < n && !(sql[i] == '*' && sql[i+1] == '/') {
|
for i+1 < n && (sql[i] != '*' || sql[i+1] != '/') {
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
if i+1 < n {
|
if i+1 < n {
|
||||||
|
|||||||
+797
@@ -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
@@ -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
|
||||||
|
|
||||||
|
*/
|
||||||
+1105
File diff suppressed because it is too large
Load Diff
+613
@@ -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'
|
||||||
Vendored
+17
-19
@@ -1,28 +1,28 @@
|
|||||||
--select * from dropall('resolvespec_login');
|
--select * from dropall('resolvespec_login');
|
||||||
CREATE OR REPLACE FUNCTION resolvespec_login(
|
CREATE OR REPLACE FUNCTION resolvespec_login(
|
||||||
INOUT p_data jsonb
|
INOUT p_data jsonb
|
||||||
,OUT p_success boolean
|
, OUT p_success boolean
|
||||||
,OUT p_error text
|
, OUT p_error text
|
||||||
)
|
)
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
VOLATILE
|
VOLATILE
|
||||||
SECURITY DEFINER
|
SECURITY DEFINER
|
||||||
AS
|
AS
|
||||||
$$
|
$$
|
||||||
DECLARE
|
DECLARE
|
||||||
--Error Handling--
|
--Error Handling--
|
||||||
m_funcname text = 'resolvespec_login';
|
m_funcname text = 'resolvespec_login';
|
||||||
m_errmsg text;
|
m_errmsg text;
|
||||||
m_errcontext text;
|
m_errcontext text;
|
||||||
m_errdetail text;
|
m_errdetail text;
|
||||||
m_errhint text;
|
m_errhint text;
|
||||||
m_errstate text;
|
m_errstate text;
|
||||||
m_retval integer;
|
m_retval integer;
|
||||||
--Error Handling--
|
--Error Handling--
|
||||||
m_rid_user integer;
|
m_rid_user integer;
|
||||||
m_rid_hub integer;
|
m_rid_hub integer;
|
||||||
m_pass_hashed citext[];
|
m_pass_hashed citext[];
|
||||||
m_session jsonb;
|
m_session jsonb;
|
||||||
m_allow_hash_auth boolean;
|
m_allow_hash_auth boolean;
|
||||||
BEGIN
|
BEGIN
|
||||||
m_allow_hash_auth = _try_integer( p_data->'claims'->>'rid_user',0) > 0;
|
m_allow_hash_auth = _try_integer( p_data->'claims'->>'rid_user',0) > 0;
|
||||||
@@ -134,8 +134,6 @@ BEGIN
|
|||||||
where u.rid_hub = m_rid_hub;
|
where u.rid_hub = m_rid_hub;
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
select jsonb_build_object('token', m_session ->>'token'
|
select jsonb_build_object('token', m_session ->>'token'
|
||||||
, 'session', m_session ->>'session'
|
, 'session', m_session ->>'session'
|
||||||
, 'user', _jsonb_object_cat(jsonb_build_object(
|
, 'user', _jsonb_object_cat(jsonb_build_object(
|
||||||
|
|||||||
Vendored
+158
@@ -0,0 +1,158 @@
|
|||||||
|
DO
|
||||||
|
$$
|
||||||
|
DECLARE
|
||||||
|
m_programtype citext;
|
||||||
|
m_ins integer;
|
||||||
|
m_upd integer;
|
||||||
|
m_retval bigint;
|
||||||
|
BEGIN
|
||||||
|
|
||||||
|
select s.setvalue
|
||||||
|
from core.setting s
|
||||||
|
where s.setname = 'programtype'
|
||||||
|
and nv(s.disableflag) = 0
|
||||||
|
into m_programtype;
|
||||||
|
|
||||||
|
--select newid()
|
||||||
|
insert into core.mastertype(category, mastertype, description, inactive, forprefix,guid)
|
||||||
|
select *
|
||||||
|
from (
|
||||||
|
values
|
||||||
|
('module','system','Required System Module',0,'','2D4EFDEA-4E7D-4998-8E2A-1F2B67770983')
|
||||||
|
) r(category, mastertype, description, inactive, forprefix,guid)
|
||||||
|
where not exists (
|
||||||
|
select 1 from core.mastertype mt
|
||||||
|
where mt.mastertype = r.mastertype
|
||||||
|
and mt.category = r.category
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
update core.mastertype u
|
||||||
|
set jsonvalue = _jsonb_object_cat(u.jsonvalue,jsonb_build_object('subtypes',jsonb_build_array('mergetype','mergetargettype')))
|
||||||
|
where u.category = 'docgentype'
|
||||||
|
and u.mastertype = 'merge'
|
||||||
|
;
|
||||||
|
|
||||||
|
----Merge Target Type
|
||||||
|
with src(category, mastertype, description, inactive, forprefix,guid,rid_parent) as (
|
||||||
|
select *
|
||||||
|
, (
|
||||||
|
select mt.rid_mastertype from core.mastertype mt where mt.guid = '5004E9EC-2E4B-4B36-814C-CFD96EFB434B' limit 1
|
||||||
|
) --docvault
|
||||||
|
from (
|
||||||
|
values ('mergetargettype', 'html', 'HTML', 0, '', '479C8772-52C5-418F-B4FD-FA5F511831E8')
|
||||||
|
, ('mergetargettype', 'docx', 'Word Document', 0, '', '4899A6E6-467F-41BE-95EE-C3A3938C57C1')
|
||||||
|
, ('mergetargettype', 'xlsx', 'Excel Document', 0, '', 'AB40EC69-9CEE-4834-A1AC-B317CC47D679')
|
||||||
|
, ('mergetargettype', 'csv', 'Comma Separated Values (CSV)', 0, '', 'D8EE416A-6A40-4F09-80A7-2576278512AA')
|
||||||
|
, ('mergetargettype', 'pdf', 'PDF', 0, '', '3776F161-1BF8-474C-8C1A-097D851D6BF9')
|
||||||
|
, ('mergetargettype', 'same', 'Same as Source', 0, '', 'C7797F0B-5702-4B54-B102-0B610D62730C')
|
||||||
|
) r(category, mastertype, description, inactive, forprefix, guid)
|
||||||
|
), ins as (
|
||||||
|
insert into core.mastertype (category, mastertype, description, inactive, forprefix, guid, rid_parent)
|
||||||
|
select category, mastertype, description, inactive, forprefix, guid, rid_parent
|
||||||
|
from src
|
||||||
|
where
|
||||||
|
not exists (
|
||||||
|
select 1
|
||||||
|
from core.mastertype mt2
|
||||||
|
where
|
||||||
|
mt2.category = src.category
|
||||||
|
and mt2.mastertype = src.mastertype
|
||||||
|
)
|
||||||
|
returning mastertype.*
|
||||||
|
), upd as (
|
||||||
|
update core.mastertype u
|
||||||
|
set description = s.description
|
||||||
|
, inactive = s.inactive
|
||||||
|
, forprefix = s.forprefix
|
||||||
|
, guid = s.guid
|
||||||
|
, rid_parent = s.rid_parent
|
||||||
|
from src s
|
||||||
|
where u.category = s.category
|
||||||
|
and u.mastertype = s.mastertype
|
||||||
|
and (
|
||||||
|
u.description is distinct from s.description
|
||||||
|
or u.inactive is distinct from s.inactive
|
||||||
|
or u.forprefix is distinct from s.forprefix
|
||||||
|
or u.guid is distinct from s.guid
|
||||||
|
or u.rid_parent is distinct from s.rid_parent
|
||||||
|
)
|
||||||
|
returning u.*
|
||||||
|
)
|
||||||
|
select (select count(1) from ins )
|
||||||
|
,(select count(1) from upd )
|
||||||
|
into m_ins, m_upd
|
||||||
|
;
|
||||||
|
|
||||||
|
----Merge Type
|
||||||
|
insert into core.mastertype(category, mastertype, description, inactive, forprefix,guid,rid_parent)
|
||||||
|
select *
|
||||||
|
,(select mt.rid_mastertype from core.mastertype mt where mt.guid = '5004E9EC-2E4B-4B36-814C-CFD96EFB434B' limit 1) --docvault
|
||||||
|
from (
|
||||||
|
values
|
||||||
|
('mergetype','html','HTML Template',0,'','DC3588BC-1865-400D-BE46-8FABE6F9A040')
|
||||||
|
,('mergetype','docx','Word Document Template',0,'','8535952E-3D9D-4DD4-9BD0-086A8E5A4804')
|
||||||
|
,('mergetype','xlsx','Excel Document Template',0,'','6678B42F-C06F-4CB6-9003-AF22E80A1295')
|
||||||
|
,('mergetype','csv','Comma Separated Values (CSV)',0,'','F03F0AC0-D710-4F9D-BB5C-0EAA97245E2A')
|
||||||
|
,('mergetype','pdf','PDF Fill in Template',1,'','1197CD7A-D2F6-4B01-8B83-E42315E0060C')
|
||||||
|
,('mergetype','stimulsoft','Stimulsoft Template',0,'','491ECBF2-6AD2-452C-8543-B15C65CB7431')
|
||||||
|
,('mergetype','sql','SQL Query Template',0,'','74A47C47-2759-4FAB-A2A1-0A42FA87FA46')
|
||||||
|
,('mergetype','none','None / No Merge',0,'','640B5044-DDCE-4806-A64F-098BE1324693')
|
||||||
|
) r(category, mastertype, description, inactive, forprefix,guid)
|
||||||
|
where not exists (
|
||||||
|
select 1 from core.mastertype mt2
|
||||||
|
where mt2.category = r.category
|
||||||
|
and mt2.mastertype = r.mastertype
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-----eventtype --select newid()
|
||||||
|
with src as (
|
||||||
|
select r.*
|
||||||
|
, mt.rid_mastertype as rid_parent
|
||||||
|
from (
|
||||||
|
values ('eventreactioncodetype', 'sql', 'SQL', 0, '', '24AE5A98-024F-4A3D-8BED-9F4A2C410BF9')
|
||||||
|
, ('eventreactioncodetype', 'api', 'API', 1, '', '163A3EC1-6C2A-4462-A15F-D24F953F9CDA')
|
||||||
|
, ('eventreactioncodetype', 'frontend', 'Frontend', 0, '', '50023BB9-A58A-4AE8-AAC9-3126D7122D18')
|
||||||
|
, ('eventreactioncodetype', 'json', 'JSON', 0, '', 'FA331BC2-DCB2-44E0-B835-C62C77A547E3')
|
||||||
|
|
||||||
|
) r(category, mastertype, description, inactive, forprefix, guid)
|
||||||
|
cross join (
|
||||||
|
select mt.rid_mastertype
|
||||||
|
from core.mastertype mt
|
||||||
|
where mt.guid = '2D4EFDEA-4E7D-4998-8E2A-1F2B67770983'
|
||||||
|
limit 1
|
||||||
|
) mt
|
||||||
|
), upd as (
|
||||||
|
update core.mastertype u
|
||||||
|
set mastertype = src.mastertype
|
||||||
|
,description = src.description
|
||||||
|
,inactive = src.inactive
|
||||||
|
,forprefix = src.forprefix
|
||||||
|
from src
|
||||||
|
where src.guid = u.guid
|
||||||
|
returning *
|
||||||
|
), ins as (
|
||||||
|
insert into core.mastertype (category, mastertype, description, inactive, forprefix, guid, rid_parent)
|
||||||
|
select src.category, src.mastertype, src.description, src.inactive, src.forprefix, src.guid, src.rid_parent
|
||||||
|
from src
|
||||||
|
where
|
||||||
|
not exists (
|
||||||
|
select 1
|
||||||
|
from core.mastertype mt2
|
||||||
|
where
|
||||||
|
mt2.category = src.category
|
||||||
|
and mt2.mastertype = src.mastertype
|
||||||
|
)
|
||||||
|
returning *
|
||||||
|
)
|
||||||
|
select (select count(1) from ins) + (select count(1) from upd )
|
||||||
|
into m_retval;
|
||||||
|
|
||||||
|
|
||||||
|
END;
|
||||||
|
|
||||||
|
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
Vendored
+1575
File diff suppressed because it is too large
Load Diff
+1920
File diff suppressed because it is too large
Load Diff
+8
@@ -0,0 +1,8 @@
|
|||||||
|
select u.name
|
||||||
|
,u.surname
|
||||||
|
,u.login
|
||||||
|
from public.user u
|
||||||
|
where u.id = 1
|
||||||
|
and u.name = 'test'
|
||||||
|
or u.name = 'joe'
|
||||||
|
;
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
; PgTidy Windows installer
|
||||||
|
;
|
||||||
|
; Build with (VERSION must match the release tag, without the leading "v"):
|
||||||
|
; makensis /DVERSION=1.2.3 /DSRC_EXE=path\to\pgtidy.exe windows\installer.nsi
|
||||||
|
;
|
||||||
|
; Installs pgtidy.exe into Program Files, adds the install dir to the
|
||||||
|
; machine-wide PATH, and on startup checks the git.warky.dev Gitea API for a
|
||||||
|
; newer release than the one being installed.
|
||||||
|
|
||||||
|
!ifndef VERSION
|
||||||
|
!define VERSION "0.0.0"
|
||||||
|
!endif
|
||||||
|
!ifndef SRC_EXE
|
||||||
|
!define SRC_EXE "..\dist\pgtidy.exe"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
!define PRODUCT_NAME "PgTidy"
|
||||||
|
!define PRODUCT_PUBLISHER "Warky Devs"
|
||||||
|
!define PRODUCT_HOMEPAGE "https://git.warky.dev/wdevs/pgtidy"
|
||||||
|
!define RELEASES_API_URL "https://git.warky.dev/api/v1/repos/wdevs/pgtidy/releases/latest"
|
||||||
|
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\PgTidy"
|
||||||
|
!define ENV_KEY 'HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"'
|
||||||
|
|
||||||
|
!include "MUI2.nsh"
|
||||||
|
!include "LogicLib.nsh"
|
||||||
|
|
||||||
|
Name "${PRODUCT_NAME} ${VERSION}"
|
||||||
|
OutFile "pgtidy-setup-${VERSION}.exe"
|
||||||
|
InstallDir "$PROGRAMFILES64\PgTidy"
|
||||||
|
InstallDirRegKey HKLM "${UNINST_KEY}" "InstallLocation"
|
||||||
|
RequestExecutionLevel admin
|
||||||
|
Unicode true
|
||||||
|
|
||||||
|
!define MUI_ABORTWARNING
|
||||||
|
!define MUI_ICON "..\assets\logo_128.ico"
|
||||||
|
!define MUI_UNICON "..\assets\logo_128.ico"
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_WELCOME
|
||||||
|
!insertmacro MUI_PAGE_LICENSE "..\LICENSE"
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
!insertmacro MUI_PAGE_INSTFILES
|
||||||
|
!insertmacro MUI_PAGE_FINISH
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_CONFIRM
|
||||||
|
!insertmacro MUI_UNPAGE_INSTFILES
|
||||||
|
|
||||||
|
!insertmacro MUI_LANGUAGE "English"
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; Check the Gitea releases API for a newer version than the one we are about
|
||||||
|
; to install. Best-effort only: any failure (offline, API down, no
|
||||||
|
; PowerShell) is swallowed and the installer proceeds silently.
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
Function .onInit
|
||||||
|
StrCpy $1 "$TEMP\pgtidy-latest-version.txt"
|
||||||
|
Delete "$1"
|
||||||
|
|
||||||
|
DetailPrint "Checking ${PRODUCT_HOMEPAGE} for a newer release..."
|
||||||
|
nsExec::ExecToLog 'powershell -NoProfile -NonInteractive -Command "try { $$r = Invoke-RestMethod -Uri ''${RELEASES_API_URL}'' -UseBasicParsing -TimeoutSec 5; $$r.tag_name | Out-File -Encoding ascii -NoNewline ''$1'' } catch { exit 0 }"'
|
||||||
|
|
||||||
|
${IfNot} ${FileExists} "$1"
|
||||||
|
Return
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
FileOpen $2 "$1" r
|
||||||
|
FileRead $2 $3
|
||||||
|
FileClose $2
|
||||||
|
Delete "$1"
|
||||||
|
|
||||||
|
StrCpy $4 $3
|
||||||
|
; Trim a leading "v" if the tag is e.g. "v1.2.3"
|
||||||
|
StrCpy $5 $4 1
|
||||||
|
${If} $5 == "v"
|
||||||
|
StrCpy $4 $4 "" 1
|
||||||
|
${EndIf}
|
||||||
|
|
||||||
|
${If} $4 != ""
|
||||||
|
${AndIf} $4 != "${VERSION}"
|
||||||
|
MessageBox MB_YESNO|MB_ICONINFORMATION \
|
||||||
|
"A newer version of PgTidy is available: $4 (this installer is ${VERSION}).$\n$\nOpen the releases page to download it now?$\n$\nChoosing No continues installing ${VERSION}." \
|
||||||
|
IDNO +2
|
||||||
|
ExecShell "open" "${PRODUCT_HOMEPAGE}/releases/latest"
|
||||||
|
${EndIf}
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; Adds $INSTDIR to the machine PATH if it isn't already present.
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
Function AddToPath
|
||||||
|
ReadRegStr $0 ${ENV_KEY} "Path"
|
||||||
|
Push "$0"
|
||||||
|
Push "$INSTDIR"
|
||||||
|
Call StrContains
|
||||||
|
Pop $1
|
||||||
|
${If} $1 == ""
|
||||||
|
${If} $0 == ""
|
||||||
|
StrCpy $0 "$INSTDIR"
|
||||||
|
${Else}
|
||||||
|
StrCpy $0 "$0;$INSTDIR"
|
||||||
|
${EndIf}
|
||||||
|
WriteRegExpandStr ${ENV_KEY} "Path" "$0"
|
||||||
|
SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000
|
||||||
|
${EndIf}
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
; Removes $INSTDIR from the machine PATH.
|
||||||
|
; ---------------------------------------------------------------------------
|
||||||
|
Function un.RemoveFromPath
|
||||||
|
ReadRegStr $0 ${ENV_KEY} "Path"
|
||||||
|
Push "$0;"
|
||||||
|
Push "$INSTDIR;"
|
||||||
|
Push ""
|
||||||
|
Call un.StrReplace
|
||||||
|
Pop $0
|
||||||
|
Push "$0"
|
||||||
|
Push "$INSTDIR"
|
||||||
|
Push ""
|
||||||
|
Call un.StrReplace
|
||||||
|
Pop $0
|
||||||
|
; Drop a trailing separator left behind by the replacements above.
|
||||||
|
StrCpy $1 $0 1 -1
|
||||||
|
${If} $1 == ";"
|
||||||
|
StrCpy $0 $0 -1
|
||||||
|
${EndIf}
|
||||||
|
WriteRegExpandStr ${ENV_KEY} "Path" "$0"
|
||||||
|
SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; Returns the index of needle in haystack via $R0, or "" if absent.
|
||||||
|
; Push haystack, Push needle -> Pop result
|
||||||
|
Function StrContains
|
||||||
|
Exch $R1 ; needle
|
||||||
|
Exch
|
||||||
|
Exch $R2 ; haystack
|
||||||
|
Push $R3
|
||||||
|
Push $R4
|
||||||
|
Push $R5
|
||||||
|
StrLen $R3 $R1
|
||||||
|
StrCpy $R4 0
|
||||||
|
${Do}
|
||||||
|
StrCpy $R5 $R2 $R3 $R4
|
||||||
|
${If} $R5 == $R1
|
||||||
|
StrCpy $R0 $R4
|
||||||
|
${ExitDo}
|
||||||
|
${EndIf}
|
||||||
|
${If} $R5 == ""
|
||||||
|
StrCpy $R0 ""
|
||||||
|
${ExitDo}
|
||||||
|
${EndIf}
|
||||||
|
IntOp $R4 $R4 + 1
|
||||||
|
${Loop}
|
||||||
|
Pop $R5
|
||||||
|
Pop $R4
|
||||||
|
Pop $R3
|
||||||
|
Pop $R2
|
||||||
|
Pop $R1
|
||||||
|
Push $R0
|
||||||
|
Exch
|
||||||
|
Pop $R0
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
; Push string, Push search, Push replace -> Pop result
|
||||||
|
Function un.StrReplace
|
||||||
|
Exch $R0 ; replace
|
||||||
|
Exch
|
||||||
|
Exch $R1 ; search
|
||||||
|
Exch 2
|
||||||
|
Exch $R2 ; string
|
||||||
|
Push $R3
|
||||||
|
Push $R4
|
||||||
|
Push $R5
|
||||||
|
Push $R6
|
||||||
|
StrLen $R3 $R1
|
||||||
|
StrCpy $R4 ""
|
||||||
|
${Do}
|
||||||
|
StrCpy $R5 $R2 $R3
|
||||||
|
${If} $R5 == $R1
|
||||||
|
StrCpy $R4 "$R4$R0"
|
||||||
|
StrCpy $R2 $R2 "" $R3
|
||||||
|
${ElseIf} $R2 == ""
|
||||||
|
${ExitDo}
|
||||||
|
${Else}
|
||||||
|
StrCpy $R6 $R2 1
|
||||||
|
StrCpy $R4 "$R4$R6"
|
||||||
|
StrCpy $R2 $R2 "" 1
|
||||||
|
${EndIf}
|
||||||
|
${Loop}
|
||||||
|
Pop $R6
|
||||||
|
Pop $R5
|
||||||
|
Pop $R4
|
||||||
|
Pop $R3
|
||||||
|
Pop $R2
|
||||||
|
Pop $R1
|
||||||
|
Pop $R0
|
||||||
|
Push $R4
|
||||||
|
FunctionEnd
|
||||||
|
|
||||||
|
Section "PgTidy" SEC_MAIN
|
||||||
|
SectionIn RO
|
||||||
|
SetOutPath "$INSTDIR"
|
||||||
|
File "${SRC_EXE}"
|
||||||
|
File "..\LICENSE"
|
||||||
|
|
||||||
|
Call AddToPath
|
||||||
|
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${PRODUCT_NAME}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${VERSION}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "InstallLocation" "$INSTDIR"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$INSTDIR\uninstall.exe"
|
||||||
|
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$INSTDIR\uninstall.exe /S"
|
||||||
|
WriteRegDWORD HKLM "${UNINST_KEY}" "NoModify" 1
|
||||||
|
WriteRegDWORD HKLM "${UNINST_KEY}" "NoRepair" 1
|
||||||
|
|
||||||
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section "Uninstall"
|
||||||
|
Call un.RemoveFromPath
|
||||||
|
Delete "$INSTDIR\pgtidy.exe"
|
||||||
|
Delete "$INSTDIR\LICENSE"
|
||||||
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
|
RMDir "$INSTDIR"
|
||||||
|
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||||
|
SectionEnd
|
||||||
Reference in New Issue
Block a user