Skip to content
Merged
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ Run the package benchmark suite with:
make test_bench
```

The PostgreSQL driver has a bounded, driver-wide Cypher compilation cache for both text Cypher and programmatic graph
queries. Its default capacity is 256 entries; callers that need a different capacity can use
`pg.NewDriverWithOptions`. The process-wide `pg.SetOptimizedTranslation` switch selects baseline translation for
newly started compilations across every PostgreSQL driver in the process. See
[PostgreSQL translation](docs/postgresql_translation.md#translation-cache) for the cache contract and rollback controls.

Use `cmd/benchdiff` to compare benchmarks between two committed refs without changing the active worktree:

```bash
Expand Down
45 changes: 45 additions & 0 deletions docs/postgresql_translation.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,51 @@ Substring and suffix predicates are not promoted to blanket schema indexes. Post
parameter/property forms that lower to helper functions remain outside the hard index-match contract until their
lowering changes.

## Translation Cache

The PostgreSQL driver keeps one bounded, driver-wide compilation cache with a default capacity of 256 rendered SQL
statements. It serves both `Transaction.Query` and the legacy programmatic query builder used by relationship and node
queries. A warm builder hit avoids optimization, lowering-plan construction, PostgreSQL AST construction, and SQL
rendering; it still builds the request AST, deterministically names its runtime parameters, renders the canonical
Cypher cache identity, and assembles its debug comment.

Entries are partitioned by the SHA-256 digest of trimmed Cypher text, target graph ID, sorted parameter names and PostgreSQL type shapes, a
translation-key format/policy identity, and the schema generation. Parameter values are never retained or used as key
material, and the cache key does not retain the source text. The retained data is limited to the SQL string and generated-parameter-to-Cypher-parameter source names;
the cache does not retain caller maps or values, ASTs, contexts, transactions, connections, rows, or connection
strings. Structural literals remain part of the cache identity. Generated parameters that cannot be reconstructed from
request-local named sources, translation errors, disabled/closed cache state, and source text over 64 KiB bypass
retention.

Configure the bounded cache through the PostgreSQL driver constructor:

```go
database := pg.NewDriverWithOptions(0, pool, pg.DriverOptions{
TranslationCacheEntries: 0, // disables cache lookup and retention
})
```

For a process-wide rollback, disable optimized translation directly and restore the prior state when appropriate:

```go
previous := pg.SetOptimizedTranslation(false)
defer pg.SetOptimizedTranslation(previous)
```

When disabled, newly started PostgreSQL compilations bypass cache lookup and retention and translate the original AST
with no PostgreSQL rewrite rules or lowering decisions. The setting is atomic and applies to every PostgreSQL driver in
the process; each compilation snapshots it at entry, so already-running compilations continue with their selected path.
Cached optimized entries remain dormant and are reusable after re-enabling. A zero cache capacity still runs optimized
translation without retaining entries. The default remains optimized translation with a 256-entry cache.

Concurrent cacheable misses for the same key share one translation. Waiters behind a failed, canceled, or non-cacheable
build continue independently rather than serializing behind repeated failed work. Statistics are available through
`(*pg.Driver).TranslationCacheStats()` and expose aggregate hits, build-leader misses, coalesced requests, bypasses,
unoptimized compilations, insertions, evictions, binding/build failures, live size, capacity, and generation; they never
expose query or value data. Successful schema assertions and kind refreshes advance the schema generation and discard completed entries.
External schema or type changes cannot be detected automatically; recreate the driver/pool after those changes. Driver
close retires the cache before the PostgreSQL pool closes.

## Validation Workflow

Optimizer changes should include focused optimizer/lowering tests, SQL-shape translation tests, and backend-equivalent
Expand Down
74 changes: 72 additions & 2 deletions drivers/pg/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,84 @@ import (

"github.com/specterops/dawgs/cypher/frontend"
"github.com/specterops/dawgs/cypher/models/cypher"
cypherFormat "github.com/specterops/dawgs/cypher/models/cypher/format"
"github.com/specterops/dawgs/cypher/models/pgsql/translate"
"github.com/specterops/dawgs/cypher/models/walk"
"github.com/specterops/dawgs/query"
)

const builderParameterPrefix = "__dawgs_builder_p"

var builderCommentNewlines = strings.NewReplacer("\r\n", "\n-- ", "\r", "\n-- ", "\n", "\n-- ")

// preparedRegularQuery holds a canonical view of a builder query. The source
// AST remains untouched on cache hits; cold compilation creates its own copy.
// Parameter values remain request-local and are never retained by the cache.
type preparedRegularQuery struct {
query *cypher.RegularQuery
source string
commentSource string
parameters map[string]any
}

func prepareRegularQuery(regularQuery *cypher.RegularQuery) (preparedRegularQuery, error) {
namer := query.NewParameterNamerWithPrefix(builderParameterPrefix)
if err := walk.Cypher(regularQuery, namer); err != nil {
return preparedRegularQuery{}, err
} else if source, err := cypherFormat.RegularQueryWithParameterSequence(regularQuery, false, namer.Symbols); err != nil {
return preparedRegularQuery{}, err
} else if commentSource, err := cypherFormat.RegularQueryWithParameterSequence(regularQuery, true, namer.Symbols); err != nil {
return preparedRegularQuery{}, err
} else {
return preparedRegularQuery{
query: regularQuery,
source: strings.TrimSpace(source),
commentSource: strings.TrimSpace(commentSource),
parameters: namer.Parameters,
}, nil
}
}

func (s preparedRegularQuery) translationQuery() (*cypher.RegularQuery, error) {
owned := cypher.Copy(s.query)
rewriter := query.NewParameterRewriterWithPrefix(builderParameterPrefix)
if err := walk.Cypher(owned, rewriter); err != nil {
return nil, err
}

return owned, nil
}

// Both PostgreSQL Cypher entry points use these methods so a builder query and
// its text equivalent share the same cache and cacheability rules.
func (s *SchemaManager) compileText(ctx context.Context, source string, parameters map[string]any, graphID int32) (string, map[string]any, error) {
return s.compile(ctx, strings.TrimSpace(source), parameters, graphID, func() (*cypher.RegularQuery, error) {
return frontend.ParseCypher(frontend.NewContext(), source)
})
}

func (s *SchemaManager) compileRegularQuery(ctx context.Context, prepared preparedRegularQuery, graphID int32) (string, map[string]any, error) {
return s.compile(ctx, prepared.source, prepared.parameters, graphID, func() (*cypher.RegularQuery, error) {
return prepared.translationQuery()
})
}

func (s *SchemaManager) compile(ctx context.Context, source string, parameters map[string]any, graphID int32, parse func() (*cypher.RegularQuery, error)) (string, map[string]any, error) {
translationCache := s.translationCacheProvider.TranslationCache()
var (
translationCache = s.translationCacheProvider.TranslationCache()
optimized = OptimizedTranslationEnabled()
translationOptions = translate.Options{
OptimizerMode: translate.OptimizerDisabled,
}
)
if optimized {
translationOptions.OptimizerMode = translate.OptimizerEnabled
}

build := func() (string, translationCacheBuildResult, error) {
if regularQuery, err := parse(); err != nil {
return "", translationCacheBuildResult{}, err
} else if translated, parameterSources, err := translate.TranslateWithOptionsAndParameterSources(ctx, regularQuery, s, parameters, graphID, translate.DefaultOptions()); err != nil {
} else if translated, parameterSources, err := translate.TranslateWithOptionsAndParameterSources(ctx, regularQuery, s, parameters, graphID, translationOptions); err != nil {
return "", translationCacheBuildResult{}, err
} else if sqlQuery, err := translate.Translated(translated); err != nil {
return "", translationCacheBuildResult{}, err
Expand All @@ -33,6 +95,14 @@ func (s *SchemaManager) compile(ctx context.Context, source string, parameters m
}
}

if !optimized {
return translationCache.BuildUnoptimized(build)
}

key := translationCache.Key(source, graphID, parameters)
return translationCache.GetOrBuildContext(ctx, key, parameters, build)
}

func commentRegularQuery(source, sql string) string {
return "-- " + builderCommentNewlines.Replace(source) + "\n" + sql
}
Loading
Loading