Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions internal/asset/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/viant/afs"
"github.com/viant/afs/file"
"github.com/viant/afs/url"
"go/format"
"strings"
)

Expand Down Expand Up @@ -36,5 +37,12 @@ func (f Files) uploadContent(ctx context.Context, fs afs.Service, URL string, co
if ok, _ := fs.Exists(ctx, baseURL); !ok {
_ = fs.Create(ctx, baseURL, file.DefaultDirOsMode, true)
}
// gofmt generated Go files so their layout is stable across regenerations.
// Non-Go files and not-yet-valid Go are written through unchanged.
if strings.HasSuffix(URL, ".go") {
if formatted, err := format.Source([]byte(content)); err == nil {
content = string(formatted)
}
}
return fs.Upload(ctx, URL, file.DefaultFileOsMode, strings.NewReader(content))
}
7 changes: 7 additions & 0 deletions repository/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/viant/tagly/format/text"
"github.com/viant/toolbox/data"
"github.com/viant/xreflect"
"go/format"
"path"
"reflect"
"strconv"
Expand Down Expand Up @@ -198,6 +199,12 @@ func (c *Component) GenerateOutputCode(ctx context.Context, withDefineComponent,

result := builder.String()
result = c.View.Resource().ReverseSubstitutes(result)
// Normalize the generated source with gofmt so appended snippets (e.g. the
// EmbedFS method) are properly indented and the file ends with a newline.
// Fall back to the unformatted source if it is not yet valid Go.
if formatted, formatErr := format.Source([]byte(result)); formatErr == nil {
result = string(formatted)
}
return result
}

Expand Down
6 changes: 6 additions & 0 deletions repository/path/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/viant/datly/repository/version"
"gopkg.in/yaml.v3"
"path"
"sort"
"strings"
"sync"
"time"
Expand Down Expand Up @@ -151,6 +152,11 @@ func (s *Service) createPathFiles(ctx context.Context) error {
if err != nil {
return err
}
// Sort candidates by URL so paths.yaml is written in a deterministic
// order regardless of the underlying filesystem listing order.
sort.Slice(candidates, func(i, j int) bool {
return candidates[i].URL() < candidates[j].URL()
})
rootPath := url.Path(s.URL)
for _, candidate := range candidates {
if candidate.IsDir() {
Expand Down
30 changes: 28 additions & 2 deletions repository/shape/column/detector.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"reflect"
"strings"

"github.com/viant/datly/shared"
"github.com/viant/datly/view"
viewcolumn "github.com/viant/datly/view/column"
"github.com/viant/datly/view/state"
Expand Down Expand Up @@ -110,6 +111,9 @@ func discoverySQL(aView *view.View, resource *view.Resource) string {
}
return cleaned
}
// A view whose SQL is a derived table is wrapped in ( ... ); the balanced
// pair confuses the falsifier's parser, so trim it before falsifying.
cleaned = strings.TrimSpace(shared.TrimPair(cleaned, '(', ')'))
if falsified, ok := falsifyQuery(cleaned); ok {
return falsified
}
Expand Down Expand Up @@ -473,6 +477,21 @@ func needsDiscovery(aView *view.View) bool {
return usesWildcard(aView)
}

// discoveryReplacementFor keeps a discovery query valid once a ${...} template
// is removed. A predicate builder that opens a WHERE clause is replaced with a
// tautology so any trailing AND still binds; any other builder is dropped; a
// plain value expression becomes an empty string literal.
func discoveryReplacementFor(expr string) string {
if !strings.Contains(expr, ".Build(") {
return "''"
}
lower := strings.ToLower(expr)
if strings.Contains(lower, `build("where")`) || strings.Contains(lower, `build('where')`) {
return " WHERE 1 = 1 "
}
return ""
}

// stripTemplateVariables removes velocity/velty template constructs from SQL
// so it can be parsed and executed for column discovery.
// Handles: $variable, ${expression}, #if...#end, #foreach...#end, #set(...)
Expand Down Expand Up @@ -508,8 +527,15 @@ func stripTemplateVariables(sql string) string {
}
j++
}
// Replace with empty string or placeholder
b.WriteString("''")
// Predicate builders emit SQL clauses (e.g. Build("WHERE")); replace
// them with a valid stand-in so the discovery query stays parseable.
// Plain value expressions fall back to an empty string literal.
exprEnd := j - 1
if exprEnd < i+2 {
// Unterminated "${" at end of input: no expression body.
exprEnd = i + 2
}
b.WriteString(discoveryReplacementFor(sql[i+2 : exprEnd]))
i = j
continue
}
Expand Down
8 changes: 4 additions & 4 deletions repository/shape/column/strip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func TestStripTemplateVariables(t *testing.T) {
{
name: "expression in braces",
input: "SELECT * FROM VENDOR WHERE ${predicate.Build(\"AND\")}",
expect: "SELECT * FROM VENDOR WHERE ''",
expect: "SELECT * FROM VENDOR WHERE ",
},
{
name: "if directive",
Expand All @@ -64,7 +64,7 @@ func TestStripTemplateVariables(t *testing.T) {
{
name: "mixed templates and SQL",
input: "SELECT vendor.*, products.* FROM (SELECT * FROM VENDOR t) vendor JOIN (SELECT * FROM PRODUCT t WHERE 1=1 ${predicate.Builder().CombineOr($predicate.FilterGroup(0, \"AND\")).Build(\"AND\")}) products ON products.VENDOR_ID = vendor.ID",
expect: "SELECT vendor.*, products.* FROM (SELECT * FROM VENDOR t) vendor JOIN (SELECT * FROM PRODUCT t WHERE 1=1 '') products ON products.VENDOR_ID = vendor.ID",
expect: "SELECT vendor.*, products.* FROM (SELECT * FROM VENDOR t) vendor JOIN (SELECT * FROM PRODUCT t WHERE 1=1 ) products ON products.VENDOR_ID = vendor.ID",
},
{
name: "UNION ALL with templates",
Expand Down Expand Up @@ -94,7 +94,7 @@ func TestStripTemplateVariables(t *testing.T) {
{
name: "complex predicate builder",
input: "WHERE ${predicate.Builder().CombineOr($predicate.FilterGroup(0, \"AND\")).Build(\"AND\")}",
expect: "WHERE ''",
expect: "WHERE ",
},
{
name: "dollar at end",
Expand Down Expand Up @@ -156,7 +156,7 @@ perf AS (
FROM fact_performance p
JOIN params prm ON TRUE
WHERE p.event_date BETWEEN prm.start_date AND prm.end_date
''

GROUP BY 1
)
SELECT v.* FROM perf v ORDER BY v.agency_id`,
Expand Down
29 changes: 28 additions & 1 deletion repository/shape/discoverysql/prepare.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package discoverysql
import (
"strings"

"github.com/viant/datly/shared"
"github.com/viant/sqlparser"
"github.com/viant/sqlparser/expr"
"github.com/viant/sqlparser/node"
Expand All @@ -22,12 +23,30 @@ func PrepareDiscoverySQL(sql string) (string, bool) {
if cleaned == "" || !strings.Contains(strings.ToLower(cleaned), "select") {
return cleaned, false
}
// A view whose SQL is a derived table is wrapped in ( ... ); the balanced
// pair confuses the falsifier's parser, so trim it before falsifying.
cleaned = strings.TrimSpace(shared.TrimPair(cleaned, '(', ')'))
if rewritten, ok := falsifyQuery(cleaned); ok {
return rewritten, true
}
return falsifyQueryText(cleaned)
}

// discoveryReplacementFor keeps a discovery query valid once a ${...} template
// is removed. A predicate builder that opens a WHERE clause is replaced with a
// tautology so any trailing AND still binds; any other builder is dropped; a
// plain value expression becomes an empty string literal.
func discoveryReplacementFor(expr string) string {
if !strings.Contains(expr, ".Build(") {
return "''"
}
lower := strings.ToLower(expr)
if strings.Contains(lower, `build("where")`) || strings.Contains(lower, `build('where')`) {
return " WHERE 1 = 1 "
}
return ""
}

func falsifyQuery(sql string) (string, bool) {
sql = strings.TrimSpace(sql)
if sql == "" {
Expand Down Expand Up @@ -573,7 +592,15 @@ func stripTemplateVariables(sql string) string {
}
j++
}
b.WriteString("''")
// Predicate builders emit SQL clauses (e.g. Build("WHERE")); replace
// them with a valid stand-in so the discovery query stays parseable.
// Plain value expressions fall back to an empty string literal.
exprEnd := j - 1
if exprEnd < i+2 {
// Unterminated "${" at end of input: no expression body.
exprEnd = i + 2
}
b.WriteString(discoveryReplacementFor(sql[i+2 : exprEnd]))
i = j
continue
}
Expand Down
Loading