Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4abc89700f | ||
|
|
ad52f21cc2 | ||
|
|
60602d1de7 | ||
|
|
8d19258aa0 | ||
|
|
5cba6beeb1 |
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -111,8 +111,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
|
||||||
|
|||||||
+30
-8
@@ -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 {
|
||||||
@@ -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 {
|
||||||
@@ -92,14 +102,14 @@ func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
_, _ = 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
|
||||||
}
|
}
|
||||||
@@ -115,7 +125,7 @@ func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
_, _ = 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 {
|
||||||
@@ -130,13 +140,24 @@ func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -150,6 +171,7 @@ 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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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"
|
||||||
@@ -33,17 +31,7 @@
|
|||||||
<statusBarWidgetFactory id="com.pgtidy.StatusBarWidget"
|
<statusBarWidgetFactory id="com.pgtidy.StatusBarWidget"
|
||||||
implementation="com.pgtidy.datagrip.PgTidyStatusBarWidgetFactory"
|
implementation="com.pgtidy.datagrip.PgTidyStatusBarWidgetFactory"
|
||||||
order="last"/>
|
order="last"/>
|
||||||
|
<externalAnnotator language="" implementationClass="com.pgtidy.datagrip.PgTidyExternalAnnotator"/>
|
||||||
</extensions>
|
</extensions>
|
||||||
|
|
||||||
<extensions defaultExtensionNs="com.redhat.devtools.lsp4ij">
|
|
||||||
<server id="com.pgtidy.lsp"
|
|
||||||
name="PgTidy"
|
|
||||||
factoryClass="com.pgtidy.datagrip.PgTidyServerFactory">
|
|
||||||
<description>PostgreSQL formatter and linter (pgtidy lsp)</description>
|
|
||||||
</server>
|
|
||||||
|
|
||||||
<fileNamePatternMapping patterns="*.sql;*.pgsql"
|
|
||||||
serverId="com.pgtidy.lsp"
|
|
||||||
languageId="SQL"/>
|
|
||||||
</extensions>
|
|
||||||
</idea-plugin>
|
</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.4
|
pkgver=0.0.6
|
||||||
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.4
|
Version: 0.0.6
|
||||||
Release: 1%{?dist}
|
Release: 1%{?dist}
|
||||||
Summary: PostgreSQL SQL formatter and linter
|
Summary: PostgreSQL SQL formatter and linter
|
||||||
|
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
+128
-7
@@ -441,13 +441,9 @@ func formatBodyStatements(text string, st config.Style) string {
|
|||||||
stmtLines = joinThenToCondition(stmt)
|
stmtLines = joinThenToCondition(stmt)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, ll := range stmtLines {
|
formattedLines := formatBodyStmtLines(stmtLines, baseIndent, st)
|
||||||
if i == 0 || ll.indent == "" {
|
for _, line := range formattedLines {
|
||||||
result.WriteString(baseIndent)
|
result.WriteString(line)
|
||||||
} else {
|
|
||||||
result.WriteString(ll.indent)
|
|
||||||
}
|
|
||||||
result.WriteString(ll.text)
|
|
||||||
result.WriteString(nl)
|
result.WriteString(nl)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,6 +570,131 @@ 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
|
||||||
|
if 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") {
|
||||||
|
out = append(out, baseIndent+st.Indent+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
|
// joinThenToCondition merges a THEN line (on its own bline) into the preceding
|
||||||
// condition line when plpgsql_if_then_newline is false.
|
// condition line when plpgsql_if_then_newline is false.
|
||||||
func joinThenToCondition(lines []bline) []bline {
|
func joinThenToCondition(lines []bline) []bline {
|
||||||
|
|||||||
@@ -107,6 +107,66 @@ func TestFormatMmProcBroken(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFormatIssue1PLpgSQLIndenting(t *testing.T) {
|
||||||
|
src := "CREATE FUNCTION f() RETURNS void LANGUAGE plpgsql AS $$\n" +
|
||||||
|
"DECLARE\n" +
|
||||||
|
" r_lp record;\n" +
|
||||||
|
"BEGIN\n" +
|
||||||
|
" if r_lp.total > 0\n" +
|
||||||
|
" and r_lp.totaldone >= r_lp.total\n" +
|
||||||
|
" then\n" +
|
||||||
|
" update core.process u\n" +
|
||||||
|
" set status = 'done'\n" +
|
||||||
|
" where u.rid_process = r_lp.rid_process\n" +
|
||||||
|
" and nv(u.status) <> 'done';\n" +
|
||||||
|
" elsif r_lp.total > 0\n" +
|
||||||
|
" then\n" +
|
||||||
|
" update core.process u\n" +
|
||||||
|
" set status = 'open'\n" +
|
||||||
|
" where u.rid_process = r_lp.rid_process\n" +
|
||||||
|
" and nv(u.status) <> 'open';\n" +
|
||||||
|
"\n" +
|
||||||
|
" end if;\n" +
|
||||||
|
"$$;\n"
|
||||||
|
|
||||||
|
want := "CREATE FUNCTION f(\n" +
|
||||||
|
")\n" +
|
||||||
|
"RETURNS void\n" +
|
||||||
|
"LANGUAGE plpgsql\n" +
|
||||||
|
"AS\n" +
|
||||||
|
"$$\n" +
|
||||||
|
"DECLARE\n" +
|
||||||
|
" r_lp record;\n" +
|
||||||
|
"BEGIN\n" +
|
||||||
|
" if r_lp.total > 0\n" +
|
||||||
|
" and r_lp.totaldone >= r_lp.total\n" +
|
||||||
|
" then\n" +
|
||||||
|
" update core.process u\n" +
|
||||||
|
" set status = 'done'\n" +
|
||||||
|
" where\n" +
|
||||||
|
" u.rid_process = r_lp.rid_process\n" +
|
||||||
|
" and nv(u.status) <> 'done';\n" +
|
||||||
|
" elsif r_lp.total > 0\n" +
|
||||||
|
" then\n" +
|
||||||
|
" update core.process u\n" +
|
||||||
|
" set status = 'open'\n" +
|
||||||
|
" where\n" +
|
||||||
|
" u.rid_process = r_lp.rid_process\n" +
|
||||||
|
" and nv(u.status) <> 'open';\n" +
|
||||||
|
"\n" +
|
||||||
|
" end if;\n" +
|
||||||
|
"$$;\n"
|
||||||
|
|
||||||
|
got := format(src)
|
||||||
|
if got != want {
|
||||||
|
t.Errorf("issue #1 PL/pgSQL indenting\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||||
|
}
|
||||||
|
checkDML(t, "issue #1 PL/pgSQL indenting", got)
|
||||||
|
if !semanticallyEqual(src, got) {
|
||||||
|
t.Errorf("issue #1 PL/pgSQL indenting changed semantics")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCorpusIdempotentAndSafe(t *testing.T) {
|
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)
|
||||||
|
|||||||
Vendored
+18
-18
@@ -632,7 +632,7 @@ BEGIN
|
|||||||
end if;*/
|
end if;*/
|
||||||
|
|
||||||
if G_BENCHMARK = 1
|
if G_BENCHMARK = 1
|
||||||
then
|
then
|
||||||
perform log_event(m_funcname,format('Perf Merge Replace SinceStart: %s Duration: %s', clock_timestamp() - m_start, clock_timestamp() - m_ltime),bt_enum('eventlog','local notice'));
|
perform log_event(m_funcname,format('Perf Merge Replace SinceStart: %s Duration: %s', clock_timestamp() - m_start, clock_timestamp() - m_ltime),bt_enum('eventlog','local notice'));
|
||||||
m_ltime = clock_timestamp();
|
m_ltime = clock_timestamp();
|
||||||
end if;
|
end if;
|
||||||
@@ -735,7 +735,7 @@ BEGIN
|
|||||||
, '\[(.*?)\]', 'ig') r(v) into m_errmsg,m_retval;
|
, '\[(.*?)\]', 'ig') r(v) into m_errmsg,m_retval;
|
||||||
|
|
||||||
if m_retval > 0
|
if m_retval > 0
|
||||||
then
|
then
|
||||||
raise exception E'The following data fields could not be found for prefix % \r\n%', ifblnk(m_data_prefix, 'Null'), m_errmsg using hint = 'in ID replace process';
|
raise exception E'The following data fields could not be found for prefix % \r\n%', ifblnk(m_data_prefix, 'Null'), m_errmsg using hint = 'in ID replace process';
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
@@ -1136,16 +1136,16 @@ BEGIN
|
|||||||
-- );
|
-- );
|
||||||
|
|
||||||
if r_lp_t.table_name = any(a_inner_selected) and nv(r_lp_t.table_name ) <> ''
|
if r_lp_t.table_name = any(a_inner_selected) and nv(r_lp_t.table_name ) <> ''
|
||||||
then
|
then
|
||||||
raise notice 'Table used as inner table: %', r_lp_t.table_name;
|
raise notice 'Table used as inner table: %', r_lp_t.table_name;
|
||||||
continue;
|
continue;
|
||||||
end if;
|
end if;
|
||||||
--raise notice 'Field:% Table:% Tag: % Type: %', r_lp_t.field_name, r_lp_t.table_name, r_lp_t.mergetag, r_lp_t.merge_type;
|
--raise notice 'Field:% Table:% Tag: % Type: %', r_lp_t.field_name, r_lp_t.table_name, r_lp_t.mergetag, r_lp_t.merge_type;
|
||||||
if nv(r_lp_t.field_name) = '' and r_lp_t.merge_type not in (G_MTYPE_TBLROOT,G_MTYPE_SPECIAL)
|
if nv(r_lp_t.field_name) = '' and r_lp_t.merge_type not in (G_MTYPE_TBLROOT,G_MTYPE_SPECIAL)
|
||||||
then
|
then
|
||||||
|
|
||||||
if G_DEBUG
|
if G_DEBUG
|
||||||
then
|
then
|
||||||
perform log_event(m_funcname,format('Blank field name on Complex merge for p_doctype=%s, p_commtype=%s, p_data_prefix=%s, p_data_rid=%s
|
perform log_event(m_funcname,format('Blank field name on Complex merge for p_doctype=%s, p_commtype=%s, p_data_prefix=%s, p_data_rid=%s
|
||||||
field_name=%s, merge_type=%s, table_name=%s
|
field_name=%s, merge_type=%s, table_name=%s
|
||||||
' ,p_doctype, p_commtype,p_data_prefix,p_data_rid
|
' ,p_doctype, p_commtype,p_data_prefix,p_data_rid
|
||||||
@@ -1159,7 +1159,7 @@ BEGIN
|
|||||||
end if;
|
end if;
|
||||||
|
|
||||||
if nv(m_exec_orderstr) = '' and nv(r_lp_t.parent_order_string) <> ''
|
if nv(m_exec_orderstr) = '' and nv(r_lp_t.parent_order_string) <> ''
|
||||||
then
|
then
|
||||||
m_exec_orderstr = r_lp_t.parent_order_string;
|
m_exec_orderstr = r_lp_t.parent_order_string;
|
||||||
--raise notice 'Applying order % by for % %.', r_lp_t.parent_order_string, r_lp_t.parent_table_name,r_lp_t.field_name;
|
--raise notice 'Applying order % by for % %.', r_lp_t.parent_order_string, r_lp_t.parent_table_name,r_lp_t.field_name;
|
||||||
/*
|
/*
|
||||||
@@ -1176,7 +1176,7 @@ BEGIN
|
|||||||
end if;
|
end if;
|
||||||
|
|
||||||
if nv(r_lp_t.ops_string) = ''
|
if nv(r_lp_t.ops_string) = ''
|
||||||
then
|
then
|
||||||
r_lp_t.ops_string = r_lp_t.field_name;
|
r_lp_t.ops_string = r_lp_t.field_name;
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
@@ -1192,19 +1192,19 @@ BEGIN
|
|||||||
end loop;
|
end loop;
|
||||||
|
|
||||||
if r_lp_t.merge_type in (G_MTYPE_TBLFIELD, G_MTYPE_CONDFIELD)
|
if r_lp_t.merge_type in (G_MTYPE_TBLFIELD, G_MTYPE_CONDFIELD)
|
||||||
then
|
then
|
||||||
m_execstr = format($S$%s|| '%s"%s":' || json_build_object('value',json_agg(%s::text %s), 'type', '%s')::text %s$S$
|
m_execstr = format($S$%s|| '%s"%s":' || json_build_object('value',json_agg(%s::text %s), 'type', '%s')::text %s$S$
|
||||||
,m_execstr,m_comma,r_lp_t.mergetag
|
,m_execstr,m_comma,r_lp_t.mergetag
|
||||||
,r_lp_t.ops_string
|
,r_lp_t.ops_string
|
||||||
, m_exec_orderstr, r_lp_t.merge_type, E'\r\n');
|
, m_exec_orderstr, r_lp_t.merge_type, E'\r\n');
|
||||||
|
|
||||||
elseif r_lp_t.merge_type = G_MTYPE_SPECIAL--special fields
|
elseif r_lp_t.merge_type = G_MTYPE_SPECIAL--special fields
|
||||||
then
|
then
|
||||||
m_execstr = format($S$%s|| '%s"%s":' || json_build_object('value',%s, 'type', '%s')::text %s$S$
|
m_execstr = format($S$%s|| '%s"%s":' || json_build_object('value',%s, 'type', '%s')::text %s$S$
|
||||||
,m_execstr,m_comma,r_lp_t.mergetag,quote_literal(r_lp_t.tagvalue), r_lp_t.merge_type, E'\r\n');
|
,m_execstr,m_comma,r_lp_t.mergetag,quote_literal(r_lp_t.tagvalue), r_lp_t.merge_type, E'\r\n');
|
||||||
--raise notice 'Special Field: %s',r_lp_t;
|
--raise notice 'Special Field: %s',r_lp_t;
|
||||||
elseif r_lp.merge_type = G_MTYPE_PICTURE
|
elseif r_lp.merge_type = G_MTYPE_PICTURE
|
||||||
then
|
then
|
||||||
m_execstr = format($S$%s|| '%s"%s":'
|
m_execstr = format($S$%s|| '%s"%s":'
|
||||||
|| json_build_object('value',%s::text, 'type', '%s'
|
|| json_build_object('value',%s::text, 'type', '%s'
|
||||||
, 'w', mailmerge_specialfield('width', '%s', %s) ,'h', mailmerge_specialfield('height', '%s', %s))::text %s$S$
|
, 'w', mailmerge_specialfield('width', '%s', %s) ,'h', mailmerge_specialfield('height', '%s', %s))::text %s$S$
|
||||||
@@ -1212,7 +1212,7 @@ BEGIN
|
|||||||
,r_lp.mergetag,quote_nullable(m_data_rid),r_lp.mergetag,quote_nullable(m_data_rid), E'\r\n');
|
,r_lp.mergetag,quote_nullable(m_data_rid),r_lp.mergetag,quote_nullable(m_data_rid), E'\r\n');
|
||||||
|
|
||||||
elseif nv(r_lp_t.field_name) <> ''
|
elseif nv(r_lp_t.field_name) <> ''
|
||||||
then
|
then
|
||||||
m_execstr = format($S$%s|| '%s"%s":' || json_build_object('value',%s::text, 'type', '%s')::text %s$S$
|
m_execstr = format($S$%s|| '%s"%s":' || json_build_object('value',%s::text, 'type', '%s')::text %s$S$
|
||||||
,m_execstr,m_comma,r_lp_t.mergetag
|
,m_execstr,m_comma,r_lp_t.mergetag
|
||||||
, r_lp_t.ops_string
|
, r_lp_t.ops_string
|
||||||
@@ -1225,7 +1225,7 @@ BEGIN
|
|||||||
m_blankexec = format($S$%s|| '%s"%s":' || json_build_object('value','', 'type', '%s')::text %s$S$,m_blankexec,m_comma,r_lp_t.mergetag, r_lp_t.merge_type, E'\r\n');
|
m_blankexec = format($S$%s|| '%s"%s":' || json_build_object('value','', 'type', '%s')::text %s$S$,m_blankexec,m_comma,r_lp_t.mergetag, r_lp_t.merge_type, E'\r\n');
|
||||||
|
|
||||||
if r_lp_t.rn = 1
|
if r_lp_t.rn = 1
|
||||||
then
|
then
|
||||||
--Inner level tables (2)
|
--Inner level tables (2)
|
||||||
--raise notice 'Begin: parent: %', r_lp_t;
|
--raise notice 'Begin: parent: %', r_lp_t;
|
||||||
for r_lp_c in (
|
for r_lp_c in (
|
||||||
@@ -1345,7 +1345,7 @@ BEGIN
|
|||||||
end loop;
|
end loop;
|
||||||
|
|
||||||
if ifblnk(r_lp_t.parent_table_name,'') = ''
|
if ifblnk(r_lp_t.parent_table_name,'') = ''
|
||||||
then
|
then
|
||||||
m_execstr = format(E'select (''{'' %s \r\n || ''}'')::json ;',m_execstr );
|
m_execstr = format(E'select (''{'' %s \r\n || ''}'')::json ;',m_execstr );
|
||||||
else
|
else
|
||||||
select string_agg(s.filter_string, ' ')
|
select string_agg(s.filter_string, ' ')
|
||||||
@@ -1364,7 +1364,7 @@ BEGIN
|
|||||||
from exec_json(m_execstr, 'str json') r into m_retval,m_errmsg, m_json;
|
from exec_json(m_execstr, 'str json') r into m_retval,m_errmsg, m_json;
|
||||||
|
|
||||||
if m_json is null
|
if m_json is null
|
||||||
then
|
then
|
||||||
|
|
||||||
select r.p_retval, r.p_errmsg, r.p_json - > 'str'
|
select r.p_retval, r.p_errmsg, r.p_json - > 'str'
|
||||||
from exec_json(m_execstr, 'str json') r into m_retval,m_errmsg, m_json;
|
from exec_json(m_execstr, 'str json') r into m_retval,m_errmsg, m_json;
|
||||||
@@ -1373,12 +1373,12 @@ BEGIN
|
|||||||
m_debug_exestr = nv(m_debug_exestr) || E'\r\n/*'|| nv(r_lp_t.parent_table_name) || ' len:' || nv(length(m_json::text)) ||E'*/ \r\n' || nv(m_execstr) || E'\r\n ';
|
m_debug_exestr = nv(m_debug_exestr) || E'\r\n/*'|| nv(r_lp_t.parent_table_name) || ' len:' || nv(length(m_json::text)) ||E'*/ \r\n' || nv(m_execstr) || E'\r\n ';
|
||||||
|
|
||||||
if m_json_full_complex is null
|
if m_json_full_complex is null
|
||||||
then
|
then
|
||||||
m_json_full_complex = jsonb_build_object(r_lp_t.tblid::text,m_json);
|
m_json_full_complex = jsonb_build_object(r_lp_t.tblid::text,m_json);
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
if (m_json_full_complex->r_lp_t.tblid::text) is null
|
if (m_json_full_complex->r_lp_t.tblid::text) is null
|
||||||
then
|
then
|
||||||
m_json_full_complex = jsonb_set(m_json_full_complex, format('{%s}',r_lp_t.tblid)::text[], m_json::jsonb,true);
|
m_json_full_complex = jsonb_set(m_json_full_complex, format('{%s}',r_lp_t.tblid)::text[], m_json::jsonb,true);
|
||||||
else
|
else
|
||||||
m_json_full_complex = jsonb_set(m_json_full_complex, format('{%s}',r_lp_t.tblid)::text[], _jsonb_object_cat(m_json_full_complex->r_lp_t.tblid,m_json::jsonb),true);
|
m_json_full_complex = jsonb_set(m_json_full_complex, format('{%s}',r_lp_t.tblid)::text[], _jsonb_object_cat(m_json_full_complex->r_lp_t.tblid,m_json::jsonb),true);
|
||||||
@@ -1399,13 +1399,13 @@ BEGIN
|
|||||||
end if;
|
end if;
|
||||||
|
|
||||||
if nv(m_comma) = '' and length(m_execstr) > 2
|
if nv(m_comma) = '' and length(m_execstr) > 2
|
||||||
then
|
then
|
||||||
m_comma = ',';
|
m_comma = ',';
|
||||||
end if;
|
end if;
|
||||||
end loop;
|
end loop;
|
||||||
|
|
||||||
if G_DEBUG
|
if G_DEBUG
|
||||||
then
|
then
|
||||||
perform pl_writefile(r_template.debugsql_filename, convert_to(m_debug_exestr,'utf8'));
|
perform pl_writefile(r_template.debugsql_filename, convert_to(m_debug_exestr,'utf8'));
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user