Port Compile API extensions from EOPA (#7887)

* server: port compile API

Also adds e2e tests: These include coverage for ucast in the prisma
setting, and thus require some JS runtime.

* e2e: selectively skip e2e Compile API tests

...for macos runs, and for the go-compat suites.

* server: accept timer_rego_external_resolve_ns metrics with value 0

When running the tests in a loop for a while, I would see values of 0ns
for this metric. However, comparing with its non-zero values, which are
often 41 or 42ns, it seems like this is just not happening in this code
path. So if "almost nothing" actually goes below 1ns, it's OK.

* e2e: split dep-heavy e2e tests into their own go module
* Makefile: export DOCKER_RUNNING (make e2e read it)

---------

Co-authored-by: Philip Conrad <philip@chariot-chaser.net>
Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
This commit is contained in:
Stephan Renatus
2025-09-22 12:11:06 +02:00
committed by GitHub
parent 5816e4f4f8
commit 4ab2ac0c61
243 changed files with 33516 additions and 4256 deletions
+16
View File
@@ -13,6 +13,22 @@ updates:
- "*"
exclude-patterns:
- "go.opentelemetry.io/*"
- package-ecosystem: gomod
directory: /e2e
schedule:
interval: monthly
groups:
all:
patterns:
- "*"
- package-ecosystem: npm
directory: /e2e/api/compile/prisma
schedule:
interval: monthly
groups:
e2e/prisma:
patterns:
- "*"
- package-ecosystem: github-actions
directory: /
schedule:
+6
View File
@@ -224,6 +224,9 @@ jobs:
with:
go-version: ${{ steps.go_version.outputs.go_version }}
- name: Install Node
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
- name: Download generated artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
@@ -233,6 +236,9 @@ jobs:
run: make test-coverage
timeout-minutes: 30
- name: E2E Test Golang
run: make e2e
go-lint:
name: Go Lint
runs-on: ubuntu-24.04
+8 -1
View File
@@ -32,7 +32,7 @@ GOLANGCI_LINT_VERSION := v2.4.0
YAML_LINT_VERSION := 0.29.0
YAML_LINT_FORMAT ?= auto
DOCKER_RUNNING ?= $(shell docker ps >/dev/null 2>&1 && echo 1 || echo 0)
export DOCKER_RUNNING ?= $(shell docker ps >/dev/null 2>&1 && echo 1 || echo 0)
# We use root because the windows build, invoked through the ci-go-build-windows
# target, installs the gcc mingw32 cross-compiler.
@@ -117,6 +117,13 @@ install: generate
.PHONY: test
test: go-test wasm-test
.PHONY: e2e e2e-prep
e2e: e2e-prep
cd e2e/ && $(GO) test $(GO_TAGS) -v ./...
e2e-prep:
cd e2e/api/compile/prisma && npm ci
.PHONY: test-short
test-short: go-test-short
+6
View File
@@ -0,0 +1,6 @@
# github.com/open-policy-agent/opa/e2e
This Go module is for end-to-end tests that come with dependencies we don't otherwise need for OPA, to avoid bloat in OPA's Go module's deps.
> [!warning]
> This module should never be imported from any of the other `github.com/open-policy-agent/opa/*` packages!
+794
View File
@@ -0,0 +1,794 @@
// Copyright 2025 The OPA Authors
// SPDX-License-Identifier: Apache-2.0
package compile
import (
"bufio"
"bytes"
"context"
"database/sql"
_ "embed"
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
"slices"
"strings"
"testing"
"time"
_ "github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
_ "github.com/microsoft/go-mssqldb"
_ "modernc.org/sqlite"
"github.com/docker/go-connections/nat"
"github.com/google/go-cmp/cmp"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"github.com/open-policy-agent/opa/v1/test/e2e"
)
type DBType string
const (
Postgres DBType = "postgresql"
MySQL DBType = "mysql"
MSSQL DBType = "sqlserver"
SQLite DBType = "sqlite"
)
// TestConfig holds test configuration
type TestConfig struct {
db *sql.DB
dbName string
dbType DBType
dbURL string
}
// containerConfig holds database-specific container configuration
type containerConfig struct {
image string
port string
env map[string]string
waitFor wait.Strategy
urlTemplate string
}
var dbConfigs = map[DBType]containerConfig{
Postgres: {
image: "postgres:17-alpine",
port: "5432/tcp",
env: map[string]string{
"POSTGRES_DB": "testdb",
"POSTGRES_USER": "testuser",
"POSTGRES_PASSWORD": "testpass",
},
waitFor: wait.ForLog("database system is ready to accept connections").
WithOccurrence(2).
WithStartupTimeout(15 * time.Second),
urlTemplate: "postgres://testuser:testpass@%s:%s/testdb?sslmode=disable",
},
MySQL: {
image: "mysql:9",
port: "3306/tcp",
env: map[string]string{
"MYSQL_DATABASE": "testdb",
"MYSQL_USER": "testuser",
"MYSQL_PASSWORD": "testpass",
"MYSQL_ROOT_PASSWORD": "rootpass",
},
waitFor: wait.ForLog("port: 3306 MySQL Community Server"),
urlTemplate: "testuser:testpass@tcp(%s:%s)/testdb",
},
MSSQL: {
image: "mcr.microsoft.com/mssql/server:2022-latest",
port: "1433/tcp",
env: map[string]string{
"ACCEPT_EULA": "Y",
"MSSQL_SA_PASSWORD": "MyStr0ngPassw0rd!",
},
waitFor: wait.ForLog("Recovery is complete."),
urlTemplate: "sqlserver://sa:MyStr0ngPassw0rd!@%s:%s",
},
SQLite: {
urlTemplate: ":memory:",
},
}
// setupTestContainer creates and starts a database container
func setupTestContainer(ctx context.Context, dbType DBType) (testcontainers.Container, string, error) {
if dbType == SQLite {
return nil, ":memory:", nil
}
config := dbConfigs[dbType]
containerReq := testcontainers.ContainerRequest{
Image: config.image,
ExposedPorts: []string{config.port},
WaitingFor: config.waitFor,
Env: config.env,
}
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: containerReq,
Started: true,
})
if err != nil {
return nil, "", fmt.Errorf("failed to start container: %v", err)
}
port, err := container.MappedPort(ctx, nat.Port(config.port))
if err != nil {
return nil, "", fmt.Errorf("failed to get container port: %v", err)
}
host, err := container.Host(ctx)
if err != nil {
return nil, "", fmt.Errorf("failed to get container host: %v", err)
}
dbURL := fmt.Sprintf(config.urlTemplate, host, port.Port())
return container, dbURL, nil
}
// getCreateTableSQL returns database-specific CREATE TABLE SQL
func getCreateTableSQL(dbType DBType) string {
switch dbType {
case Postgres:
return `
CREATE TABLE IF NOT EXISTS fruit (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
colour VARCHAR(100) NOT NULL,
price INT
)`
case MySQL:
return `
CREATE TABLE IF NOT EXISTS fruit (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
colour VARCHAR(100) NOT NULL,
price INT
)`
case SQLite:
return `
CREATE TABLE IF NOT EXISTS fruit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) NOT NULL,
colour VARCHAR(100) NOT NULL,
price INT
)`
case MSSQL:
return `
IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='fruit' AND xtype='U')
CREATE TABLE fruit (
id INT IDENTITY(1,1) PRIMARY KEY,
name NVARCHAR(100) NOT NULL,
colour NVARCHAR(100) NOT NULL,
price INT
)`
}
panic("unknown db type")
}
// initializeTestData sets up initial test data in the database
func initializeTestData(db *sql.DB, dbType DBType) error {
createTableSQL := getCreateTableSQL(dbType)
if _, err := db.Exec(createTableSQL); err != nil {
return fmt.Errorf("failed to create table: %v", err)
}
// Insert test data - using parameterized queries for better compatibility
var insertDataSQL string
switch dbType {
case Postgres:
insertDataSQL = "INSERT INTO fruit (name, colour, price) VALUES ($1, $2, $3)"
case MSSQL:
insertDataSQL = "INSERT INTO fruit (name, colour, price) VALUES (@p1, @p2, @p3)"
default:
insertDataSQL = "INSERT INTO fruit (name, colour, price) VALUES (?, ?, ?)"
}
for _, f := range []struct {
name string
colour string
price int
}{
{"apple", "green", 10},
{"banana", "yellow", 20},
{"cherry", "red", 11},
} {
if _, err := db.Exec(insertDataSQL, f.name, f.colour, f.price); err != nil {
return fmt.Errorf("failed to insert test data: %v", err)
}
}
if _, err := db.Exec(insertDataSQL, "orange", "orange", nil); err != nil {
return fmt.Errorf("failed to insert test data: %v", err)
}
return nil
}
// setupDB initializes a test database of the specified type
func setupDB(t *testing.T, dbType DBType) (*TestConfig, func()) {
t.Helper()
ctx := context.Background()
container, dbURL, err := setupTestContainer(ctx, dbType)
if err != nil {
t.Fatalf("failed to setup test container: %v", err)
}
typ := string(dbType)
if dbType == Postgres {
typ = "postgres"
}
db, err := sql.Open(typ, dbURL)
if err != nil {
t.Fatalf("failed to connect to database: %v", err)
}
if dbType == SQLite {
db.SetMaxOpenConns(1)
}
if err := initializeTestData(db, dbType); err != nil {
t.Fatalf("failed to initialize test data: %v", err)
}
cleanup := func() {
if container == nil {
return
}
if err := db.Close(); err != nil {
t.Errorf("failed to close database connection: %v", err)
}
if err := container.Terminate(ctx); err != nil {
t.Errorf("failed to terminate container: %v", err)
}
}
return &TestConfig{
db: db,
dbName: "testdb",
dbType: dbType,
dbURL: dbURL,
}, cleanup
}
type fruitRow struct {
ID int
Name string
Colour string
Price sql.NullInt64
}
type fruitJSON struct {
ID int
Name string
Colour string
Price *int
}
func toFruitRows(xs []fruitJSON) []fruitRow {
rows := make([]fruitRow, len(xs))
for i, x := range xs {
rows[i] = fruitRow{
ID: x.ID,
Name: x.Name,
Colour: x.Colour,
}
if x.Price != nil {
rows[i].Price = sql.NullInt64{Int64: int64(*x.Price), Valid: true}
}
}
return rows
}
// In these test, we test the compile API end-to-end. We start an instance of
// OPA, load a policy, and then run a series of tests that compile a query and
// then execute it against a database. The query is a simple "include" query
// that filters rows from a table based on some conditions. The conditions are
// defined in the data policy.
func TestCompileHappyPathE2E(t *testing.T) {
// NOTE(sr): Technically, we don't need a "docker" binary for these tests to use
// docker via testcontainers. But being able to run "docker ps" is an acceptable litmus
// test. By relying on the Makefile checking this for us, we don't have to adjust all
// the test runs (basic linux/windows, go-compat, race-detector).
if os.Getenv("DOCKER_RUNNING") != "1" {
t.Skip("docker-dependant tests are not runnable")
}
ctx, cancel := context.WithCancel(t.Context())
params := e2e.NewAPIServerTestParams()
params.Addrs = &[]string{"0.0.0.0:0"}
testRuntime, err := e2e.NewTestRuntime(params)
if err != nil {
t.Fatal(err)
}
done := make(chan bool)
go func() {
err := testRuntime.Runtime.Serve(ctx)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
done <- true
}()
t.Cleanup(cancel)
if err := testRuntime.WaitForServer(); err != nil {
t.Fatal(err)
}
opaURL := testRuntime.URL()
t.Log(opaURL)
dbTypes := []DBType{Postgres, MySQL, MSSQL, SQLite}
unknowns := []string{"input.fruits"}
var input any = map[string]any{"fav_colour": "yellow"}
path := `filters%d/include`
mapping := map[string]any{
"fruits": map[string]any{
"$self": "fruit",
},
}
price := func(x int) sql.NullInt64 { return sql.NullInt64{Int64: int64(x), Valid: true} }
apple := fruitRow{ID: 1, Name: "apple", Colour: "green", Price: price(10)}
banana := fruitRow{ID: 2, Name: "banana", Colour: "yellow", Price: price(20)}
cherry := fruitRow{ID: 3, Name: "cherry", Colour: "red", Price: price(11)}
orange := fruitRow{ID: 4, Name: "orange", Colour: "orange"}
tests := []struct {
name string
policy string
expRows []fruitRow
prisma bool
exclude []DBType
}{
{
name: "no conditions",
policy: `include if true`,
expRows: []fruitRow{apple, banana, cherry, orange},
prisma: true,
},
{
name: "unconditional NO",
policy: `include if false`,
prisma: true,
},
{
name: "simple equality",
policy: `include if input.fruits.colour == input.fav_colour`,
expRows: []fruitRow{banana},
prisma: true,
},
{
name: "comparison with two unknowns",
policy: `include if input.fruits.price >= input.fruits.id`,
expRows: []fruitRow{apple, banana, cherry},
},
{
name: "simple comparison",
policy: `include if input.fruits.price < 11`,
expRows: []fruitRow{apple},
prisma: true,
},
{
name: "equal null",
policy: `include if input.fruits.price == null`,
expRows: []fruitRow{orange},
prisma: true,
},
{
name: "not equal null",
policy: `include if input.fruits.price != null`,
expRows: []fruitRow{apple, banana, cherry},
prisma: true,
},
{
name: "simple startswith",
policy: `include if startswith(input.fruits.name, "app")`,
expRows: []fruitRow{apple},
prisma: true,
exclude: []DBType{SQLite},
},
{
name: "simple contains",
policy: `include if contains(input.fruits.name, "a")`,
expRows: []fruitRow{apple, banana, orange},
prisma: true,
exclude: []DBType{SQLite},
},
{
name: "startswith + escaping '_'",
policy: `include if startswith(input.fruits.name, "ap_")`, // if "_" wasn't escaped properly, it would match "apple"
expRows: nil,
exclude: []DBType{SQLite},
},
{
name: "startswith + escaping '%'",
policy: `include if startswith(input.fruits.name, "%ppl")`, // if "%" wasn't escaped properly, it would match "apple"
expRows: nil,
exclude: []DBType{SQLite},
},
{
name: "simple endswith",
policy: `include if endswith(input.fruits.name, "le")`,
expRows: []fruitRow{apple},
prisma: true,
exclude: []DBType{SQLite},
},
{
name: "internal.member_2",
policy: `include if input.fruits.name in {"apple", "cherry", "pineapple"}`,
expRows: []fruitRow{apple, cherry},
prisma: true,
},
{
name: "conjunct query, inequality",
policy: `include if {
input.fruits.name != "apple"
input.fruits.name != "banana"
}`,
expRows: []fruitRow{cherry, orange},
prisma: true,
},
{
name: "disjunct query, equality",
policy: `include if input.fruits.name == "apple"
include if input.fruits.name == "banana"`,
expRows: []fruitRow{apple, banana},
prisma: true,
},
{
name: "not+internal.member_2",
policy: `include if not input.fruits.name in {"apple", "cherry", "pineapple", "orange"}`,
expRows: []fruitRow{banana},
prisma: true,
},
{
name: "not+lt",
policy: `include if not input.fruits.price < 12`,
expRows: []fruitRow{banana},
prisma: true,
},
}
for _, dbType := range dbTypes {
t.Run(string(dbType), func(t *testing.T) {
t.Parallel()
config, cleanup := setupDB(t, dbType)
t.Cleanup(cleanup)
for i, tt := range tests {
// first, we override the policy with the current test case
policy := fmt.Sprintf("package filters%d\n%s", i, tt.policy)
req, err := http.NewRequest("PUT", fmt.Sprintf("%s/v1/policies/policy%d.rego", opaURL, i), strings.NewReader(policy))
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
if _, err := http.DefaultClient.Do(req); err != nil {
t.Fatalf("post policy: %v", err)
}
path := fmt.Sprintf(path, i)
if tt.prisma && dbType == Postgres {
t.Run("prisma/"+tt.name, func(t *testing.T) { // also run via our prisma contraption
t.Parallel()
// second, query the compile API
payload := map[string]any{
"input": input,
"unknowns": unknowns,
"options": map[string]any{
"targetSQLTableMappings": map[string]any{
"ucast": mapping,
},
},
}
// get the UCAST IR, process with @styra/ucast-prisma, and run a findMany query
// with that against the DB, return rows
rowsData := getUCASTAndRunPrisma(t, path, payload, config, opaURL)
if diff := cmp.Diff(tt.expRows, rowsData); diff != "" {
t.Errorf("unexpected result (-want +got):\n%s", diff)
}
})
}
t.Run("db/"+tt.name, func(t *testing.T) {
t.Parallel()
if slices.Contains(tt.exclude, dbType) {
t.Skip("skipped via exclude")
}
// second, query the compile API
mappings := make(map[string]any, 1)
mappings[string(config.dbType)] = mapping
payload := map[string]any{
"input": input,
"unknowns": unknowns,
"options": map[string]any{
"targetSQLTableMappings": mappings,
},
}
// get the SQL where clauses, and run the concatenated query against db,
// return rows
rowsData := getSQLAndRunQuery(t, path, payload, config, opaURL)
// finally, compare with expected!
if diff := cmp.Diff(tt.expRows, rowsData); diff != "" {
t.Errorf("unexpected result (-want +got):\n%s", diff)
}
})
}
})
}
}
// This test runs three Compile API queries and asserts that the handler
// execution added something to the exposed prometheus metrics at /v1/metrics.
// Also, it checks that the cache hit/miss metrics have been exposed accordingly.
func TestPrometheusMetrics(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
params := e2e.NewAPIServerTestParams()
params.Addrs = &[]string{"0.0.0.0:0"}
testRuntime, err := e2e.NewTestRuntime(params)
if err != nil {
t.Fatal(err)
}
done := make(chan bool)
go func() {
err := testRuntime.Runtime.Serve(ctx)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
done <- true
}()
t.Cleanup(cancel)
if err := testRuntime.WaitForServer(); err != nil {
t.Fatal(err)
}
opaURL := testRuntime.URL()
input := map[string]any{"fav_colour": "yellow"}
path := `filters/include`
policy := `package filters
# METADATA
# scope: document
# custom:
# unknowns: [input.fruits]
include if input.fruits.name == "banana"
`
{ // exercise the Compile API
req, err := http.NewRequest("PUT", opaURL+"/v1/policies/policy.rego", strings.NewReader(policy))
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
if _, err := http.DefaultClient.Do(req); err != nil {
t.Fatalf("post policy: %v", err)
}
// query the compile API
payload := map[string]any{
"input": input,
}
queryBytes, err := json.Marshal(payload)
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
for range 3 {
// POST to Compile API
req, err = http.NewRequest("POST",
opaURL+"/v1/compile/"+path,
strings.NewReader(string(queryBytes)))
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/vnd.opa.sql.postgresql+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("failed to execute request: %v", err)
}
defer resp.Body.Close()
var respPayload struct {
Result struct {
Query any `json:"query"`
} `json:"result"`
}
if err := json.NewDecoder(resp.Body).Decode(&respPayload); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if exp, act := "WHERE fruits.name = E'banana'", respPayload.Result.Query; exp != act {
t.Errorf("response: expected %v, got %v (response: %v)", exp, act, respPayload)
}
}
}
{ // check the /v1/metrics endpoint for the handler invokation lines
needle := `http_request_duration_seconds_bucket{code="200",handler="v1/compile",method="post",`
act := findMetrics(t, opaURL, needle)
// NOTE(sr): The metric is a histogram, and we did multiple requests; so we only check
// for ANY lines being present, not their actual count.
if len(act) < 1 {
t.Errorf("expected v1/compile lines in metrics, got %v", act)
}
}
}
func findMetrics(t *testing.T, eopaURL string, needles ...string) []string {
t.Helper()
founds := []string{}
resp, err := http.Get(eopaURL + "/metrics")
if err != nil {
t.Fatalf("failed to send request: %v", err)
}
// search for line containing v1/compile
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
for i := range needles {
if strings.Contains(scanner.Text(), needles[i]) {
founds = append(founds, scanner.Text())
}
}
}
if err := scanner.Err(); err != nil {
t.Fatalf("failed to scan response: %v", err)
}
return founds
}
func getSQLAndRunQuery(t *testing.T, path string, payload map[string]any, config *TestConfig, opaURL string) []fruitRow {
t.Helper()
queryBytes, err := json.Marshal(payload)
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
req, err := http.NewRequest("POST",
opaURL+"/v1/compile/"+path,
strings.NewReader(string(queryBytes)))
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", fmt.Sprintf("application/vnd.opa.sql.%s+json", config.dbType))
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("failed to execute request: %v", err)
}
defer resp.Body.Close()
if status := resp.StatusCode; status != http.StatusOK {
t.Errorf("expected status %v, got %v", http.StatusOK, status)
}
var respPayload struct {
Result struct {
Query any `json:"query"`
} `json:"result"`
}
if err := json.NewDecoder(resp.Body).Decode(&respPayload); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
t.Log(respPayload)
var rowsData []fruitRow
var whereClauses string
switch w := respPayload.Result.Query.(type) {
case nil: // unconditional NO
return rowsData // empty
case string:
whereClauses = w
}
// finally, query the database with the resulting WHERE clauses
stmt := "SELECT * FROM fruit " + whereClauses
rows, err := config.db.Query(stmt)
if err != nil {
t.Fatalf("%s: error: %v", stmt, err)
}
// collect rows into rowsData
for rows.Next() {
var fruit fruitRow
// scan row into fruit, ignoring created_at
if err := rows.Scan(&fruit.ID, &fruit.Name, &fruit.Colour, &fruit.Price); err != nil {
t.Fatalf("failed to scan row: %v", err)
}
rowsData = append(rowsData, fruit)
}
return rowsData
}
func getUCASTAndRunPrisma(t *testing.T, path string, payload map[string]any, config *TestConfig, opaURL string) []fruitRow {
t.Helper()
queryBytes, err := json.Marshal(payload)
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
// Query EOPA for UCAST IR
req, err := http.NewRequest("POST",
opaURL+"/v1/compile/"+path,
strings.NewReader(string(queryBytes)))
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/vnd.opa.ucast.prisma+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("failed to execute request: %v", err)
}
defer resp.Body.Close()
if status := resp.StatusCode; status != http.StatusOK {
t.Errorf("expected status %v, got %v", http.StatusOK, status)
}
var respPayload struct {
Result struct {
Query map[string]any `json:"query"`
} `json:"result"`
}
if err := json.NewDecoder(resp.Body).Decode(&respPayload); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
t.Log("ucast IR:", respPayload.Result.Query)
// Execute prisma script with UCAST IR as input
cmd := exec.Command("node", "index.js")
cmd.Dir = "./prisma"
cmd.Env = append(cmd.Env, "DATABASE_URL="+config.dbURL)
stdin, err := cmd.StdinPipe()
if err != nil {
t.Fatalf("failed to get stdin pipe: %v", err)
}
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
t.Fatalf("failed to start prisma script: %v", err)
}
// Write UCAST IR to stdin
if err := json.NewEncoder(stdin).Encode(respPayload.Result.Query); err != nil {
t.Fatalf("failed to write to stdin: %v", err)
}
stdin.Close()
if err := cmd.Wait(); err != nil {
t.Fatalf("prisma script failed: %v\nstderr: %s", err, stderr.String())
}
t.Log("prisma query:", stderr.String())
// Parse the output into []fruitRow
if stdout.Len() == 0 {
return nil
}
var rowsData []fruitJSON
if err := json.NewDecoder(&stdout).Decode(&rowsData); err != nil {
t.Fatalf("failed to decode prisma output: %v\noutput was: %s", err, stdout.String())
}
return toFruitRows(rowsData)
}
+188
View File
@@ -0,0 +1,188 @@
// Copyright 2025 The OPA Authors
// SPDX-License-Identifier: Apache-2.0
package compile
import (
"context"
"encoding/json"
"fmt"
"net/http"
"slices"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/open-policy-agent/opa/v1/logging/test"
"github.com/open-policy-agent/opa/v1/runtime"
"github.com/open-policy-agent/opa/v1/test/e2e"
)
var ignores = []string{
"timestamp",
"metrics",
"labels",
"intermediate",
"requested_by",
}
var stdIgnores = cmpopts.IgnoreMapEntries(func(k string, _ any) bool {
return slices.Contains(ignores, k)
})
func TestDecisionLogsCompileAPIResult(t *testing.T) {
policy := `
package filters
# METADATA
# custom:
# unknowns: [input.fruits]
# mask_rule: data.filters.mask
include if input.fruits.name in input.favorites
default mask.fruits.supplier := {"replace": {"value": "***"}}
`
path := "filters/include"
params := e2e.NewAPIServerTestParams()
params.ConfigOverrides = []string{
"decision_logs.console=true",
}
params.Logging = runtime.LoggingConfig{Level: "error"}
consoleLogger := test.New()
params.ConsoleLogger = consoleLogger
testRuntime, err := e2e.NewTestRuntime(params)
if err != nil {
t.Fatal(err)
}
testRuntime.ConsoleLogger = consoleLogger
ctx, cancel := context.WithCancel(t.Context())
done := make(chan bool)
go func() {
err := testRuntime.Runtime.Serve(ctx)
if err != nil {
t.Errorf("Unexpected error: %s", err)
}
done <- true
}()
t.Cleanup(cancel)
if err := testRuntime.WaitForServer(); err != nil {
t.Fatal(err)
}
opaURL := testRuntime.URL()
{ // store policy
req, err := http.NewRequest("PUT", opaURL+"/v1/policies/policy.rego", strings.NewReader(policy))
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
if _, err := http.DefaultClient.Do(req); err != nil {
t.Fatalf("put policy: %v", err)
}
}
{ // act: send Compile API request
input := map[string]any{"favorites": []string{"banana", "orange"}}
payload := map[string]any{
"input": input,
"options": map[string]any{
"targetSQLTableMappings": map[string]any{
"postgresql": map[string]any{
"fruits": map[string]string{
"$self": "f",
"name": "n",
},
},
},
},
}
queryBytes, err := json.Marshal(payload)
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
req, err := http.NewRequest("POST",
fmt.Sprintf("%s/v1/compile/%s", opaURL, path),
strings.NewReader(string(queryBytes)))
if err != nil {
t.Fatalf("failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/vnd.opa.sql.postgresql+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("failed to execute request: %v", err)
}
defer resp.Body.Close()
var respPayload struct {
Result struct {
Query any `json:"query"`
Masks map[string]any `json:"masks,omitempty"`
} `json:"result"`
}
if err := json.NewDecoder(resp.Body).Decode(&respPayload); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
if exp, act := "WHERE f.n IN (E'banana', E'orange')", respPayload.Result.Query; exp != act {
t.Errorf("response: expected %v, got %v (response: %v)", exp, act, respPayload)
}
exp, act := map[string]any{"fruits": map[string]any{"supplier": map[string]any{"replace": map[string]any{"value": "***"}}}}, respPayload.Result.Masks
if diff := cmp.Diff(exp, act); diff != "" {
t.Errorf("response: expected %v, got %v (response: %v)", exp, act, respPayload)
}
}
var entry test.LogEntry
for _, entry = range testRuntime.ConsoleLogger.Entries() {
if entry.Message == "Decision Log" {
break
}
}
if entry.Message == "" {
t.Fatal("no DL messages logged")
}
dl := map[string]any{
"decision_id": "",
"path": path,
"result": map[string]any{
"query": "WHERE f.n IN (E'banana', E'orange')",
"masks": map[string]any{
"fruits": map[string]any{
"supplier": map[string]any{
"replace": map[string]any{"value": "***"},
},
},
},
},
"input": map[string]any{
"favorites": []any{"banana", "orange"},
},
"req_id": json.Number("3"),
"type": "openpolicyagent.org/decision_logs",
"custom": map[string]any{
"options": map[string]any{
"targetSQLTableMappings": map[string]any{
"postgresql": map[string]any{
"fruits": map[string]any{
"$self": "f",
"name": "n",
},
},
},
},
"unknowns": []any{"input.fruits"},
"type": "open-policy-agent/compile",
"mask_rule": "data.filters.mask",
},
}
if diff := cmp.Diff(dl, entry.Fields, stdIgnores); diff != "" {
t.Errorf("diff: (-want +got):\n%s", diff)
}
}
+1
View File
@@ -0,0 +1 @@
/node_modules
+26
View File
@@ -0,0 +1,26 @@
# E2E Prisma helper
This is a tiny script that is used to run our E2E tests via Prisma and [@open-policy-agent/ucast-prisma](https://www.npmjs.com/package/@open-policy-agent/ucast-prisma).
It's invoked by `e2e/compile/e2e_test.go` for a subset of the tests.
It reads UCAST conditions (as JSON) on STDIN, converts them to Prisma filters, and then runs
a "SELECT * FROM ..." query (findMany) against the `fruits` table like the other tests.
The returned rows are printed as JSON on STDOUT.
## Test a change
To use a local change to ucast-prisma with the Compile E2E suite, you'll have to do this:
In `package.json`, replace the line with "@open-policy-agent/ucast-prisma" with the following:
```json
"@open-policy-agent/ucast-prisma": "file:../../../opa-typescript/packages/ucast-prisma"
```
Also run `npm run build` in "opa-typescript/packages/ucast-prisma" to make sure the changes are built.
Run `npm i` in `e2e/prisma`, and then run the E2E tests for the prisma-enabled subset:
```
go test -tags e2e ./compile -run 'TestCompileHappyPathE2E/postgres/prisma/.' -v
```
+46
View File
@@ -0,0 +1,46 @@
/**
* Copyright 2025 The OPA Authors
* SPDX-License-Identifier: Apache-2.0
*/
const util = require("util");
const { PrismaClient } = require("@prisma/client");
const { ucastToPrisma } = require("@open-policy-agent/ucast-prisma");
const prisma = new PrismaClient();
// Function to read JSON from stdin
async function getStdinJson() {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
const data = Buffer.concat(chunks).toString();
try {
return JSON.parse(data);
} catch (error) {
throw new Error("Invalid JSON input");
}
}
async function main() {
try {
const filters = await getStdinJson();
if (filters === null) return;
const where = ucastToPrisma(filters, "fruit", {
fruits: { $self: "fruit" },
});
console.error(util.inspect(where, { depth: null }));
const results = await prisma.fruit.findMany({ where });
process.stdout.write(JSON.stringify(results, null, 2));
} catch (error) {
console.error("Error:", error.message);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
// Run the script
main();
+456
View File
@@ -0,0 +1,456 @@
{
"name": "prisma-ucast-e2e",
"version": "0.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "prisma-ucast-e2e",
"version": "0.0.1",
"dependencies": {
"@open-policy-agent/ucast-prisma": "^0.1.6",
"@prisma/client": "^6.15.0"
},
"devDependencies": {
"prisma": "^6.15.0"
}
},
"node_modules/@open-policy-agent/ucast-prisma": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@open-policy-agent/ucast-prisma/-/ucast-prisma-0.1.6.tgz",
"integrity": "sha512-4bnz9aZzQQyfoo6OrXCWA8IH6Kp6pB34e4A2vGv2B0S/dbfFVN8TzmUn+WTJg7Eyr/NiF2EPFShiFgBwbUhhrA==",
"license": "Apache-2.0",
"dependencies": {
"@ucast/core": "^1.10.1",
"lodash.merge": "^4.6.2"
}
},
"node_modules/@prisma/client": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.15.0.tgz",
"integrity": "sha512-wR2LXUbOH4cL/WToatI/Y2c7uzni76oNFND7+23ypLllBmIS8e3ZHhO+nud9iXSXKFt1SoM3fTZvHawg63emZw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
},
"peerDependencies": {
"prisma": "*",
"typescript": ">=5.1.0"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/@prisma/config": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.15.0.tgz",
"integrity": "sha512-KMEoec9b2u6zX0EbSEx/dRpx1oNLjqJEBZYyK0S3TTIbZ7GEGoVyGyFRk4C72+A38cuPLbfQGQvgOD+gBErKlA==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"c12": "3.1.0",
"deepmerge-ts": "7.1.5",
"effect": "3.16.12",
"empathic": "2.0.0"
}
},
"node_modules/@prisma/debug": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.15.0.tgz",
"integrity": "sha512-y7cSeLuQmyt+A3hstAs6tsuAiVXSnw9T55ra77z0nbNkA8Lcq9rNcQg6PI00by/+WnE/aMRJ/W7sZWn2cgIy1g==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.15.0.tgz",
"integrity": "sha512-opITiR5ddFJ1N2iqa7mkRlohCZqVSsHhRcc29QXeldMljOf4FSellLT0J5goVb64EzRTKcIDeIsJBgmilNcKxA==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.15.0",
"@prisma/engines-version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
"@prisma/fetch-engine": "6.15.0",
"@prisma/get-platform": "6.15.0"
}
},
"node_modules/@prisma/engines-version": {
"version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb.tgz",
"integrity": "sha512-a/46aK5j6L3ePwilZYEgYDPrhBQ/n4gYjLxT5YncUTJJNRnTCVjPF86QdzUOLRdYjCLfhtZp9aum90W0J+trrg==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.15.0.tgz",
"integrity": "sha512-xcT5f6b+OWBq6vTUnRCc7qL+Im570CtwvgSj+0MTSGA1o9UDSKZ/WANvwtiRXdbYWECpyC3CukoG3A04VTAPHw==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.15.0",
"@prisma/engines-version": "6.15.0-5.85179d7826409ee107a6ba334b5e305ae3fba9fb",
"@prisma/get-platform": "6.15.0"
}
},
"node_modules/@prisma/get-platform": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.15.0.tgz",
"integrity": "sha512-Jbb+Xbxyp05NSR1x2epabetHiXvpO8tdN2YNoWoA/ZsbYyxxu/CO/ROBauIFuMXs3Ti+W7N7SJtWsHGaWte9Rg==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.15.0"
}
},
"node_modules/@standard-schema/spec": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/@ucast/core": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz",
"integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==",
"license": "Apache-2.0"
},
"node_modules/c12": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz",
"integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.3",
"confbox": "^0.2.2",
"defu": "^6.1.4",
"dotenv": "^16.6.1",
"exsolve": "^1.0.7",
"giget": "^2.0.0",
"jiti": "^2.4.2",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"perfect-debounce": "^1.0.0",
"pkg-types": "^2.2.0",
"rc9": "^2.1.2"
},
"peerDependencies": {
"magicast": "^0.3.5"
},
"peerDependenciesMeta": {
"magicast": {
"optional": true
}
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/citty": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz",
"integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"consola": "^3.2.3"
}
},
"node_modules/confbox": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/consola": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
"integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": "^14.18.0 || >=16.10.0"
}
},
"node_modules/deepmerge-ts": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
"integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==",
"devOptional": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"devOptional": true,
"license": "MIT"
},
"node_modules/destr": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
"devOptional": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/effect": {
"version": "3.16.12",
"resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz",
"integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"fast-check": "^3.23.1"
}
},
"node_modules/empathic": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
"integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/exsolve": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
"integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
"devOptional": true,
"license": "MIT"
},
"node_modules/fast-check": {
"version": "3.23.2",
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz",
"integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==",
"devOptional": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT",
"dependencies": {
"pure-rand": "^6.1.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/giget": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz",
"integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"citty": "^0.1.6",
"consola": "^3.4.0",
"defu": "^6.1.4",
"node-fetch-native": "^1.6.6",
"nypm": "^0.6.0",
"pathe": "^2.0.3"
},
"bin": {
"giget": "dist/cli.mjs"
}
},
"node_modules/jiti": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
"integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
"devOptional": true,
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"license": "MIT"
},
"node_modules/node-fetch-native": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
"integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==",
"devOptional": true,
"license": "MIT"
},
"node_modules/nypm": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz",
"integrity": "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"citty": "^0.1.6",
"consola": "^3.4.2",
"pathe": "^2.0.3",
"pkg-types": "^2.2.0",
"tinyexec": "^1.0.1"
},
"bin": {
"nypm": "dist/cli.mjs"
},
"engines": {
"node": "^14.16.0 || >=16.10.0"
}
},
"node_modules/ohash": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
"devOptional": true,
"license": "MIT"
},
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"devOptional": true,
"license": "MIT"
},
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/pkg-types": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
"integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"confbox": "^0.2.2",
"exsolve": "^1.0.7",
"pathe": "^2.0.3"
}
},
"node_modules/prisma": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.15.0.tgz",
"integrity": "sha512-E6RCgOt+kUVtjtZgLQDBJ6md2tDItLJNExwI0XJeBc1FKL+Vwb+ovxXxuok9r8oBgsOXBA33fGDuE/0qDdCWqQ==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/config": "6.15.0",
"@prisma/engines": "6.15.0"
},
"bin": {
"prisma": "build/index.js"
},
"engines": {
"node": ">=18.18"
},
"peerDependencies": {
"typescript": ">=5.1.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/pure-rand": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz",
"integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==",
"devOptional": true,
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/dubzzz"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fast-check"
}
],
"license": "MIT"
},
"node_modules/rc9": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"defu": "^6.1.4",
"destr": "^2.0.3"
}
},
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"devOptional": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/tinyexec": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz",
"integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==",
"devOptional": true,
"license": "MIT"
}
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"name": "prisma-ucast-e2e",
"version": "0.0.1",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "",
"description": "",
"dependencies": {
"@prisma/client": "^6.15.0",
"@open-policy-agent/ucast-prisma": "^0.1.6"
},
"devDependencies": {
"prisma": "^6.15.0"
}
}
@@ -0,0 +1,18 @@
generator client {
provider = "prisma-client-js"
}
// Turns out using multiple databases is actually not simple:
// https://github.com/prisma/prisma/issues/2443
// So let's test only PG for now, as it keeps the code generation simpler.
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model fruit {
id Int @id @default(autoincrement())
name String @db.VarChar(100)
colour String @db.VarChar(100)
price Int?
}
+141
View File
@@ -0,0 +1,141 @@
module github.com/open-policy-agent/opa/e2e
go 1.25.0
// Always use OPA from the same checkout.
replace github.com/open-policy-agent/opa => ../
require (
github.com/docker/go-connections v0.6.0
github.com/go-sql-driver/mysql v1.9.3
github.com/google/go-cmp v0.7.0
github.com/lib/pq v1.10.9
github.com/microsoft/go-mssqldb v1.9.3
github.com/open-policy-agent/opa v1.8.0
github.com/testcontainers/testcontainers-go v0.38.0
modernc.org/sqlite v1.38.2
)
require (
dario.cat/mergo v1.0.1 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/containerd/v2 v2.1.4 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v1.0.0-rc.1 // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/dgraph-io/badger/v4 v4.8.0 // indirect
github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.2.2+incompatible // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/google/flatbuffers v25.2.10+incompatible // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huandu/go-sqlbuilder v1.36.1 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc/v3 v3.0.0 // indirect
github.com/lestrrat-go/jwx/v3 v3.0.10 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.1.0 // indirect
github.com/moby/locker v1.0.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/peterh/liner v1.2.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_golang v1.23.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.5 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
github.com/vektah/gqlparser/v2 v2.5.30 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/yashtewari/glob-intersection v0.2.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
google.golang.org/grpc v1.75.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.66.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
oras.land/oras-go/v2 v2.6.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
+399
View File
@@ -0,0 +1,399 @@
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA=
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/containerd/v2 v2.1.4 h1:/hXWjiSFd6ftrBOBGfAZ6T30LJcx1dBjdKEeI8xucKQ=
github.com/containerd/containerd/v2 v2.1.4/go.mod h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E=
github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs=
github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w=
github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM=
github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/huandu/go-assert v1.1.6 h1:oaAfYxq9KNDi9qswn/6aE0EydfxSa+tWZC1KabNitYs=
github.com/huandu/go-assert v1.1.6/go.mod h1:JuIfbmYG9ykwvuxoJ3V8TB5QP+3+ajIA54Y44TmkMxs=
github.com/huandu/go-sqlbuilder v1.36.1 h1:4S17aR2BPW8L2PeotAD4iVhSMjwzk6CO8ABmp3tMEhY=
github.com/huandu/go-sqlbuilder v1.36.1/go.mod h1:59Zjq93ndlKI6O5kHmkXQpDgriBAPMKuhByEOy6xYK8=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc/v3 v3.0.0 h1:nZUx/zFg5uc2rhlu1L1DidGr5Sj02JbXvGSpnY4LMrc=
github.com/lestrrat-go/httprc/v3 v3.0.0/go.mod h1:k2U1QIiyVqAKtkffbg+cUmsyiPGQsb9aAfNQiNFuQ9Q=
github.com/lestrrat-go/jwx/v3 v3.0.10 h1:XuoCBhZBncRIjMQ32HdEc76rH0xK/Qv2wq5TBouYJDw=
github.com/lestrrat-go/jwx/v3 v3.0.10/go.mod h1:kNMedLgTpHvPJkK5EMVa1JFz+UVyY2dMmZKu3qjl/Pk=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/microsoft/go-mssqldb v1.9.3 h1:hy4p+LDC8LIGvI3JATnLVmBOLMJbmn5X400mr5j0lPs=
github.com/microsoft/go-mssqldb v1.9.3/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA=
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw=
github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc=
github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw=
github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/vektah/gqlparser/v2 v2.5.30 h1:EqLwGAFLIzt1wpx1IPpY67DwUujF1OfzgEyDsLrN6kE=
github.com/vektah/gqlparser/v2 v2.5.30/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg=
github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM=
modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ=
modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+24 -19
View File
@@ -3,6 +3,7 @@ module github.com/open-policy-agent/opa
go 1.24.6
require (
github.com/agnivade/levenshtein v1.2.1
github.com/bytecodealliance/wasmtime-go/v3 v3.0.2
github.com/cespare/xxhash/v2 v2.3.0
github.com/containerd/containerd/v2 v2.1.4
@@ -16,6 +17,8 @@ require (
github.com/gobwas/glob v0.2.3
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/huandu/go-sqlbuilder v1.36.1
github.com/lestrrat-go/jwx/v3 v3.0.10
github.com/olekukonko/tablewriter v0.0.5
github.com/opencontainers/go-digest v1.0.0
@@ -23,9 +26,9 @@ require (
github.com/peterh/liner v1.2.2
github.com/prometheus/client_golang v1.23.0
github.com/prometheus/client_model v0.6.2
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9
github.com/sergi/go-diff v1.4.0
github.com/sirupsen/logrus v1.9.3
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.7
github.com/spf13/viper v1.20.1
@@ -41,10 +44,10 @@ require (
go.opentelemetry.io/otel/sdk v1.37.0
go.opentelemetry.io/otel/trace v1.37.0
go.uber.org/automaxprocs v1.6.0
golang.org/x/net v0.42.0
golang.org/x/net v0.43.0
golang.org/x/time v0.12.0
google.golang.org/grpc v1.74.2
google.golang.org/protobuf v1.36.6
google.golang.org/grpc v1.75.0
google.golang.org/protobuf v1.36.8
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
gopkg.in/yaml.v3 v3.0.1
oras.land/oras-go/v2 v2.6.0
@@ -52,23 +55,24 @@ require (
)
require (
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v1.0.0-rc.1 // indirect
github.com/containerd/typeurl/v2 v2.2.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/flatbuffers v25.2.10+incompatible // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
@@ -84,9 +88,10 @@ require (
github.com/moby/locker v1.0.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
@@ -94,23 +99,23 @@ require (
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.25.0 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.36.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
)
// retract directive comment below will be displayed as a warning on pkg.go.dev for the old package name. Please retain
+50 -39
View File
@@ -24,12 +24,14 @@ github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsW
github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=
github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=
github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs=
@@ -63,8 +65,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -77,6 +79,14 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+uBhcekkmy4IkffJww=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/huandu/go-assert v1.1.6 h1:oaAfYxq9KNDi9qswn/6aE0EydfxSa+tWZC1KabNitYs=
github.com/huandu/go-assert v1.1.6/go.mod h1:JuIfbmYG9ykwvuxoJ3V8TB5QP+3+ajIA54Y44TmkMxs=
github.com/huandu/go-sqlbuilder v1.36.1 h1:4S17aR2BPW8L2PeotAD4iVhSMjwzk6CO8ABmp3tMEhY=
github.com/huandu/go-sqlbuilder v1.36.1/go.mod h1:59Zjq93ndlKI6O5kHmkXQpDgriBAPMKuhByEOy6xYK8=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -126,8 +136,9 @@ github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8
github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw=
github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
@@ -136,12 +147,13 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=
github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
@@ -153,8 +165,8 @@ github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
@@ -169,13 +181,12 @@ github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc=
@@ -215,14 +226,12 @@ go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mx
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
@@ -234,16 +243,16 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -255,8 +264,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -281,8 +290,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -297,8 +306,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -309,20 +318,22 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:AtEkQdl5b6zsybXcbz00j1LwNodDuH6hVifIaNqk7NQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+266
View File
@@ -0,0 +1,266 @@
// Copyright 2025 The OPA Authors
// SPDX-License-Identifier: Apache-2.0
package compile
import (
"cmp"
"fmt"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/rego"
)
const code = "pe_fragment_error" // TODO(sr): this is preliminary
type Results struct {
errs []*ast.Error
}
func (r *Results) ASTErrors() []*ast.Error {
if r == nil {
return nil
}
return r.errs
}
type checker struct {
constraints Constraints
shortUnknowns Set[string]
res *Results
}
func (c *checker) Results() *Results {
return c.res
}
// Check performs a set of checks on the given partial queries and support modules.
// The constraints are used to determine which features are allowed in the partial queries.
// The shorts are used to determine which short names are allowed, e.g. `input.foo` is
// allowed if it's mapped to some table column.
func Check(pq *rego.PartialQueries, constraints Constraints, shorts Set[string]) *Results {
check := checker{
constraints: constraints,
shortUnknowns: shorts,
res: &Results{},
}
for i := range pq.Queries {
check.Query(pq.Queries[i], pq.Support)
}
// NOTE(sr): So far, we've gotten better error locations from the refs into
// support modules. The support modules themselves are surprisingly useless
// for that.
// for i := range pq.Support {
// checkSupport(pq.Support[i], &res)
// }
return check.Results()
}
func (c *checker) Query(q ast.Body, sup []*ast.Module) {
for j := range q {
for i := range queryChecks {
if err := queryChecks[i](c, q[j], sup); err != nil {
c.res.errs = append(c.res.errs, err)
}
}
}
}
var queryChecks = [...]func(*checker, *ast.Expr, []*ast.Module) *ast.Error{
checkCall,
checkBuiltins,
}
var partialPrefix = ast.MustParseRef("data.partial")
func checkCall(c *checker, e *ast.Expr, sup []*ast.Module) *ast.Error {
switch {
case e.Negated:
if !e.IsCall() { // IsCall gives us comparisons etc, and rules out naked data refs
return err(e.Loc(), "\"not\" not permitted")
}
err0 := c.constraints.AssertFeature("not")
if err0 == nil {
break // OK
}
return err(e.Loc(), "\"not\" not permitted: %s", err0.Error())
case e.IsCall(): // OK
case e.IsEvery():
return err(e.Loc(), "\"every\" not permitted")
case len(e.With) > 0:
return err(e.Loc(), "\"with\" not permitted")
default:
if t, ok := e.Terms.(*ast.Term); ok {
if ref, ok := t.Value.(ast.Ref); ok && ref.HasPrefix(partialPrefix) {
loc := ref[len(ref)-1].Loc()
return err(loc, "%s", findDetails(ref, sup))
}
if ref, ok := t.Value.(ast.Ref); ok && ref.HasPrefix(ast.DefaultRootRef) {
// TODO(sr): point to rule with else -- but we don't have the full rego yet
return withDetails(err(e.Loc(), "invalid data reference \"%v\"", e),
fmt.Sprintf("has rule \"%v\" an `else`?", ref),
)
}
}
return withDetails(err(e.Loc(), "invalid statement \"%v\"", e),
fmt.Sprintf("try `%v != false`", e),
)
}
return nil
}
// some builtins need their names replaced for nicer errors
var replacements = map[string]string{
"internal.member_2": "in",
}
func checkBuiltins(c *checker, e *ast.Expr, _ []*ast.Module) *ast.Error {
if len(e.With) > 0 { // Ignore expression, we'll already have recorded errors through checkCalls.
return nil
}
op := e.OperatorTerm()
if op == nil {
return nil
}
loc := cmp.Or(op.Loc(), e.Loc())
ref := op.Value.(ast.Ref)
op0 := ref.String()
unknownMustBeFirst := false
twoRefsOK := false
switch {
case op0 == ast.Equality.Name ||
op0 == ast.NotEqual.Name ||
op0 == ast.LessThan.Name ||
op0 == ast.LessThanEq.Name ||
op0 == ast.GreaterThan.Name ||
op0 == ast.GreaterThanEq.Name:
twoRefsOK = true
case op0 == ast.StartsWith.Name ||
op0 == ast.EndsWith.Name ||
op0 == ast.Contains.Name ||
op0 == ast.Member.Name:
unknownMustBeFirst = true
// Below there are only error cases
case op0 == ast.MemberWithKey.Name:
return err(loc, "invalid use of \"... in ...\"")
case ref.HasPrefix(ast.DefaultRootRef):
// TODO(sr): point to function with else -- but we don't have the full rego yet
return withDetails(err(e.Loc(), "invalid data reference \"%v\"", e),
fmt.Sprintf("has function \"%v(...)\" an `else`?", ref),
)
default:
return err(loc, "invalid builtin `%v`", op)
}
// Also check that our target+variant allows this builtin
if err0 := c.constraints.AssertBuiltin(op0); err0 != nil {
return err(loc, "invalid builtin `%v`: %s",
cmp.Or(replacements[op0], op0), err0.Error())
}
// all our allowed builtins have two operands
for i := range 2 {
if err := checkOperand(c, op, e.Operand(i)); err != nil {
return err
}
}
// check that field-ref comparisons are supported by the targets:
unknownRefs := 0
for i := range 2 {
if _, ok := e.Operand(i).Value.(ast.Ref); ok {
unknownRefs++
}
}
if unknownRefs == 2 {
if err0 := c.constraints.AssertFeature("field-ref"); err0 != nil {
return err(loc, "reference to field: %s", err0.Error())
}
}
switch {
case unknownMustBeFirst:
if _, ok := e.Operand(0).Value.(ast.Ref); !ok {
return err(loc, "rhs of %v must be known", op)
}
default: // lhs or rhs needs to be ground scalar, or, if twoRefsOK is true, unknown input refs
// TODO(sr): collections might work, too, let's fix this later
found := false
for i := range 2 {
if ast.IsScalar(e.Operand(i).Value) {
found = true
}
}
if !found && !(twoRefsOK && unknownRefs == 2) { // nolint:staticcheck
return err(loc, "both rhs and lhs non-scalar/non-ground")
}
}
return nil
}
func checkOperand(c *checker, op, t *ast.Term) *ast.Error {
if t == nil {
return err(op.Loc(), "%v: missing operand", op)
}
loc := op.Loc()
switch v := t.Value.(type) {
case ast.Call:
if loc == nil {
loc = v[0].Loc()
}
return err(loc, "%v: nested call operand: %v", op, v)
case ast.Ref:
if v.HasPrefix(ast.InputRootRef) {
if len(v) == 3 {
return nil
}
if len(v) == 2 && c.shortUnknowns.Contains(string(v[1].Value.(ast.String))) {
return nil
}
}
if loc == nil {
loc = t.Loc()
}
return err(loc, "%v: invalid ref operand: %v", op, v)
}
return nil
}
func err(loc *ast.Location, f string, vs ...any) *ast.Error {
return ast.NewError(code, loc, f, vs...)
}
type Details struct {
Extra string `json:"details"`
}
func (d *Details) Lines() []string {
return []string{d.Extra}
}
func withDetails(err *ast.Error, dets string) *ast.Error {
err.Details = &Details{Extra: dets}
return err
}
func findDetails(partialRef ast.Ref, sup []*ast.Module) string {
for i := range sup {
count := 0
for j := range sup[i].Rules {
if r := sup[i].Rules[j]; r.Ref().Equal(partialRef) {
count++
switch {
case r.Default:
return fmt.Sprintf("use of default rule in %v", ast.DefaultRootRef.Concat(r.Ref()[2:]))
case count > 1:
return fmt.Sprintf("use of multi-value rule in %v", ast.DefaultRootRef.Concat(r.Ref()[2:]))
}
}
}
}
return ""
}
+198
View File
@@ -0,0 +1,198 @@
// Copyright 2025 The OPA Authors
// SPDX-License-Identifier: Apache-2.0
package compile
import (
"encoding/json"
"fmt"
"slices"
"strings"
"github.com/open-policy-agent/opa/internal/levenshtein"
"github.com/open-policy-agent/opa/internal/ucast"
"github.com/open-policy-agent/opa/v1/ast"
)
const (
invalidUnknownCode = "invalid_unknown"
invalidMaskRuleCode = "invalid_mask_rule"
)
type UCASTNode struct {
internal *ucast.UCASTNode
}
func (u *UCASTNode) Map() map[string]any {
if u.internal == nil { // unconditional YES
return map[string]any{}
}
// TODO(sr): find a better way
ret := map[string]any{}
bs, err := json.Marshal(u.internal)
if err != nil {
panic(err)
}
if err := json.Unmarshal(bs, &ret); err != nil {
panic(err)
}
return ret
}
func QueriesToUCAST(queries []ast.Body, mappings map[string]any) *UCASTNode {
return &UCASTNode{internal: BodiesToUCAST(queries, &Opts{Translations: mappings})}
}
func QueriesToSQL(queries []ast.Body, mappings map[string]any, dialect string) (string, error) {
sql := ""
ucast := BodiesToUCAST(queries, &Opts{Translations: mappings})
if ucast != nil { // ucast == nil means unconditional YES, for which we'll keep `sql = ""`
sql0, err := ucast.AsSQL(dialect)
if err != nil {
return "", err
}
sql = sql0
}
return sql, nil
}
func ExtractUnknownsFromAnnotations(comp *ast.Compiler, ref ast.Ref) ([]*ast.Term, []*ast.Error) {
// find ast.Rule for ref
rules := comp.GetRulesExact(ref)
if len(rules) == 0 {
return nil, nil
}
rule := rules[0] // rule scope doesn't make sense here, so it doesn't matter which rule we use
return unknownsFromAnnotationsSet(comp.GetAnnotationSet(), rule)
}
func unknownsFromAnnotationsSet(as *ast.AnnotationSet, rule *ast.Rule) ([]*ast.Term, []*ast.Error) {
if as == nil {
return nil, nil
}
var unknowns []*ast.Term
var errs []*ast.Error
for _, ar := range as.Chain(rule) {
ann := ar.Annotations
if ann == nil {
continue
}
unk, ok := ann.Custom["unknowns"]
if !ok {
continue
}
unkArray, ok := unk.([]any)
if !ok {
continue
}
for _, u := range unkArray {
s, ok := u.(string)
if !ok {
continue
}
ref, err := ast.ParseRef(s)
if err != nil {
errs = append(errs, ast.NewError(invalidUnknownCode, ann.Loc(), "unknowns must be valid refs: %s", s))
} else if ref.HasPrefix(ast.DefaultRootRef) || ref.HasPrefix(ast.InputRootRef) {
unknowns = append(unknowns, ast.NewTerm(ref))
} else {
errs = append(errs, ast.NewError(invalidUnknownCode, ann.Loc(), "unknowns must be prefixed with `input` or `data`: %v", ref))
}
}
}
return unknowns, errs
}
func ExtractMaskRuleRefFromAnnotations(comp *ast.Compiler, ref ast.Ref) (ast.Ref, *ast.Error) {
// find ast.Rule for ref
rules := comp.GetRulesExact(ref)
if len(rules) == 0 {
return nil, nil
}
rule := rules[0] // rule scope doesn't make sense here, so it doesn't matter which rule we use
return maskRuleFromAnnotationsSet(comp.GetAnnotationSet(), comp, rule)
}
func maskRuleFromAnnotationsSet(as *ast.AnnotationSet, comp *ast.Compiler, rule *ast.Rule) (ast.Ref, *ast.Error) {
if as == nil {
return nil, nil
}
for _, ar := range as.Chain(rule) {
ann := ar.Annotations
if ann == nil {
continue
}
// If the mask_rule key is present, validate and parse it.
if maskRule, ok := ann.Custom["mask_rule"]; ok {
if s, ok := maskRule.(string); ok {
maskPath := s
if !strings.HasPrefix(s, "data.") {
// If the mask_rule is not a data ref try adding package prefix.
maskPath = rule.Module.Package.Path.String() + "." + s
}
maskRuleRef, err := ast.ParseRef(maskPath)
if err != nil {
hint := FuzzyRuleNameMatchHint(comp, s)
return nil, ast.NewError(invalidMaskRuleCode, ann.Loc(), "mask_rule was not a valid ref: %s", hint)
}
return maskRuleRef, nil
}
return nil, ast.NewError(invalidMaskRuleCode, ann.Loc(), "mask_rule must be a valid ref string: %v", maskRule)
}
}
return nil, nil // No mask rule found.
}
func ShortsFromMappings(mappings map[string]any) Set[string] {
shorts := NewSet[string]()
for _, mapping := range mappings {
m, ok := mapping.(map[string]any)
if !ok {
continue
}
for n, nmap := range m {
m, ok := nmap.(map[string]any)
if !ok {
continue
}
if _, ok := m["$table"]; ok {
shorts = shorts.Add(n)
}
}
}
return shorts
}
// Returns a list of similar rule names that might match the input string.
// Warning(philip): This is expensive, as the cost grows linearly with the
// number of rules present on the compiler. It should be used only for
// error messages.
func FuzzyRuleNameMatchHint(comp *ast.Compiler, input string) string {
rules := comp.GetRules(ast.Ref{ast.DefaultRootDocument})
ruleNames := make([]string, 0, len(rules))
for _, rule := range rules {
if rule.Default {
continue
}
ruleNames = append(ruleNames, rule.Module.Package.Path.String()+"."+rule.Head.Name.String())
}
closest := levenshtein.ClosestStrings(65536, input, slices.Values(ruleNames))
proposals := slices.Compact(closest)
var msg string
switch len(proposals) {
case 0:
return ""
case 1:
msg = fmt.Sprintf("%s undefined, did you mean %s?", input, proposals[0])
default:
msg = fmt.Sprintf("%s undefined, did you mean one of %v?", input, proposals)
}
return msg
}
+226
View File
@@ -0,0 +1,226 @@
// Copyright 2025 The OPA Authors
// SPDX-License-Identifier: Apache-2.0
package compile
import (
"errors"
"fmt"
"maps"
"strings"
)
type Set[T comparable] map[T]struct{}
func NewSet[T comparable](strs ...T) Set[T] {
return make(Set[T]).Add(strs...)
}
func (s Set[T]) Clone() Set[T] {
return maps.Clone(s)
}
func (s Set[T]) Add(strs ...T) Set[T] {
for i := range strs {
s[strs[i]] = struct{}{}
}
return s
}
// Contains checks if a string exists in the set
func (s Set[T]) Contains(str T) bool {
_, exists := s[str]
return exists
}
// Intersection returns a new set containing elements present in both sets
func (s Set[T]) Intersection(other Set[T]) Set[T] {
result := NewSet[T]()
// Iterate through the smaller set for better performance
if len(s) > len(other) {
s, other = other, s
}
for elem := range s {
if other.Contains(elem) {
result.Add(elem)
}
}
return result
}
// Constraint lets us limit the Set that are allowed in a translation.
// There are hardcoded sets of supported Set. The constraints become
// effective during the post-PE analysis (compile.Checks()).
type Constraint struct {
Target string
Variant string
Builtins Set[string]
Features Set[string]
}
type Constraints interface {
Builtin(string) bool
AssertBuiltin(string) error
Supports(string) bool
AssertFeature(string) error
}
// NewConstraints returns a new Constraint object based on the type
// requested, ucast or sql.
func NewConstraints(typ, variant string) (*Constraint, error) {
c := Constraint{Target: strings.ToUpper(typ), Variant: variant, Features: NewSet[string]()}
switch typ {
case "sql":
switch v := strings.ToLower(variant); v {
case "sqlite":
c.Builtins = sqlSQLiteBuiltins
case "mysql", "postgresql", "sqlserver", "sqlite-internal":
c.Builtins = sqlBuiltins
default:
return nil, fmt.Errorf("unsupported variant for %s: %s", typ, variant)
}
c.Features.Add("not", "field-ref")
case "ucast":
switch v := strings.ToLower(variant); v {
case "all":
c.Variant = v
c.Features.Add("not", "field-ref")
c.Builtins = allBuiltins
case "prisma":
c.Variant = v
c.Features.Add("not")
c.Builtins = allBuiltins
case "linq":
c.Variant = "LINQ" // normalize spelling
c.Builtins = ucastLINQBuiltins
default:
c.Variant = ""
c.Builtins = ucastBuiltins
}
default:
return nil, fmt.Errorf("unknown target/dialect combination: %s/%s", typ, variant)
}
return &c, nil
}
// Builtin returns true if the builtin is supported by the constraint.
func (c *Constraint) Builtin(x string) bool {
return c.Builtins.Contains(x)
}
func (c *Constraint) AssertBuiltin(x string) error {
if !c.Builtin(x) {
return fmt.Errorf("unsupported for %s", c)
}
return nil
}
// Supports allows us to encode more fluent constraints, like support for "not".
// It returns an error if the feature is not supported by the constraint.
func (c *Constraint) Supports(x string) bool {
return c.Features.Contains(x)
}
func (c *Constraint) AssertFeature(x string) error {
if !c.Supports(x) {
return fmt.Errorf("unsupported feature %q for %s", x, c)
}
return nil
}
var _ fmt.Stringer = (*Constraint)(nil)
func (c *Constraint) String() string {
if c.Variant == "" {
return c.Target
}
return fmt.Sprintf("%s (%s)", c.Target, c.Variant)
}
type ConstraintSet struct {
Constraints []*Constraint
}
func NewConstraintSet(cs ...*Constraint) *ConstraintSet {
return &ConstraintSet{Constraints: cs}
}
// Builtin returns true if all the constraints in the set support the builtin.
func (cs *ConstraintSet) Builtin(x string) bool {
for i := range cs.Constraints {
if !cs.Constraints[i].Builtin(x) {
return false
}
}
return true
}
func (cs *ConstraintSet) AssertBuiltin(x string) error {
var err error
for i := range cs.Constraints {
if e := cs.Constraints[i].AssertBuiltin(x); e != nil {
err = errors.Join(err, e)
}
}
return err
}
// Supports returns true if the feature is supported by all constraints.
func (cs *ConstraintSet) Supports(x string) bool {
for i := range cs.Constraints {
if !cs.Constraints[i].Supports(x) {
return false
}
}
return true
}
// AssertFeature returns an error if the feature is not supported by any
// of the constraints. The error contains the offending target/dialect pairs.
func (cs *ConstraintSet) AssertFeature(x string) error {
var err error
for i := range cs.Constraints {
if e := cs.Constraints[i].AssertFeature(x); e != nil {
err = errors.Join(err, e)
}
}
return err
}
var _ fmt.Stringer = (*ConstraintSet)(nil)
func (cs *ConstraintSet) String() string {
result := make([]string, 0, len(cs.Constraints)+1)
for i := range cs.Constraints {
result = append(result, cs.Constraints[i].String())
}
return "multi-constraint: " + strings.Join(result, ", ")
}
var (
sqlBuiltins = allBuiltins
// sqlite doesn't support startswith/endswith/contains
sqlSQLiteBuiltins = ucastBuiltins.Clone().Add("internal.member_2")
ucastBuiltins = NewSet(
"eq",
"neq",
"lt",
"lte",
"gt",
"gte",
)
allBuiltins = ucastBuiltins.Clone().Add(
"internal.member_2",
// "nin", // TODO: deal with NOT IN
"startswith",
"endswith",
"contains",
)
ucastLINQBuiltins = ucastBuiltins.Clone().Add(
"internal.member_2",
// "nin", // TODO: deal with NOT IN
)
)
+232
View File
@@ -0,0 +1,232 @@
// Copyright 2025 The OPA Authors
// SPDX-License-Identifier: Apache-2.0
package compile
import (
"cmp"
"fmt"
"strings"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/internal/ucast"
)
type Opts struct {
Translations map[string]any
}
func BodiesToUCAST(bs []ast.Body, opts *Opts) *ucast.UCASTNode {
if len(bs) == 0 {
return nil
}
// If there's only one body, convert it directly
if len(bs) == 1 {
return bodyToUCAST(bs[0], opts)
}
// Multiple expressions are combined with AND
nodes := make([]ucast.UCASTNode, len(bs))
for i := range bs {
u := bodyToUCAST(bs[i], opts)
if u == nil {
return nil
}
nodes[i] = *u
}
return &ucast.UCASTNode{
Type: "compound",
Op: "or",
Value: nodes,
}
}
func bodyToUCAST(body ast.Body, opts *Opts) *ucast.UCASTNode {
if len(body) == 0 {
return nil
}
// If there's only one expression, convert it directly
if len(body) == 1 {
return exprToUCAST(body[0], opts)
}
// Multiple expressions are combined with AND
nodes := make([]ucast.UCASTNode, len(body))
for i, expr := range body {
u := exprToUCAST(expr, opts)
if u == nil {
return nil
}
nodes[i] = *u
}
return &ucast.UCASTNode{
Type: "compound",
Op: "and",
Value: nodes,
}
}
func exprToUCAST(expr *ast.Expr, opts *Opts) *ucast.UCASTNode {
if expr == nil || !expr.IsCall() {
return nil
}
ref, flip := refFromCall(expr)
return callToNode(expr, ref, flip, opts)
}
// refToField drops the first part of ast.Ref, and joins the rest with "."
func refToField(r ast.Ref, opts *Opts) (string, error) {
parts := make([]string, len(r)-1)
for i := range len(r) - 1 {
switch t := r[i+1].Value.(type) {
case ast.Var:
parts[i] = string(t)
case ast.String:
parts[i] = string(t)
default:
return "", fmt.Errorf("unexpected type in ref %v: %T (%[2]v)", r, t)
}
}
return translateField(strings.Join(parts, "."), opts.Translations), nil
}
func toFieldNode(op string, r ast.Ref, v ast.Value, opts *Opts, refOK bool) *ucast.UCASTNode {
var value any
switch v := v.(type) {
case ast.Ref:
if refOK {
f, err := refToField(v, opts)
if err != nil {
return nil
}
value = ucast.FieldRef{
Field: f,
}
}
default:
var err error
value, err = ast.ValueToInterface(v, nil)
if err != nil {
return nil
}
}
f, err := refToField(r, opts)
if err != nil {
return nil
}
if value == nil {
value = ucast.Null{}
}
return &ucast.UCASTNode{
Type: "field",
Op: op,
Field: f,
Value: value,
}
}
var reversed = map[string]string{
"lt": "gte",
"lte": "gt",
"gt": "lte",
"gte": "lt",
}
// callToNode converts a call expression to a UCASTNode, and flips the arguments
// and the comparison operator if needed.
func callToNode(e *ast.Expr, f ast.Ref, flip bool, opts *Opts) *ucast.UCASTNode {
ref := e.OperatorTerm().Value.(ast.Ref)
op := ref.String()
refOK := false
switch op {
case ast.NotEqual.Name:
refOK = true
op = "ne"
case ast.Equality.Name,
ast.LessThan.Name,
ast.LessThanEq.Name,
ast.GreaterThan.Name,
ast.GreaterThanEq.Name:
refOK = true
case ast.StartsWith.Name:
case ast.EndsWith.Name:
case ast.Contains.Name:
case ast.Member.Name:
op = "in"
default:
return nil
}
i := 1
if flip {
i = 0
op = cmp.Or(reversed[op], op) // optionally replace operator
}
fn := toFieldNode(op, f, e.Operand(i).Value, opts, refOK)
if !e.Negated {
return fn
}
value := make([]ucast.UCASTNode, 1)
value[0] = *fn
return &ucast.UCASTNode{
Type: "compound",
Op: "not",
Value: value,
}
}
func refFromCall(e *ast.Expr) (ast.Ref, bool) {
leftRef, ok := e.Operand(0).Value.(ast.Ref)
if ok { // lhs is unknown
return leftRef, false
}
return e.Operand(1).Value.(ast.Ref), true // rhs is unknown
}
func translateField(field string, translations map[string]any) string {
var outTable, outColumn string
if translations == nil {
return field
}
before, after, found := strings.Cut(field, ".")
outTable = before
outColumn = after
if tableMapping := translations[before]; tableMapping != nil {
if tableMapping, ok := tableMapping.(map[string]any); ok {
// See if there's a $table ref for the short unknown, and remap.
if tableName, ok := tableMapping["$table"]; ok {
outTable = tableName.(string)
// swap, make believe we picked up '<outTable>.name', not 'name'
before, after, found = outTable, before, true
outColumn = after
}
}
}
// Is there a translation available for the table name?
if tableMapping, ok := translations[before]; ok {
if tableMapping, ok := tableMapping.(map[string]any); ok {
// See if there's a mapping for the table name, and remap.
if tableName, ok := tableMapping["$self"]; ok {
outTable = tableName.(string) // XXX: be more cautious about the type
}
// If we have a column name, try remapping it.
if found {
if columnName, ok := tableMapping[after]; ok {
outColumn = columnName.(string)
}
}
}
}
if found {
return outTable + "." + outColumn
}
return outTable
}
+30
View File
@@ -0,0 +1,30 @@
// Copyright 2025 The OPA Authors
// SPDX-License-Identifier: Apache-2.0
package levenshtein
import (
"iter"
"slices"
"github.com/agnivade/levenshtein"
)
func ClosestStrings(minDistance int, a string, candidates iter.Seq[string]) []string {
closestStrings := []string{}
for c := range candidates {
levDist := levenshtein.ComputeDistance(a, c)
switch {
case levDist < minDistance:
closestStrings = []string{c}
minDistance = levDist
case levDist == minDistance:
closestStrings = append(closestStrings, c)
minDistance = levDist
default:
continue
}
}
slices.Sort(closestStrings)
return closestStrings
}
+246
View File
@@ -0,0 +1,246 @@
// Copyright 2025 The OPA Authors
// SPDX-License-Identifier: Apache-2.0
package ucast
import (
"errors"
"fmt"
"slices"
"strings"
"github.com/huandu/go-sqlbuilder"
)
// The "union" structure for incoming UCAST trees.
type UCASTNode struct {
Type string `json:"type"`
Op string `json:"operator"`
Field string `json:"field,omitempty"`
Value any `json:"value,omitempty"`
}
// FieldRef can be used in UCASTNode.Value to reference another field, i.e. database column.
type FieldRef struct {
Field string `json:"field"`
}
// Null represents a NULL value in a SQL query. We need our own type
// to control both the JSON marshalling and the SQL generation.
type Null struct{}
func (Null) MarshalJSON() ([]byte, error) {
return []byte("null"), nil
}
var (
compoundOps = []string{"and", "or", "not"}
documentOps = []string{"exists"}
fieldOps = []string{"eq", "ne", "gt", "lt", "ge", "le", "gte", "lte", "in"} // "nin"
)
func dialectToFlavor(dialect string) sqlbuilder.Flavor {
switch dialect {
case "mysql":
return sqlbuilder.MySQL
case "sqlite", "sqlite-internal":
return sqlbuilder.SQLite
case "postgres", "postgresql":
return sqlbuilder.PostgreSQL
case "sqlserver":
return sqlbuilder.SQLServer
default:
return sqlbuilder.SQLite
}
}
func interpolateByDialect(dialect string, s string, args []any) (string, error) {
return dialectToFlavor(dialect).Interpolate(s, args)
}
func (u *UCASTNode) AsSQL(dialect string) (string, error) {
// Build up the SQL expression using the UCASTNode tree.
cond := sqlbuilder.NewCond()
where := sqlbuilder.NewWhereClause()
conditionStr, err := u.asSQL(cond, dialect)
if err != nil {
return "", err
}
where.AddWhereExpr(cond.Args, conditionStr)
s, args := where.BuildWithFlavor(dialectToFlavor(dialect))
// Interpolate in the arguments into the SQL string.
return interpolateByDialect(dialect, s, args)
}
// Uses our SQL generator library to build up a larger SQL expression.
func (u *UCASTNode) asSQL(cond *sqlbuilder.Cond, dialect string) (string, error) {
cond.Args.Flavor = dialectToFlavor(dialect)
uType := u.Type
operator := u.Op
field := u.Field
value := u.Value
switch {
case slices.Contains(fieldOps, operator) || uType == "field":
switch value {
case nil:
return "", nil
case Null{}:
switch operator {
case "eq":
return cond.IsNull(field), nil
case "ne":
return cond.IsNotNull(field), nil
default:
return "", errors.New("null value can only be used with 'eq' or 'ne' operators")
}
default:
if fr, ok := value.(FieldRef); ok {
value = sqlbuilder.Raw(fr.Field)
}
}
switch operator {
case "eq":
return cond.Equal(field, value), nil
case "ne":
return cond.NotEqual(field, value), nil
case "gt":
return cond.GreaterThan(field, value), nil
case "lt":
return cond.LessThan(field, value), nil
case "ge", "gte":
return cond.GreaterEqualThan(field, value), nil
case "le", "lte":
return cond.LessEqualThan(field, value), nil
case "in":
if arr, ok := (value).([]any); ok {
return cond.In(field, arr...), nil
}
return "", errors.New("field operator 'in' requires collection argument")
case "startswith":
if dialect == "sqlite-internal" {
return cond.Var(sqlbuilder.Build("internal_startswith($?, $?)", sqlbuilder.Raw(field), value)), nil
}
pattern, err := prefix(value)
if err != nil {
return "", err
}
return cond.Like(field, pattern), nil
case "endswith":
if dialect == "sqlite-internal" {
return cond.Var(sqlbuilder.Build("internal_endswith($?, $?)", sqlbuilder.Raw(field), value)), nil
}
pattern, err := suffix(value)
if err != nil {
return "", err
}
return cond.Like(field, pattern), nil
case "contains":
if dialect == "sqlite-internal" {
return cond.Var(sqlbuilder.Build("internal_contains($?, $?)", sqlbuilder.Raw(field), value)), nil
}
pattern, err := infix(value)
if err != nil {
return "", err
}
return cond.Like(field, pattern), nil
default:
return "", fmt.Errorf("unrecognized operator: %s", operator)
}
case slices.Contains(documentOps, operator) || uType == "document":
// Note: We should add unary operations under this case, like NOT.
if value == nil {
return "", errors.New("document expression 'exists' requires a value")
}
if operator == "exists" {
return cond.Exists(value), nil
}
return "", fmt.Errorf("unrecognized operator: %s", operator)
case slices.Contains(compoundOps, operator) || uType == "compound":
switch operator {
case "and":
if value == nil {
return "", errors.New("compound expression 'and' requires a value")
}
if values, ok := (value).([]UCASTNode); ok {
conds := make([]string, 0, len(values))
for _, c := range values {
condition, err := c.asSQL(cond, dialect)
if err != nil {
return "", err
}
conds = append(conds, condition)
}
return cond.And(conds...), nil
}
return "", errors.New("value must be an array")
case "or":
if value == nil {
return "", errors.New("compound expression 'or' requires a value")
}
if values, ok := (value).([]UCASTNode); ok {
conds := make([]string, 0, len(values))
for _, c := range values {
condition, err := c.asSQL(cond, dialect)
if err != nil {
return "", err
}
conds = append(conds, condition)
}
return cond.Or(conds...), nil
}
return "", errors.New("value must be an array")
case "not":
if value == nil {
return "", errors.New("compound expression 'not' requires exactly one value")
}
node, ok := (value).([]UCASTNode)
if ok {
if len(node) != 1 {
return "", errors.New("compound expression 'not' requires exactly one value")
}
condition, err := node[0].asSQL(cond, dialect)
if err != nil {
return "", err
}
return cond.Not(condition), nil
}
return "", fmt.Errorf("value must be a ucast node, got %T: %[1]v", value)
}
return "", fmt.Errorf("unrecognized operator: %s", operator)
default:
return "", fmt.Errorf("unrecognized operator: %s", operator)
}
}
func prefix(p any) (string, error) {
p0, ok := p.(string)
if !ok {
return "", fmt.Errorf("'startswith' pattern requires string argument, got %v %[1]T", p)
}
return escaped(p0) + "%", nil
}
func suffix(p any) (string, error) {
p0, ok := p.(string)
if !ok {
return "", fmt.Errorf("'endswith' pattern requires string argument, got %v %[1]T", p)
}
return "%" + escaped(p0), nil
}
func infix(p any) (string, error) {
p0, ok := p.(string)
if !ok {
return "", fmt.Errorf("'contains' pattern requires string argument, got %v %[1]T", p)
}
return "%" + escaped(p0) + "%", nil
}
func escaped(p0 string) string {
p0 = strings.ReplaceAll(p0, `\`, `\\`)
p0 = strings.ReplaceAll(p0, "_", `\_`)
p0 = strings.ReplaceAll(p0, "%", `\%`)
return p0
}
+110
View File
@@ -0,0 +1,110 @@
// Copyright 2025 The OPA Authors
// SPDX-License-Identifier: Apache-2.0
package ucast
import (
"testing"
)
// Note: Currently only implements tests for the Postgres dialect.
func TestUCASTNodeAsSQL(t *testing.T) {
t.Parallel()
tests := []struct {
Note string
Source UCASTNode
Dialect string
Result string
Error string
}{
{
Note: "Nil argument",
Source: UCASTNode{Type: "field", Op: "eq", Field: "name", Value: nil},
Dialect: "postgres",
Result: "",
Error: "field expression requires a value",
},
{
Note: "special handling for NULL",
Source: UCASTNode{Type: "field", Op: "eq", Field: "name", Value: Null{}},
Dialect: "postgres",
Result: "WHERE name IS NULL",
},
{
Note: "special handling for NOT NULL",
Source: UCASTNode{Type: "field", Op: "ne", Field: "name", Value: Null{}},
Dialect: "postgres",
Result: "WHERE name IS NOT NULL",
},
{
Note: "Basic compound expression",
Source: UCASTNode{Type: "compound", Op: "and", Value: []UCASTNode{
{Type: "field", Op: "eq", Field: "name", Value: "bob"},
{Type: "field", Op: "gt", Field: "salary", Value: 50000},
}},
Dialect: "postgres",
Result: "WHERE (name = E'bob' AND salary > 50000)",
},
{
Note: "startswith + pattern",
Source: UCASTNode{Type: "field", Field: "name", Op: "startswith", Value: `f\oo_b%ar`},
Dialect: "postgres",
Result: `WHERE name LIKE E'f\\\\oo\\_b\\%ar%'`,
},
{
Note: "endswith + pattern",
Source: UCASTNode{Type: "field", Field: "name", Op: "endswith", Value: `f\oo_b%ar`},
Dialect: "postgres",
Result: `WHERE name LIKE E'%f\\\\oo\\_b\\%ar'`,
},
{
Note: "contains + pattern",
Source: UCASTNode{Type: "field", Field: "name", Op: "contains", Value: `f\oo_b%ar`},
Dialect: "postgres",
Result: `WHERE name LIKE E'%f\\\\oo\\_b\\%ar%'`,
},
{
Note: "Basic nested compound expression",
Source: UCASTNode{Type: "compound", Op: "and", Value: []UCASTNode{
{Type: "field", Op: "eq", Field: "name", Value: "bob"},
{Type: "field", Op: "gt", Field: "salary", Value: 50000},
{Type: "compound", Op: "or", Value: []UCASTNode{
{Type: "field", Op: "eq", Field: "role", Value: "admin"},
{Type: "field", Op: "ge", Field: "salary", Value: 100000},
}},
}},
Dialect: "postgres",
Result: "WHERE (name = E'bob' AND salary > 50000 AND (role = E'admin' OR salary >= 100000))",
},
{
Note: "'in' expression",
Source: UCASTNode{Type: "field", Field: "f", Op: "in", Value: []any{"foo", "bar"}},
Dialect: "postgres",
Result: "WHERE f IN (E'foo', E'bar')",
},
{
Note: "'not' compound expression",
Source: UCASTNode{Type: "compound", Op: "not", Value: []UCASTNode{
{Type: "field", Op: "eq", Field: "name", Value: "bob"},
}},
Dialect: "postgres",
Result: "WHERE NOT name = E'bob'",
},
}
for _, tc := range tests {
t.Run(tc.Note, func(t *testing.T) {
t.Parallel()
actual, err := tc.Source.AsSQL(tc.Dialect)
if err != nil && tc.Error != err.Error() {
t.Fatal(err)
}
if actual != tc.Result {
t.Fatalf("expected SQL string: '%s', got string: '%s'", tc.Result, actual)
}
})
}
}
+21 -15
View File
@@ -19,21 +19,27 @@ import (
// Well-known metric names.
const (
BundleRequest = "bundle_request"
ServerHandler = "server_handler"
ServerQueryCacheHit = "server_query_cache_hit"
SDKDecisionEval = "sdk_decision_eval"
RegoQueryCompile = "rego_query_compile"
RegoQueryEval = "rego_query_eval"
RegoQueryParse = "rego_query_parse"
RegoModuleParse = "rego_module_parse"
RegoDataParse = "rego_data_parse"
RegoModuleCompile = "rego_module_compile"
RegoPartialEval = "rego_partial_eval"
RegoInputParse = "rego_input_parse"
RegoLoadFiles = "rego_load_files"
RegoLoadBundles = "rego_load_bundles"
RegoExternalResolve = "rego_external_resolve"
BundleRequest = "bundle_request"
ServerHandler = "server_handler"
ServerQueryCacheHit = "server_query_cache_hit"
SDKDecisionEval = "sdk_decision_eval"
RegoQueryCompile = "rego_query_compile"
RegoQueryEval = "rego_query_eval"
RegoQueryParse = "rego_query_parse"
RegoModuleParse = "rego_module_parse"
RegoDataParse = "rego_data_parse"
RegoModuleCompile = "rego_module_compile"
RegoPartialEval = "rego_partial_eval"
RegoInputParse = "rego_input_parse"
RegoLoadFiles = "rego_load_files"
RegoLoadBundles = "rego_load_bundles"
RegoExternalResolve = "rego_external_resolve"
CompilePrepPartial = "compile_prep_partial"
CompileEvalConstraints = "compile_eval_constraints"
CompileTranslateQueries = "compile_translate_queries"
CompileExtractAnnotationsUnknowns = "compile_extract_annotations_unknowns"
CompileExtractAnnotationsMask = "compile_extract_annotations_mask"
CompileEvalMaskRule = "compile_eval_mask_rule"
)
// Info contains attributes describing the underlying metrics provider.
+360
View File
@@ -0,0 +1,360 @@
// Package compile collects a specialized interface to package rego, built for
// compiling policies into filters. It's a combination of simple evals (of any
// masking rule), and partial eval, equipped with the correct settings for some
// options; and paired with post-checks that determine if the result of partial
// evaluation can be translated into filter queries for certain targets/dialects.
// On success, the PE results are translated into queries, i.e. SQL WHERE clauses
// or UCAST expressions.
package compile
import (
"context"
"fmt"
"github.com/open-policy-agent/opa/internal/compile"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/metrics"
"github.com/open-policy-agent/opa/v1/rego"
// "github.com/open-policy-agent/opa/v1/storage"
"github.com/open-policy-agent/opa/v1/util"
)
type CompileOption func(*Compile)
type Compile struct {
targets []string
dialects []string
maskRule ast.Ref
unknowns []*ast.Term
query ast.Body
mappings map[string]any
metrics metrics.Metrics
regoOpts []func(*rego.Rego)
}
// Rego allows passing through common `*rego.Rego` options
func Rego(o ...func(*rego.Rego)) CompileOption {
return func(c *Compile) {
c.regoOpts = append(c.regoOpts, o...)
}
}
// Target lets you control the targets of a filter compilation. If repeated,
// it'll apply constraints for all the targets simultaneously (i.e. the
// union of their constraints = the intersection of supported features).
func Target(target, dialect string) CompileOption {
return func(c *Compile) {
c.targets = append(c.targets, target)
c.dialects = append(c.dialects, dialect)
}
}
// MaskRule determines which rule of the provided modules is to be evaluated
// to determine the masking of columns. Applying those masking rules is an out-
// of-band concern, when processing the results of the query.
func MaskRule(rule ast.Ref) CompileOption {
return func(c *Compile) {
c.maskRule = rule
}
}
// Mappings allows controlling the table and column names of the generated
// queries, if they don't match what's in the policy's unknowns.
// These can be simple maps, like
//
// {
// fruit: {
// $self: "fruit_table",
// name: "name_col",
// }
// }
//
// or per-target/per-dialect,
//
// {
// sql: { // per-target
// fruit: {
// $self: "fruit_table",
// name: "name_col",
// }
// }
// }
//
// {
// postgresql: { // per-dialect
// fruit: {
// $self: "fruit_table",
// name: "name_col",
// }
// }
// }
func Mappings(m map[string]any) CompileOption {
return func(c *Compile) {
c.mappings = m
}
}
// Metrics allows passing the `metrics.Metrics` to use for recording timers.
// It's passed along to the underlying `rego.Rego` evals, too.
func Metrics(m metrics.Metrics) CompileOption {
return func(c *Compile) {
c.metrics = m
}
}
// ParsedUnknowns lets you pass in the unknowns of this filter compilation.
func ParsedUnknowns(s ...*ast.Term) CompileOption {
return func(c *Compile) {
c.unknowns = s
}
}
// ParsedQuery lets you pass in the main entrypoint of this filter compilation.
func ParsedQuery(q ast.Body) CompileOption {
return func(c *Compile) {
c.query = q
}
}
// New creates a new `*compile.Compile` struct.
func New(opts ...CompileOption) *Compile {
c := &Compile{}
for i := range opts {
opts[i](c)
}
c.regoOpts = append(c.regoOpts,
rego.Metrics(c.metrics),
// We require evaluating non-det builtins for the translated targets:
// We're not able to meaningfully tanslate things like http.send, sql.send, or
// io.jwt.decode_verify into SQL or UCAST, so we try to eval them out where possible.
rego.NondeterministicBuiltins(true),
)
return c
}
// Prepared represents a ready-for-eval intermediate state where everything not depending
// on input has already been done.
type Prepared struct {
compile *Compile // carry along
constraintSet *compile.ConstraintSet
shorts compile.Set[string]
regoPrepareOptions []rego.PrepareOption
preparedMaskQuery *rego.PreparedEvalQuery
preparedPartialQuery *rego.PreparedPartialQuery
}
type PrepareOption func(*Prepared)
// RegoPrepareOptions lets you pass through any `rego.PrepareOption`.
func RegoPrepareOptions(o ...rego.PrepareOption) PrepareOption {
return func(p *Prepared) {
p.regoPrepareOptions = append(p.regoPrepareOptions, o...)
}
}
// noop allows us to call c.timer() for metrics even if we have no metrics
type noop struct{}
var n = noop{}
type minimetrics interface {
Start()
Stop() int64
}
func (noop) Start() {}
func (noop) Stop() int64 { return 0 }
func (c *Compile) timer(t string) minimetrics {
if c.metrics != nil {
return c.metrics.Timer(t)
}
return n
}
// Prepare evaluates as much as possible without knowing the (known) input yet
func (c *Compile) Prepare(ctx context.Context, po ...PrepareOption) (*Prepared, error) {
p := &Prepared{
compile: c,
}
for i := range po {
po[i](p)
}
// constraints
c.timer(metrics.CompileEvalConstraints).Start()
defer c.timer(metrics.CompileEvalConstraints).Stop()
constrs := make([]*compile.Constraint, len(p.compile.targets))
for i := range p.compile.targets {
var err error
constrs[i], err = compile.NewConstraints(p.compile.targets[i], p.compile.dialects[i]) // NewConstraints validates the tuples
if err != nil {
return nil, err
}
}
p.constraintSet = compile.NewConstraintSet(constrs...)
p.shorts = compile.ShortsFromMappings(p.compile.mappings)
// mask prep
if c.maskRule != nil {
rp, err := rego.New(append(c.regoOpts,
rego.ParsedQuery(ast.NewBody(ast.NewExpr(ast.NewTerm(c.maskRule)))),
)...).PrepareForEval(ctx, p.regoPrepareOptions...)
if err != nil {
return nil, fmt.Errorf("prepare for mask eval: %w", err)
}
p.preparedMaskQuery = &rp
}
// PE prep
rp, err := rego.New(append(c.regoOpts,
rego.ParsedQuery(c.query),
)...).PrepareForPartial(ctx, p.regoPrepareOptions...)
if err != nil {
return nil, fmt.Errorf("prepare for partial: %w", err)
}
p.preparedPartialQuery = &rp
return p, nil
}
// Filter represents the result of a policy-to-filter compilation for one
// specific target/dialect
type Filter struct {
Query any
Masks map[string]any
}
// Filters represents all the filters compiled from a policy. Can contain
// `compile.Filter` for various target/dialect combinations.
type Filters struct {
filters map[string]map[string]Filter
}
func (f *Filters) push(target, dialect string, query any, masks map[string]any) {
if f.filters == nil {
f.filters = make(map[string]map[string]Filter)
}
if f.filters[target] == nil {
f.filters[target] = make(map[string]Filter)
}
f.filters[target][dialect] = Filter{Query: query, Masks: masks}
}
// For is a helper method to retrieve the `compile.Filter` for a specific
// target/dialect.
func (f *Filters) For(target, dialect string) Filter {
return f.filters[target][dialect]
}
// One is a helper that extracts a single `compile.Filter`. Only safe to use
// when filters have been compiled for a single target/dialect, otherwise it'll
// return a random result of the compile filters. Panics when there's no filter
// at all.
func (f *Filters) One() Filter {
for i := range f.filters {
for j := range f.filters[i] {
return f.filters[i][j]
}
}
panic("empty filters")
}
// Compile does all the steps needed to generate `*compile.Filters` from a
// prepared state (`*compile.Prepared`).
func (p *Prepared) Compile(ctx context.Context, eo ...rego.EvalOption) (*Filters, error) {
// mask eval (non-partial)
var maskResult map[string]any
maskOpts := append(eo,
rego.EvalMetrics(p.compile.metrics),
rego.EvalRuleIndexing(false),
rego.EvalNondeterministicBuiltins(true),
)
if p.preparedMaskQuery != nil {
p.compile.timer(metrics.CompileEvalMaskRule).Start()
rs, err := p.preparedMaskQuery.Eval(ctx, maskOpts...)
if err != nil {
return nil, fmt.Errorf("evaluate masks: %w", err)
}
if len(rs) != 0 {
maskResultValue, err := ast.InterfaceToValue(rs[0].Expressions[0].Value)
if err != nil {
return nil, fmt.Errorf("convert masks: %w", err)
}
if err := util.Unmarshal([]byte(maskResultValue.String()), &maskResult); err != nil {
return nil, fmt.Errorf("convert masks: %w", err)
}
}
p.compile.timer(metrics.CompileEvalMaskRule).Stop()
}
// PE for conversion
opts := append(maskOpts,
rego.EvalParsedUnknowns(p.compile.unknowns),
)
pq, err := p.preparedPartialQuery.Partial(ctx, opts...)
if err != nil {
return nil, fmt.Errorf("partial eval: %w", err)
}
if errs := compile.Check(pq, p.constraintSet, p.shorts).ASTErrors(); errs != nil {
return nil, ast.Errors(errs)
}
p.compile.timer(metrics.CompileTranslateQueries).Start()
defer p.compile.timer(metrics.CompileTranslateQueries).Start()
ret := Filters{}
for i := range p.compile.targets {
target, dialect := p.compile.targets[i], p.compile.dialects[i]
if pq.Queries == nil { // unconditional NO
ret.push(target, dialect, nil, nil)
continue
}
mappings, err := lookupMappings(p.compile.mappings, target, dialect)
if err != nil {
return nil, fmt.Errorf("mappings: %w", err)
}
switch target {
case "ucast":
query := compile.QueriesToUCAST(pq.Queries, mappings)
ret.push(target, dialect, query.Map(), maskResult)
case "sql":
sql, err := compile.QueriesToSQL(pq.Queries, mappings, dialect)
if err != nil {
return nil, fmt.Errorf("convert to queries: %w", err)
}
ret.push(target, dialect, sql, maskResult)
}
}
return &ret, nil
}
func lookupMappings(mappings map[string]any, target, dialect string) (map[string]any, error) {
if mappings == nil {
return nil, nil
}
if md := mappings[dialect]; md != nil {
m, ok := md.(map[string]any)
if !ok {
return nil, fmt.Errorf("invalid mappings for dialect %s", dialect)
}
if m != nil {
return m, nil
}
}
if mt := mappings[target]; mt != nil {
n, ok := mt.(map[string]any)
if !ok {
return nil, fmt.Errorf("invalid mappings for target %s", target)
}
return n, nil
}
return mappings, nil
}
+168
View File
@@ -0,0 +1,168 @@
package compile_test
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/rego"
"github.com/open-policy-agent/opa/v1/rego/compile"
)
func TestCompileFilters(t *testing.T) {
t.Run("single target+dialect/mask", func(t *testing.T) {
target, dialect := "sql", "postgresql"
unknowns := []*ast.Term{ast.MustParseTerm("input.fruit")}
query := ast.MustParseBody("data.filters.include")
maskRule := ast.MustParseRef("data.filters.mask")
module := `package filters
include if input.fruit.name in input.names
mask.fruit.owner := {"replace": {"value": "***"}} if "banana" in input.names
`
r := compile.New(
compile.Target(target, dialect),
compile.ParsedUnknowns(unknowns...),
compile.ParsedQuery(query),
compile.MaskRule(maskRule),
compile.Rego(
rego.Module("filters.rego", module),
rego.Input(map[string]any{"names": []string{"apple", "banana"}}),
),
)
prep, err := r.Prepare(t.Context())
if err != nil {
t.Fatal(err)
}
filters, err := prep.Compile(t.Context())
if err != nil {
t.Fatal(err)
}
if exp, act := "WHERE fruit.name IN (E'apple', E'banana')", filters.One().Query; exp != act {
t.Errorf("query: expected %q, got %q", exp, act)
}
exp := map[string]any{"fruit": map[string]any{"owner": map[string]any{"replace": map[string]any{"value": "***"}}}}
act := filters.One().Masks
if diff := cmp.Diff(exp, act); diff != "" {
t.Error("unexpected masks (-want, +got):", diff)
}
})
t.Run("single target+dialect/mappings", func(t *testing.T) {
target, dialect := "sql", "postgresql"
unknowns := []*ast.Term{ast.MustParseTerm("input.fruit")}
query := ast.MustParseBody("data.filters.include")
module := `package filters
include if input.fruit.name in input.names
`
r := compile.New(
compile.Target(target, dialect),
compile.ParsedUnknowns(unknowns...),
compile.ParsedQuery(query),
compile.Mappings(map[string]any{"fruit": map[string]any{
"$self": "F",
"name": "N",
}}),
compile.Rego(
rego.Module("filters.rego", module),
rego.Input(map[string]any{"names": []string{"apple", "banana"}}),
),
)
prep, err := r.Prepare(t.Context())
if err != nil {
t.Fatal(err)
}
filters, err := prep.Compile(t.Context())
if err != nil {
t.Fatal(err)
}
if exp, act := "WHERE F.N IN (E'apple', E'banana')", filters.One().Query; exp != act {
t.Errorf("query: expected %q, got %q", exp, act)
}
})
t.Run("multiple target+dialect", func(t *testing.T) {
unknowns := []*ast.Term{ast.MustParseTerm("input.fruit")}
query := ast.MustParseBody("data.filters.include")
maskRule := ast.MustParseRef("data.filters.mask")
module := `package filters
include if input.fruit.name in input.names
mask.fruit.owner := {"replace": {"value": "***"}}
`
r := compile.New(
compile.Target("sql", "sqlserver"),
compile.Target("sql", "mysql"),
compile.Target("ucast", "prisma"),
compile.ParsedUnknowns(unknowns...),
compile.ParsedQuery(query),
compile.MaskRule(maskRule),
compile.Rego(
rego.Module("filters.rego", module),
rego.Input(map[string]any{"names": []string{"apple", "banana"}}),
),
)
prep, err := r.Prepare(t.Context())
if err != nil {
t.Fatal(err)
}
filters, err := prep.Compile(t.Context())
if err != nil {
t.Fatal(err)
}
t.Run("mysql", func(t *testing.T) {
if exp, act := "WHERE fruit.name IN ('apple', 'banana')", filters.For("sql", "mysql").Query; exp != act {
t.Errorf("query: expected %q, got %q", exp, act)
}
exp := map[string]any{"fruit": map[string]any{"owner": map[string]any{"replace": map[string]any{"value": "***"}}}}
act := filters.For("sql", "mysql").Masks
if diff := cmp.Diff(exp, act); diff != "" {
t.Error("unexpected masks (-want, +got):", diff)
}
})
t.Run("sqlserver", func(t *testing.T) {
if exp, act := "WHERE fruit.name IN (N'apple', N'banana')", filters.For("sql", "sqlserver").Query; exp != act {
t.Errorf("query: expected %q, got %q", exp, act)
}
exp := map[string]any{"fruit": map[string]any{"owner": map[string]any{"replace": map[string]any{"value": "***"}}}}
act := filters.For("sql", "sqlserver").Masks
if diff := cmp.Diff(exp, act); diff != "" {
t.Error("unexpected masks (-want, +got):", diff)
}
})
t.Run("ucast", func(t *testing.T) {
{
exp := map[string]any{
"field": "fruit.name",
"operator": "in",
"type": "field",
"value": []any{"apple", "banana"},
}
act := filters.For("ucast", "prisma").Query
if diff := cmp.Diff(exp, act); diff != "" {
t.Error("unexpected query: (-want, +got)", diff)
}
}
{
exp := map[string]any{"fruit": map[string]any{"owner": map[string]any{"replace": map[string]any{"value": "***"}}}}
act := filters.For("ucast", "prisma").Masks
if diff := cmp.Diff(exp, act); diff != "" {
t.Error("unexpected masks (-want, +got):", diff)
}
}
})
})
}
+605
View File
@@ -0,0 +1,605 @@
// Copyright 2025 The OPA Authors
// SPDX-License-Identifier: Apache-2.0
package server
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"slices"
"strings"
"github.com/open-policy-agent/opa/internal/compile"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/metrics"
"github.com/open-policy-agent/opa/v1/rego"
rego_compile "github.com/open-policy-agent/opa/v1/rego/compile"
"github.com/open-policy-agent/opa/v1/server/failtracer"
"github.com/open-policy-agent/opa/v1/server/types"
"github.com/open-policy-agent/opa/v1/server/writer"
"github.com/open-policy-agent/opa/v1/storage"
"github.com/open-policy-agent/opa/v1/topdown"
"github.com/open-policy-agent/opa/v1/topdown/builtins"
"github.com/open-policy-agent/opa/v1/util"
)
const (
// decisionLogType is injected under custom.type in the DL entry
decisionLogType = "open-policy-agent/compile"
invalidUnknownCode = "invalid_unknown"
// Timer names
timerPrepPartial = metrics.CompilePrepPartial
timerExtractAnnotationsUnknowns = metrics.CompileExtractAnnotationsUnknowns
timerExtractAnnotationsMask = metrics.CompileExtractAnnotationsMask
unknownsCacheSize = 500
maskingRuleCacheSize = 500
// These need to be kept up to date with `CompileApiKnownHeaders()` below
multiTargetJSON = "application/vnd.opa.multitarget+json"
ucastAllJSON = "application/vnd.opa.ucast.all+json"
ucastMinimalJSON = "application/vnd.opa.ucast.minimal+json"
ucastPrismaJSON = "application/vnd.opa.ucast.prisma+json"
ucastLINQJSON = "application/vnd.opa.ucast.linq+json"
sqlPostgresJSON = "application/vnd.opa.sql.postgresql+json"
sqlMySQLJSON = "application/vnd.opa.sql.mysql+json"
sqlSQLServerJSON = "application/vnd.opa.sql.sqlserver+json"
sqliteJSON = "application/vnd.opa.sql.sqlite+json"
// back-compat
applicationJSON = "application/json"
)
func CompileAPIKnownHeaders() []string {
return []string{
multiTargetJSON,
ucastAllJSON,
ucastMinimalJSON,
ucastPrismaJSON,
ucastLINQJSON,
sqlPostgresJSON,
sqlMySQLJSON,
sqlSQLServerJSON,
sqliteJSON,
}
}
var allKnownHeaders = append(CompileAPIKnownHeaders(), applicationJSON)
type CompileResult struct {
Query any `json:"query"`
Masks map[string]any `json:"masks,omitempty"`
}
// Incoming JSON request body structure.
type CompileFiltersRequestV1 struct {
Input *any `json:"input"`
Query string `json:"query"`
Unknowns *[]string `json:"unknowns"`
Options struct {
DisableInlining []string `json:"disableInlining,omitempty"`
Mappings map[string]any `json:"targetSQLTableMappings,omitempty"`
TargetDialects []string `json:"targetDialects,omitempty"`
MaskRule string `json:"maskRule,omitempty"`
} `json:"options"`
}
type compileFiltersRequest struct {
Query ast.Body
Input ast.Value
Unknowns []*ast.Term
Options compileFiltersRequestOptions
}
type compileFiltersRequestOptions struct {
MaskRule ast.Ref `json:"maskRule,omitempty"`
MaskInput ast.Value `json:"maskInput,omitempty"`
}
type CompileResponseV1 struct {
Result *any `json:"result,omitempty"`
Explanation types.TraceV1 `json:"explanation,omitempty"`
Metrics types.MetricsV1 `json:"metrics,omitempty"`
Hints []failtracer.Hint `json:"hints,omitempty"`
}
func (s *Server) v1CompileFilters(w http.ResponseWriter, r *http.Request) {
urlPath := r.PathValue("path")
if urlPath == "" {
writer.Error(w, http.StatusBadRequest, types.NewErrorV1(types.CodeInvalidParameter, "missing required 'path' parameter"))
return
}
ctx := r.Context()
explainMode := getExplain(r.URL, types.ExplainOffV1)
includeInstrumentation := getBoolParam(r.URL, types.ParamInstrumentV1, true)
m := metrics.New()
m.Timer(metrics.ServerHandler).Start()
m.Timer(metrics.RegoQueryParse).Start()
// decompress the input if sent as zip
body, err := util.ReadMaybeCompressedBody(r)
if err != nil {
writer.Error(w, http.StatusBadRequest, types.NewErrorV1(types.CodeInvalidParameter, "could not decompress the body: %v", err))
return
}
comp := s.getCompiler() // used for fuzzy rule name hints, and rego evals further down
// NOTE(sr): We keep some fields twice: from the unparsed and from the transformed
// request (`orig` and `request` respectively), because otherwise, we'd need to re-
// transform the values back for including them in the decision logs.
orig, request, reqErr := readInputCompileFiltersV1(comp, body, urlPath, s.manager.ParserOptions())
if reqErr != nil {
writer.Error(w, http.StatusBadRequest, reqErr)
return
}
m.Timer(metrics.RegoQueryParse).Stop()
c := storage.NewContext().WithMetrics(m)
txn, err := s.store.NewTransaction(ctx, storage.TransactionParams{Context: c})
if err != nil {
writer.ErrorAuto(w, err)
return
}
defer s.store.Abort(ctx, txn)
var buf *topdown.BufferTracer
if explainMode != types.ExplainOffV1 {
buf = topdown.NewBufferTracer()
}
unknowns := request.Unknowns // always wins over annotations if provided
maskingRule := request.Options.MaskRule // always wins over annotations if provided
// check annotations if one of these is missing, needs own compiler
// NB(sr): we throw this compiler away at the end -- what it produced for us is going to be cached
var annotationsCompiler *ast.Compiler
if len(unknowns) == 0 || maskingRule == nil {
var errs ast.Errors
annotationsCompiler, errs = prepareAnnotations(ctx, comp, s.store, txn, s.manager.ParserOptions())
if len(errs) > 0 {
writer.Error(w, http.StatusBadRequest,
types.NewErrorV1(types.CodeEvaluation, types.MsgEvaluationError).
WithASTErrors(errs))
return
}
}
if len(unknowns) == 0 { // check cache for unknowns
var errs []*ast.Error
unknowns, errs = s.compileFiltersUnknowns(m, annotationsCompiler, urlPath, request.Query)
if errs != nil {
writer.Error(w, http.StatusBadRequest,
types.NewErrorV1(types.CodeEvaluation, types.MsgEvaluationError).
WithASTErrors(errs))
return
}
}
if maskingRule == nil {
var errs []*ast.Error
maskingRule, errs = s.compileFiltersMaskRule(m, annotationsCompiler, urlPath, request.Query)
if errs != nil {
writer.Error(w, http.StatusBadRequest,
types.NewErrorV1(types.CodeEvaluation, types.MsgEvaluationError).
WithASTErrors(errs))
return
}
}
var ndbCache builtins.NDBCache
if s.ndbCacheEnabled {
ndbCache = builtins.NDBCache{}
}
contentType, err := sanitizeHeader(r.Header.Get("Accept"))
if err != nil {
writer.Error(w, http.StatusBadRequest, types.NewErrorV1(types.CodeInvalidParameter, "Accept header: %s", err.Error()))
return
}
target, dialect := targetDialect(contentType)
targetOption := []rego_compile.CompileOption{}
multi := make([][2]string, len(orig.Options.TargetDialects))
switch target {
case "multi":
for i, targetTuple := range orig.Options.TargetDialects {
s := strings.Split(targetTuple, "+")
target, dialect := s[0], s[1]
multi[i] = [2]string{target, dialect}
targetOption = append(targetOption, rego_compile.Target(target, dialect))
}
default:
targetOption = append(targetOption, rego_compile.Target(target, dialect))
}
m.Timer(timerPrepPartial).Start()
// NB(sr): just cache preparedCompile by path?
preparedCompile, err := rego_compile.New(
append(targetOption,
rego_compile.ParsedUnknowns(unknowns...),
rego_compile.ParsedQuery(request.Query),
rego_compile.Metrics(m),
rego_compile.Mappings(orig.Options.Mappings),
rego_compile.MaskRule(maskingRule),
rego_compile.Rego(
rego.Compiler(comp),
rego.Store(s.store),
rego.Transaction(txn),
rego.DisableInlining(orig.Options.DisableInlining),
rego.QueryTracer(buf),
rego.Instrument(includeInstrumentation),
rego.NDBuiltinCache(ndbCache),
rego.Runtime(s.runtime),
rego.UnsafeBuiltins(unsafeBuiltinsMap),
rego.InterQueryBuiltinCache(s.interQueryBuiltinCache),
rego.InterQueryBuiltinValueCache(s.interQueryBuiltinValueCache),
rego.PrintHook(s.manager.PrintHook()),
rego.DistributedTracingOpts(s.distributedTracingOpts),
),
)...,
).Prepare(ctx)
if err != nil {
switch err := err.(type) {
case ast.Errors:
writer.Error(w, http.StatusBadRequest, types.NewErrorV1(types.CodeInvalidParameter, types.MsgCompileModuleError).WithASTErrors(err))
default:
writer.ErrorAuto(w, err)
}
return
}
m.Timer(timerPrepPartial).Stop()
qt := failtracer.New()
filters, err := preparedCompile.Compile(ctx,
rego.EvalTransaction(txn),
rego.EvalParsedInput(request.Input),
rego.EvalPrintHook(s.manager.PrintHook()),
rego.EvalNDBuiltinCache(ndbCache),
rego.EvalInterQueryBuiltinCache(s.interQueryBuiltinCache),
rego.EvalInterQueryBuiltinValueCache(s.interQueryBuiltinValueCache),
rego.EvalQueryTracer(qt),
)
if err != nil {
switch err := err.(type) {
case ast.Errors:
writer.Error(w, http.StatusBadRequest, types.NewErrorV1(types.CodeEvaluation, types.MsgEvaluationError).WithASTErrors(err))
default:
writer.ErrorAuto(w, err)
}
return
}
result := CompileResponseV1{
Hints: qt.Hints(unknowns),
}
switch target {
case "multi":
targets := struct {
UCAST *CompileResult `json:"ucast,omitempty"`
Postgres *CompileResult `json:"postgresql,omitempty"`
MySQL *CompileResult `json:"mysql,omitempty"`
MSSQL *CompileResult `json:"sqlserver,omitempty"`
SQLite *CompileResult `json:"sqlite,omitempty"`
}{}
for _, targetTuple := range multi {
target, dialect := targetTuple[0], targetTuple[1]
f := filters.For(target, dialect)
var cr *CompileResult
if f.Query != nil {
cr = &CompileResult{Query: f.Query, Masks: f.Masks}
}
switch target {
case "ucast":
if targets.UCAST != nil {
continue // there's only one UCAST representation, don't translate that twice
}
targets.UCAST = cr
case "sql":
switch dialect {
case "postgresql":
targets.Postgres = cr
case "mysql":
targets.MySQL = cr
case "sqlserver":
targets.MSSQL = cr
case "sqlite":
targets.SQLite = cr
}
}
}
t0 := any(targets)
result.Result = &t0
default:
f := filters.For(target, dialect)
if f.Query != nil {
s0 := any(&CompileResult{Query: f.Query, Masks: f.Masks})
result.Result = &s0
}
}
m.Timer(metrics.ServerHandler).Stop()
fin(w, result, contentType, m, includeMetrics(r), includeInstrumentation, pretty(r))
unk := make([]string, len(unknowns))
for i := range unknowns {
unk[i] = unknowns[i].String()
}
br, _ := getRevisions(ctx, s.store, txn)
ctx, logger := s.getDecisionLogger(ctx, br)
custom := map[string]any{
"options": orig.Options,
"unknowns": unk,
"type": decisionLogType,
"mask_rule": maskingRule.String(),
}
if err := logger.Log(ctx, txn, urlPath, orig.Query, orig.Input, request.Input, result.Result, ndbCache, nil, m, custom); err != nil {
writer.ErrorAuto(w, err)
return
}
}
func (s *Server) compileFiltersUnknowns(m metrics.Metrics, comp *ast.Compiler, path string, query ast.Body) ([]*ast.Term, []*ast.Error) {
key := path
unknowns, ok := s.compileUnknownsCache.Get(key)
if ok {
return unknowns, nil
}
m.Timer(timerExtractAnnotationsUnknowns).Start()
if len(query) != 1 {
return nil, nil
}
q, ok := query[0].Terms.(*ast.Term)
if !ok {
return nil, nil
}
queryRef, ok := q.Value.(ast.Ref)
if !ok {
return nil, nil
}
parsedUnknowns, errs := compile.ExtractUnknownsFromAnnotations(comp, queryRef)
if errs != nil {
return nil, errs
}
unknowns = parsedUnknowns
m.Timer(timerExtractAnnotationsUnknowns).Stop()
s.compileUnknownsCache.Add(key, unknowns)
return unknowns, nil
}
func (s *Server) compileFiltersMaskRule(m metrics.Metrics, comp *ast.Compiler, path string, query ast.Body) (ast.Ref, []*ast.Error) {
key := path
mr, ok := s.compileMaskingRulesCache.Get(key)
if ok {
return mr, nil
}
m.Timer(timerExtractAnnotationsMask).Start()
if len(query) != 1 {
return nil, nil
}
q, ok := query[0].Terms.(*ast.Term)
if !ok {
return nil, nil
}
queryRef, ok := q.Value.(ast.Ref)
if !ok {
return nil, nil
}
parsedMaskingRule, err := compile.ExtractMaskRuleRefFromAnnotations(comp, queryRef)
if err != nil {
return parsedMaskingRule, []*ast.Error{err}
}
mr = parsedMaskingRule
m.Timer(timerExtractAnnotationsMask).Stop()
s.compileMaskingRulesCache.Add(key, parsedMaskingRule)
return mr, nil
}
func readInputCompileFiltersV1(comp *ast.Compiler, reqBytes []byte, urlPath string, queryParserOptions ast.ParserOptions) (*CompileFiltersRequestV1, *compileFiltersRequest, *types.ErrorV1) {
var request CompileFiltersRequestV1
if len(reqBytes) > 0 {
if err := util.NewJSONDecoder(bytes.NewBuffer(reqBytes)).Decode(&request); err != nil {
return nil, nil, types.NewErrorV1(types.CodeInvalidParameter, "error(s) occurred while decoding request: %v", err.Error())
}
}
var query ast.Body
var err error
if urlPath != "" {
v, err := stringPathToDataRef(urlPath)
if err != nil {
return nil, nil, types.NewErrorV1(types.CodeInvalidParameter, "invalid path: %v", err)
}
query = []*ast.Expr{ast.NewExpr(ast.NewTerm(v))}
} else { // attempt to parse query
query, err = ast.ParseBodyWithOpts(request.Query, queryParserOptions)
if err != nil {
switch err := err.(type) {
case ast.Errors:
return nil, nil, types.NewErrorV1(types.CodeInvalidParameter, types.MsgParseQueryError).WithASTErrors(err)
default:
return nil, nil, types.NewErrorV1(types.CodeInvalidParameter, "%v: %v", types.MsgParseQueryError, err)
}
}
}
var input ast.Value
if request.Input != nil {
input, err = ast.InterfaceToValue(*request.Input)
if err != nil {
return nil, nil, types.NewErrorV1(types.CodeInvalidParameter, "error(s) occurred while converting input: %v", err)
}
}
var unknowns []*ast.Term
if request.Unknowns != nil {
unknowns = make([]*ast.Term, len(*request.Unknowns))
for i, s := range *request.Unknowns {
unknowns[i], err = ast.ParseTerm(s)
if err != nil {
return nil, nil, types.NewErrorV1(types.CodeInvalidParameter, "error(s) occurred while parsing unknowns: %v", err)
}
}
}
var maskRuleRef ast.Ref
if request.Options.MaskRule != "" {
maskPath := request.Options.MaskRule
if !strings.HasPrefix(request.Options.MaskRule, "data.") {
// If the mask_rule is not a data ref try adding package prefix from URL path.
dataFiltersRuleRef, _ := stringPathToDataRef(urlPath)
maskPath = dataFiltersRuleRef[:len(dataFiltersRuleRef)-1].String() + "." + request.Options.MaskRule
}
maskRuleRef, err = ast.ParseRef(maskPath)
if err != nil {
hint := compile.FuzzyRuleNameMatchHint(comp, request.Options.MaskRule)
return nil, nil, types.NewErrorV1(types.CodeInvalidParameter, "error(s) occurred while parsing mask_rule name: %s", hint)
}
}
return &request, &compileFiltersRequest{
Query: query,
Input: input,
Unknowns: unknowns,
Options: compileFiltersRequestOptions{
MaskRule: maskRuleRef,
},
}, nil
}
func fin(w http.ResponseWriter,
result CompileResponseV1,
contentType string,
metrics metrics.Metrics,
includeMetrics, includeInstrumentation, pretty bool,
) {
if includeMetrics || includeInstrumentation {
result.Metrics = metrics.All()
}
enc := json.NewEncoder(w)
if pretty {
enc.SetIndent("", " ")
}
w.Header().Add("Content-Type", contentType)
// If Encode() calls w.Write() for the first time, it'll set the HTTP status
// to 200 OK.
if err := enc.Encode(result); err != nil {
writer.ErrorAuto(w, err)
return
}
}
func sanitizeHeader(accept string) (string, error) {
if accept == "" {
return "", errors.New("missing required header")
}
if strings.Contains(accept, ",") {
return "", errors.New("multiple headers not supported")
}
if !slices.Contains(allKnownHeaders, accept) {
return "", fmt.Errorf("unsupported header: %s", accept)
}
return accept, nil
}
func targetDialect(accept string) (string, string) {
switch accept {
case applicationJSON:
return "", ""
case multiTargetJSON:
return "multi", ""
case ucastAllJSON:
return "ucast", "all"
case ucastMinimalJSON:
return "ucast", "minimal"
case ucastPrismaJSON:
return "ucast", "prisma"
case ucastLINQJSON:
return "ucast", "linq"
case sqlPostgresJSON:
return "sql", "postgresql"
case sqlMySQLJSON:
return "sql", "mysql"
case sqlSQLServerJSON:
return "sql", "sqlserver"
case sqliteJSON:
return "sql", "sqlite"
}
panic("unreachable")
}
func cloneCompiler(c *ast.Compiler) *ast.Compiler {
return ast.NewCompiler().
WithDefaultRegoVersion(c.DefaultRegoVersion()).
WithCapabilities(c.Capabilities())
}
func prepareAnnotations(
ctx context.Context,
comp *ast.Compiler,
store storage.Store,
txn storage.Transaction,
po ast.ParserOptions,
) (*ast.Compiler, ast.Errors) {
var errs []*ast.Error
mods, err := store.ListPolicies(ctx, txn)
if err != nil {
return nil, append(errs, ast.NewError(invalidUnknownCode, nil, "failed to list policies for annotation set: %s", err))
}
po.ProcessAnnotation = true
po.RegoVersion = comp.DefaultRegoVersion()
modules := make(map[string]*ast.Module, len(mods))
for _, module := range mods {
vsn, ok := comp.Modules[module]
if ok { // NB(sr): I think this should be impossible. Let's try not to panic, and fall back to the default if it _does happen_.
po.RegoVersion = vsn.RegoVersion()
}
rego, err := store.GetPolicy(ctx, txn, module)
if err != nil {
return nil, append(errs, ast.NewError(invalidUnknownCode, nil, "failed to read module for annotation set: %s", err))
}
m, err := ast.ParseModuleWithOpts(module, string(rego), po)
if err != nil {
errs = append(errs, ast.NewError(invalidUnknownCode, nil, "failed to parse module for annotation set: %s", err))
continue
}
modules[module] = m
}
if errs != nil {
return nil, errs
}
comp0 := cloneCompiler(comp)
comp0.WithPathConflictsCheck(storage.NonEmpty(ctx, store, txn)).Compile(modules)
if len(comp0.Errors) > 0 {
return nil, comp0.Errors
}
return comp0, nil
}
+75
View File
@@ -0,0 +1,75 @@
// Copyright 2025 The OPA Authors
// SPDX-License-Identifier: Apache-2.0
package server
import (
"bytes"
_ "embed"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
//go:embed testdata/bench_filters.rego
var benchRego []byte
//go:embed testdata/roles.json
var rolesJSON []byte
var roles = func() any {
var roles any
if err := json.Unmarshal(rolesJSON, &roles); err != nil {
panic(err)
}
return roles
}()
func BenchmarkCompileHandler(b *testing.B) {
b.ReportAllocs()
f := setup(b, string(benchRego), roles)
input := map[string]any{
"user": "caesar",
"tenant": map[string]any{
"id": 2,
"name": "acmecorp",
},
}
path := "filters/include"
targets := []string{
"application/vnd.opa.sql.postgresql+json",
"application/vnd.opa.ucast.prisma+json",
}
for _, target := range targets {
b.Run(strings.Split(target, "/")[1], func(b *testing.B) {
// NB(sr): Unknowns are provided with the request: we don't want to benchmark the cache here
// The percentile-recording tests below is making use of the unknowns cache.
payload := map[string]any{
"input": input,
"unknowns": []string{"input.tickets", "input.users"},
}
jsonData, err := json.Marshal(payload)
if err != nil {
b.Fatalf("Failed to marshal JSON: %v", err)
}
b.ResetTimer()
for b.Loop() {
req := httptest.NewRequest("POST", "/v1/compile/"+path, bytes.NewBuffer(jsonData))
if err != nil {
b.Fatalf("Failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", target)
if err := f.executeRequest(req, http.StatusOK, ""); err != nil {
b.Fatal(err)
}
}
})
}
}
+864
View File
@@ -0,0 +1,864 @@
// Copyright 2025 The OPA Authors
// SPDX-License-Identifier: Apache-2.0
package server
import (
"bytes"
"cmp"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/open-policy-agent/opa/internal/compile"
"github.com/open-policy-agent/opa/v1/ast"
)
const (
ucastAcceptHeader = "application/vnd.opa.ucast.prisma+json"
sqlAcceptHeader = "application/vnd.opa.sql.postgresql+json"
)
// Error is needed here because the ast.Error type cannot be
// unmarshalled from JSON: it contains an interface value.
type Error struct {
Code string `json:"code"`
Message string `json:"message"`
Location *ast.Location `json:"location,omitempty"`
Details *compile.Details `json:"details,omitempty"`
}
// NOTE(sr): The important thing about these tests is that we don't mock
// the partially-evaluated Rego. Instead, we store the data filter policy,
// run the PE-post-analysing handler, and have assertions on its response.
// The happy-path assertions all request a UCAST response, with the "all"
// variant, which includes all the builtins. For error cases, we check
// both constrained UCAST, and SQL.
func TestPostPartialChecks(t *testing.T) {
t.Parallel()
const defaultPath = "filters/include"
defaultInput := map[string]any{
"a": true,
"b": false,
"colour": "orange",
}
defaultUnknowns := []string{"input.fruits", "input.baskets"}
for _, tc := range []struct {
note string
target string
rego string
regoVerbatim bool
omitUnknowns bool
input any
query string
errors []Error
mappings map[string]any
skip string
result map[string]any
}{
{
note: "happy path",
rego: `include if input.fruits.colour == input.colour`,
result: map[string]any{"type": "field", "field": "fruits.colour", "operator": "eq", "value": "orange"},
},
{
note: "unconditional NO",
rego: `include if false`,
result: nil,
},
{
note: "unconditional YES",
rego: `include if true`,
result: map[string]any{},
},
{
note: "happy path, reading unknowns from metadata (document scope)",
omitUnknowns: true,
rego: `
# METADATA
# scope: document
# custom:
# unknowns:
# - input.fruits
include if input.fruits.colour == input.colour
_use_metadata := rego.metadata.chain()`,
result: map[string]any{"type": "field", "field": "fruits.colour", "operator": "eq", "value": "orange"},
},
{
note: "happy path, reading unknowns from metadata (package scope)",
omitUnknowns: true,
regoVerbatim: true,
rego: `
# METADATA
# scope: package
# custom:
# unknowns:
# - input.fruits
package filters
include if input.fruits.colour == input.colour
_use_metadata := rego.metadata.chain()`,
result: map[string]any{"type": "field", "field": "fruits.colour", "operator": "eq", "value": "orange"},
},
{
note: "happy path, reading unknowns from metadata (package scope, no kludge)",
omitUnknowns: true,
regoVerbatim: true,
rego: `
# METADATA
# scope: package
# custom:
# unknowns:
# - input.fruits
package filters
include if input.fruits.colour == input.colour
_use_metadata := rego.metadata.chain()`,
result: map[string]any{"type": "field", "field": "fruits.colour", "operator": "eq", "value": "orange"},
},
{
note: "happy path, reading unknowns from metadata chain, correct rule only (document scope)",
omitUnknowns: true,
regoVerbatim: true,
rego: `
# METADATA
# scope: package
# custom:
# unknowns:
# - input.fruits
package filters
# METADATA
# scope: document
# custom:
# unknowns:
# - input.baskets
include if {
input.fruits.colour == input.colour
input.baskets.colour == input.colour
}
# METADATA
# scope: document
# description: if this metadata were picked up, the checks would fail
# custom:
# unknowns:
# - input.colour
red_herring if true
_use_metadata := rego.metadata.chain()`,
result: map[string]any{
"type": "compound",
"operator": "and",
"value": []any{
map[string]any{"type": "field", "field": "fruits.colour", "operator": "eq", "value": "orange"},
map[string]any{"type": "field", "field": "baskets.colour", "operator": "eq", "value": "orange"},
},
},
},
{
note: "happy path, reading unknowns from metadata chain, correct rule only (document scope, without kludge)",
omitUnknowns: true,
regoVerbatim: true,
rego: `
# METADATA
# scope: package
# custom:
# unknowns:
# - input.fruits
package filters
# METADATA
# scope: document
# custom:
# unknowns:
# - input.baskets
include if {
input.fruits.colour == input.colour
input.baskets.colour == input.colour
}
# METADATA
# scope: document
# description: if this metadata were picked up, the checks would fail
# custom:
# unknowns:
# - input.colour
red_herring if true
`,
result: map[string]any{
"type": "compound",
"operator": "and",
"value": []any{
map[string]any{"type": "field", "field": "fruits.colour", "operator": "eq", "value": "orange"},
map[string]any{"type": "field", "field": "baskets.colour", "operator": "eq", "value": "orange"},
},
},
},
{
note: "undefined field value",
rego: `include if input.fruits.colour == null`,
result: map[string]any{"type": "field", "field": "fruits.colour", "operator": "eq", "value": nil},
},
{
note: "happy path, compound 'and'",
rego: `include if { input.fruits.colour == input.colour; input.fruits.name == "clementine" }`,
result: map[string]any{
"type": "compound",
"operator": "and",
"value": []any{
map[string]any{"type": "field", "field": "fruits.colour", "operator": "eq", "value": "orange"},
map[string]any{"type": "field", "field": "fruits.name", "operator": "eq", "value": "clementine"},
},
},
},
{
note: "happy path, compound 'or'",
rego: `include if input.fruits.colour == input.colour
include if input.fruits.name == "clementine"`,
result: map[string]any{
"type": "compound",
"operator": "or",
"value": []any{
map[string]any{"type": "field", "field": "fruits.colour", "operator": "eq", "value": "orange"},
map[string]any{"type": "field", "field": "fruits.name", "operator": "eq", "value": "clementine"},
},
},
},
{
note: "happy path, compound mixed",
rego: `include if input.fruits.colour == input.colour
include if { input.fruits.name == "clementine"; input.fruits.price > 10 }`,
result: map[string]any{
"type": "compound",
"operator": "or",
"value": []any{
map[string]any{"type": "field", "field": "fruits.colour", "operator": "eq", "value": "orange"},
map[string]any{
"type": "compound",
"operator": "and",
"value": []any{
map[string]any{"type": "field", "field": "fruits.name", "operator": "eq", "value": "clementine"},
map[string]any{"type": "field", "field": "fruits.price", "operator": "gt", "value": float64(10)},
},
},
},
},
},
{
note: "happy path, reversed",
rego: `include if input.colour == input.fruits.colour`,
result: map[string]any{"type": "field", "field": "fruits.colour", "operator": "eq", "value": "orange"},
},
{
note: "happy path, comparison",
rego: `include if input.fruits.price > 10`,
result: map[string]any{"type": "field", "field": "fruits.price", "operator": "gt", "value": float64(10)},
},
{
note: "happy path, comparison with two unknowns",
target: "application/vnd.opa.ucast.all+json",
rego: `include if input.fruits.price < input.fruits.max_price`,
result: map[string]any{"type": "field", "field": "fruits.price", "operator": "lt", "value": map[string]any{"field": "fruits.max_price"}},
},
{
note: "happy path, comparison, reversed",
rego: `include if 10 <= input.fruits.price`,
result: map[string]any{"type": "field", "field": "fruits.price", "operator": "gt", "value": float64(10)},
},
{
note: "happy path, not-equal",
rego: `include if input.fruits.name != "apple"`,
result: map[string]any{"type": "field", "field": "fruits.name", "operator": "ne", "value": "apple"},
},
{
note: "happy path, mapped short unknown",
rego: `package filters
# METADATA
# custom:
# unknowns:
# - input.name
include if input.name != "apple"`,
regoVerbatim: true,
omitUnknowns: true,
mappings: map[string]any{
"name": map[string]any{"$table": "fruits"},
},
result: map[string]any{"type": "field", "field": "fruits.name", "operator": "ne", "value": "apple"},
},
{
note: "happy path, double-mapped short unknown",
rego: `package filters
# METADATA
# custom:
# unknowns:
# - input.name
include if input.name != "apple"`,
regoVerbatim: true,
omitUnknowns: true,
mappings: map[string]any{
"name": map[string]any{"$table": "fruits"},
"fruits": map[string]any{"$self": "FRUIT", "name": "NAME"},
},
result: map[string]any{"type": "field", "field": "FRUIT.NAME", "operator": "ne", "value": "apple"},
},
{
note: "happy path, not+equal",
rego: `include if not input.fruits.name == "apple"`,
result: map[string]any{
"type": "compound",
"operator": "not",
"value": []any{
map[string]any{"type": "field", "field": "fruits.name", "operator": "eq", "value": "apple"}},
},
},
{
note: "happy path, not-equal, reversed",
rego: `include if "apple" != input.fruits.name`,
result: map[string]any{"type": "field", "field": "fruits.name", "operator": "ne", "value": "apple"},
},
{
note: "happy path, startswith",
rego: `include if startswith(input.fruits.name, "app")`,
result: map[string]any{"type": "field", "field": "fruits.name", "operator": "startswith", "value": "app"},
},
{
note: "happy path, endswith",
rego: `include if endswith(input.fruits.name, "le")`,
result: map[string]any{"type": "field", "field": "fruits.name", "operator": "endswith", "value": "le"},
},
{
note: "happy path, contains",
rego: `include if contains(input.fruits.name, "ppl")`,
result: map[string]any{"type": "field", "field": "fruits.name", "operator": "contains", "value": "ppl"},
},
{
note: "happy path, internal.member_2 (ucast/linq)",
rego: `include if input.fruits.name in {"apple", "pear"}`,
target: "application/vnd.opa.ucast.linq+json",
result: map[string]any{"type": "field", "field": "fruits.name", "operator": "in", "value": []any{"apple", "pear"}},
},
{
note: "invalid expression: not+equal (ucast/linq)",
target: "application/vnd.opa.ucast.linq+json",
rego: `include if not input.fruits.name == "apple"`,
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: "\"not\" not permitted: unsupported feature \"not\" for UCAST (LINQ)",
},
},
},
{
note: "invalid builtin: internal.member_2 (ucast/minimal)",
rego: `include if input.fruits.name in {"apple", "pear"}`,
target: "application/vnd.opa.ucast.minimal+json",
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 30),
Message: "invalid builtin `in`: unsupported for UCAST",
},
},
},
{
note: "invalid builtin: startswith (ucast/linq)",
rego: `include if startswith(input.fruits.name, "app")`,
target: "application/vnd.opa.ucast.linq+json",
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: "invalid builtin `startswith`: unsupported for UCAST (LINQ)",
},
},
},
{
note: "invalid builtin: startswith (ucast)",
rego: `include if startswith(input.fruits.name, "app")`,
target: "application/vnd.opa.ucast.minimal+json",
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: "invalid builtin `startswith`: unsupported for UCAST",
},
},
},
{
note: "invalid builtin: startswith (sqlite)",
rego: `include if startswith(input.fruits.name, "app")`,
target: "application/vnd.opa.sql.sqlite+json",
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: "invalid builtin `startswith`: unsupported for SQL (sqlite)",
},
},
},
{
note: "invalid builtin: endswith (sqlite)",
rego: `include if endswith(input.fruits.name, "ple")`,
target: "application/vnd.opa.sql.sqlite+json",
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: "invalid builtin `endswith`: unsupported for SQL (sqlite)",
},
},
},
{
note: "invalid builtin: contains (sqlite)",
rego: `include if contains(input.fruits.name, "pp")`,
target: "application/vnd.opa.sql.sqlite+json",
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: "invalid builtin `contains`: unsupported for SQL (sqlite)",
},
},
},
{
note: "invalid builtin",
rego: `include if object.get(input, ["fruits", "colour"], "grey") == input.colour`,
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: "invalid builtin `object.get`",
},
},
},
{
note: "invalid use of 'k, v in...'",
rego: `include if "k", input.fruits.colour in {"k": "grey", "k2": input.colour}`,
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 37),
Message: "invalid use of \"... in ...\"",
},
},
},
{
note: "invalid use of '...in...' (ucast)",
rego: `include if "k", input.fruits.colour in {"k": "grey", "k2": "orange"}`,
target: "application/vnd.opa.ucast.linq+json",
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 37),
Message: "invalid use of \"... in ...\"",
},
},
},
{
note: "nested comp",
rego: `include if (input.fruits.colour == "orange")>0`,
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 45),
Message: `gt: nested call operand: equal(input.fruits.colour, "orange")`, // TODO(sr): make this a user-friendlier message
},
},
},
{
note: "nested call, object.get",
rego: `user := object.get(input, ["user"], "unknown")
include if user == input.fruits.user`,
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 9),
Message: `eq: nested call operand: object.get(input, ["user"], "unknown")`, // TODO(sr): make this a user-friendlier message
},
},
},
{
note: "rhs+lhs both unknown",
rego: `include if input.fruits.colour == input.baskets.colour`,
target: "application/vnd.opa.ucast.all+json",
result: map[string]any{
"type": "field",
"field": "fruits.colour",
"operator": "eq",
"value": map[string]any{"field": "baskets.colour"},
},
},
{ // TODO(sr): ucast-prisma doesn't support this yet
note: "rhs+lhs both unknown, unsupported",
rego: `include if input.fruits.colour == input.baskets.colour`,
target: "application/vnd.opa.ucast.prisma+json",
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: `reference to field: unsupported feature "field-ref" for UCAST (prisma)`,
},
},
},
{
note: "contains: rhs unknown",
rego: `include if contains("foobar", input.fruits.colour)`,
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: "rhs of contains must be known",
},
},
},
{
note: "startswith: rhs unknown",
rego: `include if startswith("foobar", input.fruits.colour)`,
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: "rhs of startswith must be known",
},
},
},
{
note: "endswith: rhs unknown",
rego: `include if endswith("foobar", input.fruits.colour)`,
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: "rhs of endswith must be known",
},
},
},
{
note: "non-scalar comparison",
rego: `include if input.fruits.colour <= {"green", "blue"}`, // nonsense, but still
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 32), // NOTE(sr): 32 is the `<=`
Message: "both rhs and lhs non-scalar/non-ground",
},
},
},
{
note: "not a call/term",
rego: `include if input.fruits.colour`,
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: "invalid statement \"input.fruits.colour\"",
Details: &compile.Details{Extra: "try `input.fruits.colour != false`"},
},
},
},
{
note: "not a call/term, using with",
rego: `include if {
foo with input.fruits.colour as "red"
}
foo if input.fruits.colour`,
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 4, 2),
Message: "\"with\" not permitted",
},
},
},
{
note: "not a call/every",
rego: `include if every x in input.fruits.xs { x != "y" }`,
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: "\"every\" not permitted",
},
},
},
{
note: "reference other row",
rego: `include if {
some other in input.fruits
input.fruits.price > other.price
}`,
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 5, 23),
Message: "gt: invalid ref operand: input.fruits[__local1__1].price",
},
},
},
{
note: "support module: default rule that is not false",
rego: `include if other
default other := 100
other if input.fruits.price > 100
`,
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: "use of default rule in data.filters.other",
},
},
},
{
note: "support module: multi-value rule",
rego: `include if mv
mv contains 1 if input.fruits.price <= 1
mv contains 2 if input.fruits.price <= 2
`,
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: "use of multi-value rule in data.filters.mv",
},
},
},
{
// NOTE(sr): This could lead to data policies getting accepted _now_ that at
// some later time -- when the bug is fixed -- would no longer be valid.
note: "support module: default function",
rego: `include if cheap(input.fruits)
default cheap(_) := true
cheap(f) if f.price < 100
`,
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: "use of default rule in data.filters.other",
},
},
skip: `https://github.com/open-policy-agent/opa/issues/7220`,
},
{
note: "ref into module: complete rule with else",
rego: `include if other
other if input.fruits.price > 100
else := input.fruits.extra
`,
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: "invalid data reference \"data.filters.other\"",
Details: &compile.Details{Extra: "has rule \"data.filters.other\" an `else`?"},
},
},
},
{
note: "ref into module: function with else",
rego: `include if func(input.fruits)
func(f) if f.price > 100
else := true
`,
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: "invalid data reference \"data.filters.func(input.fruits)\"",
Details: &compile.Details{Extra: "has function \"data.filters.func(...)\" an `else`?"},
},
},
},
{
// NOTE(sr): this seems like one of the lower-hanging fruit for translating:
// queries: [not data.partial.__not1_0_2__]
// support:
// package partial
// __not1_0_2__ if input.fruits.price > 100
note: "invalid expression: complete rule with not",
rego: `include if not other
other if input.fruits.price > 100
`,
errors: []Error{
{
Code: "pe_fragment_error",
Location: ast.NewLocation(nil, "filters.rego", 3, 12),
Message: "\"not\" not permitted",
},
},
},
{
note: "bad metadata unknowns",
omitUnknowns: true,
rego: `
# METADATA
# custom:
# unknowns:
# - inpu.fruits
# - data.whatever
# - future.keywrds
include if input.fruits.colour == input.colour
_use_metadata := rego.metadata.chain()`,
errors: []Error{
{
Code: "invalid_unknown",
Location: ast.NewLocation(nil, "filters.rego", 4, 1),
Message: "unknowns must be prefixed with `input` or `data`: inpu.fruits",
},
{
Code: "invalid_unknown",
Location: ast.NewLocation(nil, "filters.rego", 4, 1),
Message: "unknowns must be prefixed with `input` or `data`: future.keywrds",
},
},
},
{
note: "bad metadata unknowns (no kludge)",
omitUnknowns: true,
rego: `
# METADATA
# custom:
# unknowns:
# - inpu.fruits
# - data.whatever
# - future.keywrds
include if input.fruits.colour == input.colour
`,
errors: []Error{
{
Code: "invalid_unknown",
Location: ast.NewLocation(nil, "filters.rego", 4, 1),
Message: "unknowns must be prefixed with `input` or `data`: inpu.fruits",
},
{
Code: "invalid_unknown",
Location: ast.NewLocation(nil, "filters.rego", 4, 1),
Message: "unknowns must be prefixed with `input` or `data`: future.keywrds",
},
},
},
{
note: "non-det builtin with known args: http.send",
rego: fmt.Sprintf(`include if input.fruits.yummy == http.send({"method": "POST", "url": "%s"}).body.p`, testserver.URL),
result: map[string]any{"type": "field", "field": "fruits.yummy", "operator": "eq", "value": true},
},
{
note: "non-det builtin with unknown args: http.send",
rego: fmt.Sprintf(`include if input.fruits.yummy == http.send({"method": "POST", "url": "%s", "body": input.fruits.taste}).body.p`, testserver.URL),
errors: []Error{
{
Code: "pe_fragment_error",
Message: "invalid builtin `http.send`",
Location: ast.NewLocation(nil, "filters.rego", 3, 34),
},
{
Code: "pe_fragment_error",
Message: "eq: invalid ref operand: __local0__1.body.p",
Location: ast.NewLocation(nil, "filters.rego", 3, 34),
},
},
},
{
note: "non-determistic builtin (arity 0): opa.runtime()",
rego: `include if {
some k, v in opa.runtime()
input.fruits[k] == v
}`,
result: map[string]any{
"type": "compound",
"operator": "or",
"value": []any{
map[string]any{"type": "field", "field": "fruits.foo", "operator": "eq", "value": "bar"},
map[string]any{"type": "field", "field": "fruits.fox", "operator": "eq", "value": float64(100)},
},
},
},
} {
t.Run(tc.note, func(t *testing.T) {
if tc.skip != "" {
t.Skip(tc.skip)
}
var unknowns []string
if !tc.omitUnknowns {
unknowns = defaultUnknowns
}
rego := "package filters\nimport rego.v1\n" + tc.rego
if tc.regoVerbatim {
rego = tc.rego
}
path := cmp.Or(tc.query, defaultPath)
input := cmp.Or(tc.input, any(defaultInput))
target := cmp.Or(tc.target, ucastAcceptHeader)
// second, query the compile API
payload := map[string]any{
"input": input,
"unknowns": unknowns,
"options": map[string]any{
"targetSQLTableMappings": map[string]any{
"ucast": tc.mappings,
},
},
}
jsonData, err := json.Marshal(payload)
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
f := setup(t, rego, nil)
req, err := http.NewRequest("POST", "/v1/compile/"+path, bytes.NewBuffer(jsonData))
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", target)
expCode := http.StatusOK
if len(tc.errors) > 0 {
expCode = http.StatusBadRequest
}
resp := map[string]any{}
if len(tc.errors) > 0 {
resp["errors"] = tc.errors
resp["code"] = "evaluation_error"
resp["message"] = "error(s) occurred while evaluating query"
}
if tc.result != nil {
resp["result"] = map[string]any{"query": tc.result}
}
expResp, _ := json.Marshal(resp)
if err := f.executeRequest(req, expCode, string(expResp)); err != nil {
t.Fatal(err)
}
})
}
}
var testserver = srv(func(w http.ResponseWriter, _ *http.Request) error {
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(map[string]any{
"p": true,
})
})
func srv(f func(http.ResponseWriter, *http.Request) error) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := f(w, r); err != nil {
w.WriteHeader(500)
fmt.Fprintln(w, err.Error())
}
}))
}
+394
View File
@@ -0,0 +1,394 @@
// Copyright 2025 The OPA Authors
// SPDX-License-Identifier: Apache-2.0
package server
import (
"bytes"
"context"
"encoding/json"
"fmt"
"maps"
"net/http"
"strings"
"testing"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/storage"
"github.com/open-policy-agent/opa/v1/storage/inmem"
)
type Query struct {
Query any `json:"query,omitempty"`
Masks any `json:"masks,omitempty"`
}
type Response struct {
Result struct {
Query any `json:"query,omitempty"`
Masks any `json:"masks,omitempty"`
UCAST Query `json:"ucast"` // NB: omitempty has no effect on nested struct fields (so the linter tells me)
Postgres Query `json:"postgresql"`
MySQL Query `json:"mysql"`
MSSQL Query `json:"sqlserver"`
SQLite Query `json:"sqlite"`
} `json:"result"`
Metrics map[string]float64 `json:"metrics"`
Hints []map[string]any `json:"hints"`
}
var ignoreMetrics = cmpopts.IgnoreMapEntries(func(k string, _ any) bool { return k == "metrics" })
func setup(t testing.TB, rego string, data any) *fixture {
ctx := context.Background()
store := inmem.New()
txn := storage.NewTransactionOrDie(ctx, store, storage.WriteParams)
if err := store.UpsertPolicy(ctx, txn, "filters.rego", []byte(rego)); err != nil {
t.Fatalf("upsert policy: %v", err)
}
if data != nil {
if err := store.Write(ctx, txn, storage.AddOp, storage.Path{}, data); err != nil {
t.Fatalf("write data: %v", err)
}
}
if err := store.Commit(ctx, txn); err != nil {
t.Fatalf("store policy: %v", err)
}
return newFixtureWithStore(t, store,
func(s *Server) {
_ = s.WithRuntime(ast.MustParseTerm(`{"foo": "bar", "fox": 100}`))
},
)
}
func TestCompileHandlerMultiTarget(t *testing.T) {
t.Parallel()
var roles map[string]any
if err := json.Unmarshal(rolesJSON, &roles); err != nil {
t.Fatalf("unmarshal roles: %v", err)
}
f := setup(t, string(benchRego), map[string]any{"roles": roles})
input := map[string]any{
"user": "caesar",
"tenant": map[string]any{
"id": 2,
"name": "acmecorp",
},
}
path := "filters/include"
target := "application/vnd.opa.multitarget+json"
payload := map[string]any{ // NB(sr): unknowns are taken from metadata
"input": input,
"options": map[string]any{
"targetDialects": []string{
"sql+postgresql",
"sql+mysql",
"sql+sqlserver",
"sql+sqlite",
"ucast+prisma",
},
},
}
expCode := http.StatusOK
expBody, _ := json.Marshal(map[string]any{
"result": map[string]any{
"postgresql": map[string]any{
"masks": map[string]any{"tickets": map[string]any{"id": map[string]any{"replace": map[string]any{"value": string("***")}}}},
"query": "WHERE ((tickets.tenant = E'2' AND users.name = E'caesar') OR (tickets.tenant = E'2' AND tickets.assignee IS NULL AND tickets.resolved = FALSE))",
},
"mysql": map[string]any{
"masks": map[string]any{"tickets": map[string]any{"id": map[string]any{"replace": map[string]any{"value": string("***")}}}},
"query": "WHERE ((tickets.tenant = '2' AND users.name = 'caesar') OR (tickets.tenant = '2' AND tickets.assignee IS NULL AND tickets.resolved = FALSE))",
},
"sqlserver": map[string]any{
"masks": map[string]any{"tickets": map[string]any{"id": map[string]any{"replace": map[string]any{"value": string("***")}}}},
"query": "WHERE ((tickets.tenant = N'2' AND users.name = N'caesar') OR (tickets.tenant = N'2' AND tickets.assignee IS NULL AND tickets.resolved = FALSE))",
},
"sqlite": map[string]any{
"masks": map[string]any{"tickets": map[string]any{"id": map[string]any{"replace": map[string]any{"value": string("***")}}}},
"query": "WHERE ((tickets.tenant = '2' AND users.name = 'caesar') OR (tickets.tenant = '2' AND tickets.assignee IS NULL AND tickets.resolved = FALSE))",
},
"ucast": map[string]any{
"masks": map[string]any{"tickets": map[string]any{"id": map[string]any{"replace": map[string]any{"value": string("***")}}}},
"query": map[string]any{
"operator": "or",
"type": "compound",
"value": []any{
map[string]any{
"operator": "and",
"type": "compound",
"value": []any{
map[string]any{"field": "tickets.tenant", "operator": "eq", "type": "field", "value": float64(2)},
map[string]any{"field": "users.name", "operator": "eq", "type": "field", "value": "caesar"},
},
},
map[string]any{
"operator": "and",
"type": "compound",
"value": []any{
map[string]any{"field": "tickets.tenant", "operator": "eq", "type": "field", "value": float64(2)},
map[string]any{"field": "tickets.assignee", "operator": "eq", "type": "field", "value": nil},
map[string]any{"field": "tickets.resolved", "operator": "eq", "type": "field", "value": false},
},
},
},
},
},
},
})
jsonData, err := json.Marshal(payload)
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
req, _ := http.NewRequest("POST", "/v1/compile/"+path, bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", target)
if err := f.executeRequest(req, expCode, string(expBody), ignoreMetrics); err != nil {
t.Error(err)
}
}
func TestCompileHandlerMetrics(t *testing.T) {
t.Parallel()
var roles map[string]any
if err := json.Unmarshal(rolesJSON, &roles); err != nil {
t.Fatalf("unmarshal roles: %v", err)
}
input := map[string]any{
"user": "caesar",
"tenant": map[string]any{
"id": 2,
"name": "acmecorp",
},
}
path := "filters/include"
targets := []string{
"application/vnd.opa.sql.postgresql+json",
"application/vnd.opa.ucast.prisma+json",
}
for _, target := range targets {
f := setup(t, string(benchRego), map[string]any{"roles": roles})
t.Run(strings.Split(target, "/")[1], func(t *testing.T) {
payload := map[string]any{ // NB(sr): unknowns+mask_rule are taken from metadata
"input": input,
}
{ // check metrics
req := evalReq(t, path, payload, target)
if err := f.executeRequest(req, http.StatusOK, ""); err != nil {
t.Fatal(err)
}
var resp Response
if err := json.NewDecoder(f.recorder.Result().Body).Decode(&resp); err != nil {
t.Error(err)
}
if exp, act := map[string]float64{
"timer_compile_eval_constraints_ns": 0,
"timer_compile_eval_mask_rule_ns": 0,
"timer_compile_extract_annotations_unknowns_ns": 0,
"timer_compile_extract_annotations_mask_ns": 0,
"timer_compile_prep_partial_ns": 0,
"timer_rego_external_resolve_ns": 0,
"timer_rego_partial_eval_ns": 0,
"timer_rego_query_compile_ns": 0,
"timer_rego_query_parse_ns": 0,
"timer_rego_query_eval_ns": 0,
"timer_server_handler_ns": 0,
"timer_compile_translate_queries_ns": 0,
}, resp.Metrics; !compareMetrics(exp, act) {
t.Fatalf("unexpected metrics: want %v, got %v", exp, act)
}
}
{ // Redo without resetting the cache: no extraction happens
req := evalReq(t, path, payload, target)
if err := f.executeRequest(req, http.StatusOK, ""); err != nil {
t.Fatal(err)
}
var resp Response
if err := json.NewDecoder(f.recorder.Result().Body).Decode(&resp); err != nil {
t.Error(err)
}
if n, ok := resp.Metrics["timer_compile_extract_annotations_unknowns_ns"]; ok {
t.Errorf("unexpected metric 'timer_compile_extract_annotations_unknowns_ns': %v", n)
}
if n, ok := resp.Metrics["timer_compile_extract_annotations_mask_ns"]; ok {
t.Errorf("unexpected metric 'timer_compile_extract_annotations_mask_ns': %v", n)
}
}
})
}
}
// compareMetrics only checks that the keys of `exp` and `act` are the same.
func compareMetrics(exp, act map[string]float64) bool {
return maps.EqualFunc(exp, act, func(_, _ float64) bool {
return true
})
}
func TestCompileHandlerHints(t *testing.T) {
t.Parallel()
typoRego := `package filters
# METADATA
# scope: document
# custom:
# unknowns: [input.fruits]
include if input.fruits.name == "apple"
include if input.fruit.cost < input.max
`
f := setup(t, typoRego, nil)
input := map[string]any{
"max": 1,
}
path := "filters/include"
target := "application/vnd.opa.sql.postgresql+json"
payload := map[string]any{ // NB(sr): unknowns are taken from metadata
"input": input,
}
req := evalReq(t, path, payload, target)
expCode := http.StatusOK
expResp := map[string]any{
"result": map[string]any{
"query": "WHERE fruits.name = E'apple'",
},
"hints": []map[string]any{
{
"location": map[string]any{
"col": float64(12),
"row": float64(7),
"file": "filters.rego",
},
"message": "input.fruit.cost undefined, did you mean input.fruits.cost?",
},
},
}
expBodyJSON, _ := json.Marshal(expResp)
if err := f.executeRequest(req, expCode, string(expBodyJSON), ignoreMetrics); err != nil {
t.Error(err)
}
}
func TestCompileHandlerMaskingRules(t *testing.T) {
t.Parallel()
var roles map[string]any
if err := json.Unmarshal(rolesJSON, &roles); err != nil {
t.Fatalf("unmarshal roles: %v", err)
}
input := map[string]any{
"user": "caesar",
"tenant": map[string]any{
"id": 2,
"name": "acmecorp",
},
}
path := "filters/include"
target := "application/vnd.opa.sql.postgresql+json"
t.Run("mask rule from payload parameter", func(t *testing.T) {
t.Parallel()
f := setup(t, string(benchRego), map[string]any{"roles": roles})
payload := map[string]any{ // NB(sr): unknowns are taken from metadata
"input": input,
"options": map[string]any{
"maskRule": "data.filters.masks",
},
}
req := evalReq(t, path, payload, target)
expBodyJSON, _ := json.Marshal(map[string]any{
"result": map[string]any{
"query": "WHERE ((tickets.tenant = E'2' AND users.name = E'caesar') OR (tickets.tenant = E'2' AND tickets.assignee IS NULL AND tickets.resolved = FALSE))",
"masks": map[string]any{"tickets": map[string]any{"description": map[string]any{"replace": map[string]any{"value": "***"}}}},
},
})
if err := f.executeRequest(req, http.StatusOK, string(expBodyJSON), ignoreMetrics); err != nil {
t.Error(err)
}
})
t.Run("mask rule from payload parameter + package-local matching", func(t *testing.T) {
t.Parallel()
f := setup(t, string(benchRego), map[string]any{"roles": roles})
payload := map[string]any{ // NB(sr): unknowns are taken from metadata
"input": input,
"options": map[string]any{
"maskRule": "masks",
},
}
req := evalReq(t, path, payload, target)
expBodyJSON, _ := json.Marshal(map[string]any{
"result": map[string]any{
"query": "WHERE ((tickets.tenant = E'2' AND users.name = E'caesar') OR (tickets.tenant = E'2' AND tickets.assignee IS NULL AND tickets.resolved = FALSE))",
"masks": map[string]any{"tickets": map[string]any{"description": map[string]any{"replace": map[string]any{"value": "***"}}}},
},
})
if err := f.executeRequest(req, http.StatusOK, string(expBodyJSON), ignoreMetrics); err != nil {
t.Error(err)
}
})
t.Run("mask rule from rule annotation", func(t *testing.T) {
t.Parallel()
f := setup(t, string(benchRego), map[string]any{"roles": roles})
payload := map[string]any{
"input": input,
}
req := evalReq(t, path, payload, target)
expBodyJSON, _ := json.Marshal(map[string]any{
"result": map[string]any{
"query": "WHERE ((tickets.tenant = E'2' AND users.name = E'caesar') OR (tickets.tenant = E'2' AND tickets.assignee IS NULL AND tickets.resolved = FALSE))",
"masks": map[string]any{"tickets": map[string]any{"id": map[string]any{"replace": map[string]any{"value": "***"}}}},
},
})
if err := f.executeRequest(req, http.StatusOK, string(expBodyJSON), ignoreMetrics); err != nil {
t.Error(err)
}
})
t.Run("mask rule from rule annotation + package-local matching", func(t *testing.T) {
t.Parallel()
// Mangle the mask_rule annotation to make it package-local:
benchRego := bytes.Replace(benchRego, []byte("mask_rule: data.filters.mask_from_annotation"), []byte("mask_rule: mask_from_annotation"), 1)
f := setup(t, string(benchRego), map[string]any{"roles": roles})
payload := map[string]any{
"input": input,
}
req := evalReq(t, path, payload, target)
expBodyJSON, _ := json.Marshal(map[string]any{
"result": map[string]any{
"query": "WHERE ((tickets.tenant = E'2' AND users.name = E'caesar') OR (tickets.tenant = E'2' AND tickets.assignee IS NULL AND tickets.resolved = FALSE))",
"masks": map[string]any{"tickets": map[string]any{"id": map[string]any{"replace": map[string]any{"value": "***"}}}},
},
})
if err := f.executeRequest(req, http.StatusOK, string(expBodyJSON), ignoreMetrics); err != nil {
t.Error(err)
}
})
}
func evalReq(t testing.TB, path string, payload map[string]any, target string) *http.Request {
t.Helper()
jsonData, err := json.Marshal(payload)
if err != nil {
t.Fatalf("Failed to marshal JSON: %v", err)
}
// CAVEAT(sr): We're using the httptest machinery to simulate a request, so the actual
// request path is ignored.
req, _ := http.NewRequest("POST", fmt.Sprintf("/v1/compile/%s?metrics=true", path), bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", target)
return req
}
+120
View File
@@ -0,0 +1,120 @@
package failtracer
import (
"fmt"
"slices"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/topdown"
"github.com/open-policy-agent/opa/internal/levenshtein"
)
const (
// maxDistanceForHint is the levenshtein distance below which we'll emit a hint
maxDistanceForHint = 3
)
type Hint struct {
Message string `json:"message"`
Location *ast.Location `json:"location,omitempty"`
}
type failTracer struct {
exprs []*ast.Expr
}
type FailTracer interface {
Enabled() bool
TraceEvent(topdown.Event)
Config() topdown.TraceConfig
Hints([]*ast.Term) []Hint
}
func New() FailTracer {
return &failTracer{}
}
// Enabled always returns true if the failTracer is instantiated.
func (b *failTracer) Enabled() bool {
return b != nil
}
func (b *failTracer) TraceEvent(evt topdown.Event) {
if evt.Op == topdown.FailOp {
expr, ok := evt.Node.(*ast.Expr)
if ok {
b.exprs = append(b.exprs, expr)
}
}
}
// Config returns the Tracers standard configuration
func (*failTracer) Config() topdown.TraceConfig {
return topdown.TraceConfig{PlugLocalVars: true}
}
func (b *failTracer) Hints(unknowns []*ast.Term) []Hint {
var hints []Hint //nolint:prealloc
seenRefs := map[string]struct{}{}
candidates := make([]string, 0, len(unknowns))
for i := range unknowns {
ref, ok := unknowns[i].Value.(ast.Ref)
if !ok || len(ref) < 2 {
continue
}
candidates = append(candidates, string(unknowns[i].Value.(ast.Ref)[1].Value.(ast.String)))
}
for _, expr := range b.exprs {
var ref ast.Ref // when this is processed, only one input.X.Y ref is in the expression (SSA)
switch {
case expr.IsCall():
for i := range 2 {
op := expr.Operand(i)
if r, ok := op.Value.(ast.Ref); ok && r.HasPrefix(ast.InputRootRef) {
ref = r
}
}
}
// NOTE(sr): if we allow naked ast.Term for filter policies, they need to be handled in switch ^
if len(ref) < 2 {
continue
}
tblPart, ok := ref[1].Value.(ast.String)
if !ok {
continue
}
miss := string(tblPart)
rs := ref[1:].String()
if _, ok := seenRefs[rs]; ok {
continue
}
closestStrings := levenshtein.ClosestStrings(maxDistanceForHint, miss, slices.Values(candidates))
proposals := make([]ast.Ref, len(closestStrings))
for i := range closestStrings {
prop := make([]*ast.Term, 2, len(ref))
prop[0] = ast.InputRootDocument
prop[1] = ast.StringTerm(closestStrings[i])
prop = append(prop, ref[2:]...)
proposals[i] = prop
}
var msg string
switch len(proposals) {
case 0:
continue
case 1:
msg = fmt.Sprintf("%v undefined, did you mean %s?", ref, proposals[0])
default:
msg = fmt.Sprintf("%v undefined, did you mean any of %v?", ref, proposals)
}
hints = append(hints, Hint{
Location: expr.Loc(),
Message: msg,
})
seenRefs[rs] = struct{}{}
}
return hints
}
+109
View File
@@ -0,0 +1,109 @@
// Copyright 2025 The OPA Authors
// SPDX-License-Identifier: Apache-2.0
package failtracer_test
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/server/failtracer"
"github.com/open-policy-agent/opa/v1/topdown"
)
func evtFromExpr(e string) topdown.Event {
return topdown.Event{
Op: topdown.FailOp,
Node: ast.MustParseExpr(e),
}
}
func TestHints(t *testing.T) {
for _, tc := range []struct {
note string
evts []topdown.Event
unknowns []string
exp []failtracer.Hint
}{
{
note: "simple typo, two-part ref",
evts: []topdown.Event{
evtFromExpr(`__local1__ = input.fruit.price`),
},
unknowns: []string{"input.fruits"},
exp: []failtracer.Hint{
{Message: "input.fruit.price undefined, did you mean input.fruits.price?"},
},
},
{
note: "all of input unknown, ignored",
evts: []topdown.Event{
evtFromExpr(`__local1__ = input.fruit.price`),
},
unknowns: []string{"input"},
exp: nil,
},
{
note: "simple typo, short ref",
evts: []topdown.Event{
evtFromExpr(`__local1__ = input.prize`),
},
unknowns: []string{"input.price"},
exp: []failtracer.Hint{
{Message: "input.prize undefined, did you mean input.price?"},
},
},
{
note: "large distance, no hint",
evts: []topdown.Event{
evtFromExpr(`__local1__ = input.fruit.price`),
},
unknowns: []string{"input.baskets"},
exp: nil,
},
{
note: "simple typo, multiple fail events",
evts: []topdown.Event{
evtFromExpr(`__local1__ = input.fruit.price`),
evtFromExpr(`__local2__ = input.fruit.colour`),
},
unknowns: []string{"input.fruits"},
exp: []failtracer.Hint{
{Message: "input.fruit.price undefined, did you mean input.fruits.price?"},
{Message: "input.fruit.colour undefined, did you mean input.fruits.colour?"},
},
},
{
note: "same typo, multiple fail events",
evts: []topdown.Event{
evtFromExpr(`__local1__ = input.fruit.price`),
evtFromExpr(`__local2__ = input.fruit.price`),
},
unknowns: []string{"input.fruits"},
exp: []failtracer.Hint{
{Message: "input.fruit.price undefined, did you mean input.fruits.price?"},
},
},
} {
t.Run(tc.note, func(t *testing.T) {
t.Parallel()
ft := failtracer.New()
for i := range tc.evts {
ft.TraceEvent(tc.evts[i])
}
unk := make([]*ast.Term, len(tc.unknowns))
for i := range tc.unknowns {
unk[i] = ast.MustParseTerm(tc.unknowns[i])
}
hints := ft.Hints(unk)
if diff := cmp.Diff(tc.exp, hints, cmpopts.IgnoreFields(failtracer.Hint{}, "Location")); diff != "" {
t.Errorf("unexpected hints (-want, +got):\n%s", diff)
}
})
}
}
+33 -21
View File
@@ -25,17 +25,16 @@ import (
"sync"
"time"
"github.com/open-policy-agent/opa/v1/hooks"
lru "github.com/hashicorp/golang-lru/v2"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"github.com/open-policy-agent/opa/internal/json/patch"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/bundle"
"github.com/open-policy-agent/opa/v1/hooks"
"github.com/open-policy-agent/opa/v1/logging"
"github.com/open-policy-agent/opa/v1/metrics"
"github.com/open-policy-agent/opa/v1/plugins"
@@ -153,6 +152,9 @@ type Server struct {
unixSocketPerm *string
cipherSuites *[]uint16
hooks hooks.Hooks
compileUnknownsCache *lru.Cache[string, []*ast.Term]
compileMaskingRulesCache *lru.Cache[string, ast.Ref]
}
// Metrics defines the interface that the server requires for recording HTTP
@@ -185,6 +187,8 @@ type Loop func() error
// New returns a new Server.
func New() *Server {
s := Server{}
s.compileUnknownsCache, _ = lru.New[string, []*ast.Term](unknownsCacheSize)
s.compileMaskingRulesCache, _ = lru.New[string, ast.Ref](maskingRuleCacheSize)
return &s
}
@@ -884,6 +888,8 @@ func (s *Server) initRouters(ctx context.Context) {
mainRouter.Handle("GET /v1/query", s.instrumentHandler(s.v1QueryGet, PromHandlerV1Query))
mainRouter.Handle("POST /v1/query", s.instrumentHandler(s.v1QueryPost, PromHandlerV1Query))
mainRouter.Handle("POST /v1/compile", s.instrumentHandler(s.v1CompilePost, PromHandlerV1Compile))
mainRouter.Handle("POST /v1/compile/{path...}", s.instrumentHandler(s.v1CompileFilters, PromHandlerV1Compile))
mainRouter.Handle("GET /v1/compile/{path...}", s.instrumentHandler(s.v1CompileFilters, PromHandlerV1Compile))
mainRouter.Handle("GET /v1/config", s.instrumentHandler(s.v1ConfigGet, PromHandlerV1Config))
mainRouter.Handle("GET /v1/status", s.instrumentHandler(s.v1StatusGet, PromHandlerV1Status))
mainRouter.Handle("POST /{$}", s.instrumentHandler(s.unversionedPost, PromHandlerIndex))
@@ -974,7 +980,7 @@ func (s *Server) execQuery(ctx context.Context, br bundleRevisions, txn storage.
output, err := rego.Eval(ctx)
if err != nil {
_ = logger.Log(ctx, txn, "", parsedQuery.String(), rawInput, input, nil, ndbCache, err, m)
_ = logger.Log(ctx, txn, "", parsedQuery.String(), rawInput, input, nil, ndbCache, err, m, nil)
return nil, err
}
@@ -991,7 +997,7 @@ func (s *Server) execQuery(ctx context.Context, br bundleRevisions, txn storage.
}
var x any = results.Result
if err := logger.Log(ctx, txn, "", parsedQuery.String(), rawInput, input, &x, ndbCache, nil, m); err != nil {
if err := logger.Log(ctx, txn, "", parsedQuery.String(), rawInput, input, &x, ndbCache, nil, m, nil); err != nil {
return nil, err
}
return &results, nil
@@ -1044,7 +1050,7 @@ func getRevisions(ctx context.Context, store storage.Store, txn storage.Transact
return br, nil
}
func (s *Server) reload(context.Context, storage.Transaction, storage.TriggerEvent) {
func (s *Server) reload(_ context.Context, _ storage.Transaction, evt storage.TriggerEvent) {
// NOTE(tsandall): We currently rely on the storage txn to provide
// critical sections in the server.
//
@@ -1056,6 +1062,10 @@ func (s *Server) reload(context.Context, storage.Transaction, storage.TriggerEve
s.partials = map[string]rego.PartialResult{}
s.preparedEvalQueries = newCache(pqMaxCacheSize)
s.defaultDecisionPath = s.generateDefaultDecisionPath()
if evt.PolicyChanged() {
s.compileUnknownsCache.Purge()
s.compileMaskingRulesCache.Purge()
}
}
func (s *Server) unversionedPost(w http.ResponseWriter, r *http.Request) {
@@ -1124,14 +1134,14 @@ func (s *Server) v0QueryPath(w http.ResponseWriter, r *http.Request, urlPath str
rego, err := s.makeRego(ctx, false, txn, input, urlPath, m, false, nil, opts)
if err != nil {
_ = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m)
_ = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m, nil)
writer.ErrorAuto(w, err)
return
}
pq, err := rego.PrepareForEval(ctx)
if err != nil {
_ = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m)
_ = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m, nil)
writer.ErrorAuto(w, err)
return
}
@@ -1157,7 +1167,7 @@ func (s *Server) v0QueryPath(w http.ResponseWriter, r *http.Request, urlPath str
// Handle results.
if err != nil {
_ = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m)
_ = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m, nil)
writer.ErrorAuto(w, err)
return
}
@@ -1174,7 +1184,7 @@ func (s *Server) v0QueryPath(w http.ResponseWriter, r *http.Request, urlPath str
messageType = types.MsgFoundUndefinedError
}
errV1 := types.NewErrorV1(types.CodeUndefinedDocument, "%v: %v", messageType, ref)
if err := logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, errV1, m); err != nil {
if err := logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, errV1, m, nil); err != nil {
writer.ErrorAuto(w, err)
return
}
@@ -1182,7 +1192,7 @@ func (s *Server) v0QueryPath(w http.ResponseWriter, r *http.Request, urlPath str
writer.Error(w, http.StatusNotFound, errV1)
return
}
err = logger.Log(ctx, txn, urlPath, "", goInput, input, &rs[0].Expressions[0].Value, ndbCache, nil, m)
err = logger.Log(ctx, txn, urlPath, "", goInput, input, &rs[0].Expressions[0].Value, ndbCache, nil, m, nil)
if err != nil {
writer.ErrorAuto(w, err)
return
@@ -1551,14 +1561,14 @@ func (s *Server) v1DataGet(w http.ResponseWriter, r *http.Request) {
rego, err := s.makeRego(ctx, strictBuiltinErrors, txn, input, urlPath, m, includeInstrumentation, buf, opts)
if err != nil {
_ = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m)
_ = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m, nil)
writer.ErrorAuto(w, err)
return
}
pq, err := rego.PrepareForEval(ctx)
if err != nil {
_ = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m)
_ = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m, nil)
writer.ErrorAuto(w, err)
return
}
@@ -1586,7 +1596,7 @@ func (s *Server) v1DataGet(w http.ResponseWriter, r *http.Request) {
// Handle results.
if err != nil {
_ = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m)
_ = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m, nil)
writer.ErrorAuto(w, err)
return
}
@@ -1612,7 +1622,7 @@ func (s *Server) v1DataGet(w http.ResponseWriter, r *http.Request) {
}
}
if err := logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, nil, m); err != nil {
if err := logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, nil, m, nil); err != nil {
writer.ErrorAuto(w, err)
return
}
@@ -1626,7 +1636,7 @@ func (s *Server) v1DataGet(w http.ResponseWriter, r *http.Request) {
result.Explanation = s.getExplainResponse(explainMode, *buf, pretty(r))
}
if err := logger.Log(ctx, txn, urlPath, "", goInput, input, result.Result, ndbCache, nil, m); err != nil {
if err := logger.Log(ctx, txn, urlPath, "", goInput, input, result.Result, ndbCache, nil, m, nil); err != nil {
writer.ErrorAuto(w, err)
return
}
@@ -1778,14 +1788,14 @@ func (s *Server) v1DataPost(w http.ResponseWriter, r *http.Request) {
rego, err := s.makeRego(ctx, strictBuiltinErrors, txn, input, urlPath, m, includeInstrumentation, buf, opts)
if err != nil {
_ = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m)
_ = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m, nil)
writer.ErrorAuto(w, err)
return
}
pq, err := rego.PrepareForEval(ctx)
if err != nil {
_ = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m)
_ = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m, nil)
writer.ErrorAuto(w, err)
return
}
@@ -1808,7 +1818,7 @@ func (s *Server) v1DataPost(w http.ResponseWriter, r *http.Request) {
// Handle results.
if err != nil {
_ = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m)
_ = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, err, m, nil)
writer.ErrorAuto(w, err)
return
}
@@ -1837,7 +1847,7 @@ func (s *Server) v1DataPost(w http.ResponseWriter, r *http.Request) {
return
}
}
if err = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, nil, m); err != nil {
if err = logger.Log(ctx, txn, urlPath, "", goInput, input, nil, ndbCache, nil, m, nil); err != nil {
writer.ErrorAuto(w, err)
return
}
@@ -1851,7 +1861,7 @@ func (s *Server) v1DataPost(w http.ResponseWriter, r *http.Request) {
result.Explanation = s.getExplainResponse(explainMode, *buf, pretty(r))
}
if err := logger.Log(ctx, txn, urlPath, "", goInput, input, result.Result, ndbCache, nil, m); err != nil {
if err := logger.Log(ctx, txn, urlPath, "", goInput, input, result.Result, ndbCache, nil, m, nil); err != nil {
writer.ErrorAuto(w, err)
return
}
@@ -3069,6 +3079,7 @@ func (l decisionLogger) Log(
ndbCache builtins.NDBCache,
err error,
m metrics.Metrics,
custom map[string]any,
) error {
if l.logger == nil {
return nil
@@ -3108,6 +3119,7 @@ func (l decisionLogger) Log(
Error: err,
Metrics: m,
RequestID: rctx.ReqID,
Custom: custom,
}
if ndbCache != nil {
+11 -22
View File
@@ -41,6 +41,7 @@ import (
"go.opentelemetry.io/otel/attribute"
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
"github.com/google/go-cmp/cmp"
"github.com/open-policy-agent/opa/internal/distributedtracing"
"github.com/open-policy-agent/opa/internal/prometheus"
"github.com/open-policy-agent/opa/v1/ast"
@@ -1834,7 +1835,6 @@ func TestConfigV1WithInvalidConfig(t *testing.T) {
f := &fixture{
server: server,
recorder: httptest.NewRecorder(),
t: t,
}
if err := f.v1(http.MethodGet, "/config", "", 500, `{
@@ -5253,7 +5253,6 @@ func TestQueryBindingIterationError(t *testing.T) {
f := &fixture{
server: server,
recorder: recorder,
t: t,
}
get := newReqV1(http.MethodGet, `/query?q=a=data.foo.bar`, "")
@@ -5288,7 +5287,6 @@ r contains x if { z[x] = 4 }`
type fixture struct {
server *Server
recorder *httptest.ResponseRecorder
t *testing.T
}
func newFixture(t *testing.T, opts ...any) *fixture {
@@ -5327,7 +5325,6 @@ func newFixture(t *testing.T, opts ...any) *fixture {
return &fixture{
server: server,
recorder: recorder,
t: t,
}
}
@@ -5357,11 +5354,10 @@ func newFixtureWithConfig(t *testing.T, config string, opts ...func(*Server)) *f
return &fixture{
server: server,
recorder: recorder,
t: t,
}
}
func newFixtureWithStore(t *testing.T, store storage.Store, opts ...any) *fixture {
func newFixtureWithStore(t testing.TB, store storage.Store, opts ...any) *fixture {
ctx := t.Context()
var mOpts []func(*plugins.Manager)
@@ -5400,7 +5396,6 @@ func newFixtureWithStore(t *testing.T, store storage.Store, opts ...any) *fixtur
return &fixture{
server: server,
recorder: recorder,
t: t,
}
}
@@ -5431,7 +5426,7 @@ func (f *fixture) v0(method string, path string, body string, code int, resp str
return f.executeRequest(newReqV0(method, path, body), code, resp)
}
func (f *fixture) executeRequestForHandler(h http.Handler, req *http.Request, code int, resp string) error {
func (f *fixture) executeRequestForHandler(h http.Handler, req *http.Request, code int, resp string, opts ...executeOpts) error {
f.reset()
h.ServeHTTP(f.recorder, req)
if f.recorder.Code != code {
@@ -5454,27 +5449,21 @@ func (f *fixture) executeRequestForHandler(h http.Handler, req *http.Request, co
if err := util.UnmarshalJSON([]byte(resp), &expected); err != nil {
panic(err)
}
if !reflect.DeepEqual(result, expected) {
a, err := json.MarshalIndent(expected, "", " ")
if err != nil {
panic(err)
}
b, err := json.MarshalIndent(result, "", " ")
if err != nil {
panic(err)
}
return fmt.Errorf("Expected JSON response from %v %v to equal:\n\n%s\n\nGot:\n\n%s", req.Method, req.URL, a, b)
if diff := cmp.Diff(expected, result, opts...); diff != "" {
return fmt.Errorf("unexpected JSON from %v %v (-want, +got):\n%s", req.Method, req.URL, diff)
}
}
return nil
}
func (f *fixture) executeRequest(req *http.Request, code int, resp string) error {
return f.executeRequestForHandler(f.server.Handler, req, code, resp)
type executeOpts = cmp.Option
func (f *fixture) executeRequest(req *http.Request, code int, resp string, opts ...executeOpts) error {
return f.executeRequestForHandler(f.server.Handler, req, code, resp, opts...)
}
func (f *fixture) executeDiagnosticRequest(req *http.Request, code int, resp string) error {
return f.executeRequestForHandler(f.server.DiagnosticHandler, req, code, resp)
func (f *fixture) executeDiagnosticRequest(req *http.Request, code int, resp string, opts ...executeOpts) error {
return f.executeRequestForHandler(f.server.DiagnosticHandler, req, code, resp, opts...)
}
func (f *fixture) reset() {
+60
View File
@@ -0,0 +1,60 @@
# METADATA
# scope: package
# custom:
# unknowns:
# - input.tickets
# - input.users
# mask_rule: data.filters.mask_from_annotation
package filters
tenancy if input.tickets.tenant == input.tenant.id # tenancy check
include if {
tenancy
resolver_include
}
include if {
tenancy
not user_is_resolver(input.user, input.tenant.name)
}
resolver_include if {
user_is_resolver(input.user, input.tenant.name)
# ticket is assigned to user
input.users.name == input.user
}
resolver_include if {
user_is_resolver(input.user, input.tenant.name)
# ticket is unassigned and unresolved
input.tickets.assignee == null
input.tickets.resolved == false
}
user_is_resolver(user, tenant) if "resolver" in data.roles[tenant][user] # regal ignore:external-reference
# Default-deny mask.
default masks.tickets.description := {"replace": {"value": "***"}}
# Allow viewing the field if user is an admin or a resolver.
masks.tickets.description := {} if {
"admin" in data.roles[input.tenant][input.user]
}
masks.tickets.description := {} if {
"resolver" in data.roles[input.tenant][input.user]
}
default mask_from_annotation.tickets.id := {"replace": {"value": "***"}}
# Allow viewing the field if user is an admin or a resolver.
mask_from_annotation.tickets.id := {} if {
"admin" in data.roles[input.tenant][input.user]
}
mask_from_annotation.tickets.id := {} if {
"resolver" in data.roles[input.tenant][input.user]
}
+10
View File
@@ -0,0 +1,10 @@
{
"acmecorp": {
"alice": ["admin"],
"bob": ["reader"],
"caesar": ["reader", "resolver"]
},
"hooli": {
"dylan": ["admin"]
}
}
+1
View File
@@ -1,3 +1,4 @@
// Package md2man aims in converting markdown into roff (man pages).
package md2man
import (
+7 -8
View File
@@ -47,13 +47,13 @@ const (
tableStart = "\n.TS\nallbox;\n"
tableEnd = ".TE\n"
tableCellStart = "T{\n"
tableCellEnd = "\nT}\n"
tableCellEnd = "\nT}"
tablePreprocessor = `'\" t`
)
// NewRoffRenderer creates a new blackfriday Renderer for generating roff documents
// from markdown
func NewRoffRenderer() *roffRenderer { // nolint: golint
func NewRoffRenderer() *roffRenderer {
return &roffRenderer{}
}
@@ -316,9 +316,8 @@ func (r *roffRenderer) handleTableCell(w io.Writer, node *blackfriday.Node, ente
} else if nodeLiteralSize(node) > 30 {
end = tableCellEnd
}
if node.Next == nil && end != tableCellEnd {
// Last cell: need to carriage return if we are at the end of the
// header row and content isn't wrapped in a "tablecell"
if node.Next == nil {
// Last cell: need to carriage return if we are at the end of the header row.
end += crTag
}
out(w, end)
@@ -356,7 +355,7 @@ func countColumns(node *blackfriday.Node) int {
}
func out(w io.Writer, output string) {
io.WriteString(w, output) // nolint: errcheck
io.WriteString(w, output) //nolint:errcheck
}
func escapeSpecialChars(w io.Writer, text []byte) {
@@ -395,7 +394,7 @@ func escapeSpecialCharsLine(w io.Writer, text []byte) {
i++
}
if i > org {
w.Write(text[org:i]) // nolint: errcheck
w.Write(text[org:i]) //nolint:errcheck
}
// escape a character
@@ -403,7 +402,7 @@ func escapeSpecialCharsLine(w io.Writer, text []byte) {
break
}
w.Write([]byte{'\\', text[i]}) // nolint: errcheck
w.Write([]byte{'\\', text[i]}) //nolint:errcheck
}
}
+12 -6
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"reflect"
"strings"
"sync"
"sync/atomic"
"unicode"
"unsafe"
@@ -17,22 +18,27 @@ var (
typeAddr *runtime.TypeAddr
cachedDecoderMap unsafe.Pointer // map[uintptr]decoder
cachedDecoder []Decoder
initOnce sync.Once
)
func init() {
typeAddr = runtime.AnalyzeTypeAddr()
if typeAddr == nil {
typeAddr = &runtime.TypeAddr{}
}
cachedDecoder = make([]Decoder, typeAddr.AddrRange>>typeAddr.AddrShift+1)
func initDecoder() {
initOnce.Do(func() {
typeAddr = runtime.AnalyzeTypeAddr()
if typeAddr == nil {
typeAddr = &runtime.TypeAddr{}
}
cachedDecoder = make([]Decoder, typeAddr.AddrRange>>typeAddr.AddrShift+1)
})
}
func loadDecoderMap() map[uintptr]Decoder {
initDecoder()
p := atomic.LoadPointer(&cachedDecoderMap)
return *(*map[uintptr]Decoder)(unsafe.Pointer(&p))
}
func storeDecoder(typ uintptr, dec Decoder, m map[uintptr]Decoder) {
initDecoder()
newDecoderMap := make(map[uintptr]Decoder, len(m)+1)
newDecoderMap[typ] = dec
+1
View File
@@ -10,6 +10,7 @@ import (
)
func CompileToGetDecoder(typ *runtime.Type) (Decoder, error) {
initDecoder()
typeptr := uintptr(unsafe.Pointer(typ))
if typeptr > typeAddr.MaxTypeAddr {
return compileToGetDecoderSlowPath(typeptr, typ)
+1
View File
@@ -13,6 +13,7 @@ import (
var decMu sync.RWMutex
func CompileToGetDecoder(typ *runtime.Type) (Decoder, error) {
initDecoder()
typeptr := uintptr(unsafe.Pointer(typ))
if typeptr > typeAddr.MaxTypeAddr {
return compileToGetDecoderSlowPath(typeptr, typ)
+10 -6
View File
@@ -5,6 +5,7 @@ import (
"encoding"
"encoding/json"
"reflect"
"sync"
"sync/atomic"
"unsafe"
@@ -24,14 +25,17 @@ var (
cachedOpcodeSets []*OpcodeSet
cachedOpcodeMap unsafe.Pointer // map[uintptr]*OpcodeSet
typeAddr *runtime.TypeAddr
initEncoderOnce sync.Once
)
func init() {
typeAddr = runtime.AnalyzeTypeAddr()
if typeAddr == nil {
typeAddr = &runtime.TypeAddr{}
}
cachedOpcodeSets = make([]*OpcodeSet, typeAddr.AddrRange>>typeAddr.AddrShift+1)
func initEncoder() {
initEncoderOnce.Do(func() {
typeAddr = runtime.AnalyzeTypeAddr()
if typeAddr == nil {
typeAddr = &runtime.TypeAddr{}
}
cachedOpcodeSets = make([]*OpcodeSet, typeAddr.AddrRange>>typeAddr.AddrShift+1)
})
}
func loadOpcodeMap() map[uintptr]*OpcodeSet {
+1
View File
@@ -4,6 +4,7 @@
package encoder
func CompileToGetCodeSet(ctx *RuntimeContext, typeptr uintptr) (*OpcodeSet, error) {
initEncoder()
if typeptr > typeAddr.MaxTypeAddr || typeptr < typeAddr.BaseTypeAddr {
codeSet, err := compileToGetCodeSetSlowPath(typeptr)
if err != nil {
+1
View File
@@ -10,6 +10,7 @@ import (
var setsMu sync.RWMutex
func CompileToGetCodeSet(ctx *RuntimeContext, typeptr uintptr) (*OpcodeSet, error) {
initEncoder()
if typeptr > typeAddr.MaxTypeAddr || typeptr < typeAddr.BaseTypeAddr {
codeSet, err := compileToGetCodeSetSlowPath(typeptr)
if err != nil {
+5
View File
@@ -406,6 +406,11 @@ func AppendMarshalJSON(ctx *RuntimeContext, code *Opcode, b []byte, v interface{
rv = newV
}
}
if rv.Kind() == reflect.Ptr && rv.IsNil() {
return AppendNull(ctx, b), nil
}
v = rv.Interface()
var bb []byte
if (code.Flags & MarshalerContextFlags) != 0 {
+53 -55
View File
@@ -2,6 +2,7 @@ package runtime
import (
"reflect"
"sync"
"unsafe"
)
@@ -23,8 +24,8 @@ type TypeAddr struct {
}
var (
typeAddr *TypeAddr
alreadyAnalyzed bool
typeAddr *TypeAddr
once sync.Once
)
//go:linkname typelinks reflect.typelinks
@@ -34,67 +35,64 @@ func typelinks() ([]unsafe.Pointer, [][]int32)
func rtypeOff(unsafe.Pointer, int32) unsafe.Pointer
func AnalyzeTypeAddr() *TypeAddr {
defer func() {
alreadyAnalyzed = true
}()
if alreadyAnalyzed {
return typeAddr
}
sections, offsets := typelinks()
if len(sections) != 1 {
return nil
}
if len(offsets) != 1 {
return nil
}
section := sections[0]
offset := offsets[0]
var (
min uintptr = uintptr(^uint(0))
max uintptr = 0
isAligned64 = true
isAligned32 = true
)
for i := 0; i < len(offset); i++ {
typ := (*Type)(rtypeOff(section, offset[i]))
addr := uintptr(unsafe.Pointer(typ))
if min > addr {
min = addr
once.Do(func() {
sections, offsets := typelinks()
if len(sections) != 1 {
return
}
if max < addr {
max = addr
if len(offsets) != 1 {
return
}
if typ.Kind() == reflect.Ptr {
addr = uintptr(unsafe.Pointer(typ.Elem()))
section := sections[0]
offset := offsets[0]
var (
min uintptr = uintptr(^uint(0))
max uintptr = 0
isAligned64 = true
isAligned32 = true
)
for i := 0; i < len(offset); i++ {
typ := (*Type)(rtypeOff(section, offset[i]))
addr := uintptr(unsafe.Pointer(typ))
if min > addr {
min = addr
}
if max < addr {
max = addr
}
if typ.Kind() == reflect.Ptr {
addr = uintptr(unsafe.Pointer(typ.Elem()))
if min > addr {
min = addr
}
if max < addr {
max = addr
}
}
isAligned64 = isAligned64 && (addr-min)&63 == 0
isAligned32 = isAligned32 && (addr-min)&31 == 0
}
isAligned64 = isAligned64 && (addr-min)&63 == 0
isAligned32 = isAligned32 && (addr-min)&31 == 0
}
addrRange := max - min
if addrRange == 0 {
return nil
}
var addrShift uintptr
if isAligned64 {
addrShift = 6
} else if isAligned32 {
addrShift = 5
}
cacheSize := addrRange >> addrShift
if cacheSize > maxAcceptableTypeAddrRange {
return nil
}
typeAddr = &TypeAddr{
BaseTypeAddr: min,
MaxTypeAddr: max,
AddrRange: addrRange,
AddrShift: addrShift,
}
addrRange := max - min
if addrRange == 0 {
return
}
var addrShift uintptr
if isAligned64 {
addrShift = 6
} else if isAligned32 {
addrShift = 5
}
cacheSize := addrRange >> addrShift
if cacheSize > maxAcceptableTypeAddrRange {
return
}
typeAddr = &TypeAddr{
BaseTypeAddr: min,
MaxTypeAddr: max,
AddrRange: addrRange,
AddrShift: addrShift,
}
})
return typeAddr
}
+23
View File
@@ -0,0 +1,23 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
+46
View File
@@ -0,0 +1,46 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0
linters:
fast: false
disable-all: true
enable:
- revive
- megacheck
- govet
- unconvert
- gas
- gocyclo
- dupl
- misspell
- unparam
- unused
- typecheck
- ineffassign
# - stylecheck
- exportloopref
- gocritic
- nakedret
- gosimple
- prealloc
# golangci-lint configuration file
linters-settings:
revive:
ignore-generated-header: true
severity: warning
rules:
- name: package-comments
severity: warning
disabled: true
- name: exported
severity: warning
disabled: false
arguments: ["checkPrivateReceivers", "disableStutteringCheck"]
issues:
exclude-use-default: false
exclude-rules:
- path: _test\.go
linters:
- dupl
+267
View File
@@ -0,0 +1,267 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lru
import (
"errors"
"sync"
"github.com/hashicorp/golang-lru/v2/simplelru"
)
const (
// Default2QRecentRatio is the ratio of the 2Q cache dedicated
// to recently added entries that have only been accessed once.
Default2QRecentRatio = 0.25
// Default2QGhostEntries is the default ratio of ghost
// entries kept to track entries recently evicted
Default2QGhostEntries = 0.50
)
// TwoQueueCache is a thread-safe fixed size 2Q cache.
// 2Q is an enhancement over the standard LRU cache
// in that it tracks both frequently and recently used
// entries separately. This avoids a burst in access to new
// entries from evicting frequently used entries. It adds some
// additional tracking overhead to the standard LRU cache, and is
// computationally about 2x the cost, and adds some metadata over
// head. The ARCCache is similar, but does not require setting any
// parameters.
type TwoQueueCache[K comparable, V any] struct {
size int
recentSize int
recentRatio float64
ghostRatio float64
recent simplelru.LRUCache[K, V]
frequent simplelru.LRUCache[K, V]
recentEvict simplelru.LRUCache[K, struct{}]
lock sync.RWMutex
}
// New2Q creates a new TwoQueueCache using the default
// values for the parameters.
func New2Q[K comparable, V any](size int) (*TwoQueueCache[K, V], error) {
return New2QParams[K, V](size, Default2QRecentRatio, Default2QGhostEntries)
}
// New2QParams creates a new TwoQueueCache using the provided
// parameter values.
func New2QParams[K comparable, V any](size int, recentRatio, ghostRatio float64) (*TwoQueueCache[K, V], error) {
if size <= 0 {
return nil, errors.New("invalid size")
}
if recentRatio < 0.0 || recentRatio > 1.0 {
return nil, errors.New("invalid recent ratio")
}
if ghostRatio < 0.0 || ghostRatio > 1.0 {
return nil, errors.New("invalid ghost ratio")
}
// Determine the sub-sizes
recentSize := int(float64(size) * recentRatio)
evictSize := int(float64(size) * ghostRatio)
// Allocate the LRUs
recent, err := simplelru.NewLRU[K, V](size, nil)
if err != nil {
return nil, err
}
frequent, err := simplelru.NewLRU[K, V](size, nil)
if err != nil {
return nil, err
}
recentEvict, err := simplelru.NewLRU[K, struct{}](evictSize, nil)
if err != nil {
return nil, err
}
// Initialize the cache
c := &TwoQueueCache[K, V]{
size: size,
recentSize: recentSize,
recentRatio: recentRatio,
ghostRatio: ghostRatio,
recent: recent,
frequent: frequent,
recentEvict: recentEvict,
}
return c, nil
}
// Get looks up a key's value from the cache.
func (c *TwoQueueCache[K, V]) Get(key K) (value V, ok bool) {
c.lock.Lock()
defer c.lock.Unlock()
// Check if this is a frequent value
if val, ok := c.frequent.Get(key); ok {
return val, ok
}
// If the value is contained in recent, then we
// promote it to frequent
if val, ok := c.recent.Peek(key); ok {
c.recent.Remove(key)
c.frequent.Add(key, val)
return val, ok
}
// No hit
return
}
// Add adds a value to the cache.
func (c *TwoQueueCache[K, V]) Add(key K, value V) {
c.lock.Lock()
defer c.lock.Unlock()
// Check if the value is frequently used already,
// and just update the value
if c.frequent.Contains(key) {
c.frequent.Add(key, value)
return
}
// Check if the value is recently used, and promote
// the value into the frequent list
if c.recent.Contains(key) {
c.recent.Remove(key)
c.frequent.Add(key, value)
return
}
// If the value was recently evicted, add it to the
// frequently used list
if c.recentEvict.Contains(key) {
c.ensureSpace(true)
c.recentEvict.Remove(key)
c.frequent.Add(key, value)
return
}
// Add to the recently seen list
c.ensureSpace(false)
c.recent.Add(key, value)
}
// ensureSpace is used to ensure we have space in the cache
func (c *TwoQueueCache[K, V]) ensureSpace(recentEvict bool) {
// If we have space, nothing to do
recentLen := c.recent.Len()
freqLen := c.frequent.Len()
if recentLen+freqLen < c.size {
return
}
// If the recent buffer is larger than
// the target, evict from there
if recentLen > 0 && (recentLen > c.recentSize || (recentLen == c.recentSize && !recentEvict)) {
k, _, _ := c.recent.RemoveOldest()
c.recentEvict.Add(k, struct{}{})
return
}
// Remove from the frequent list otherwise
c.frequent.RemoveOldest()
}
// Len returns the number of items in the cache.
func (c *TwoQueueCache[K, V]) Len() int {
c.lock.RLock()
defer c.lock.RUnlock()
return c.recent.Len() + c.frequent.Len()
}
// Resize changes the cache size.
func (c *TwoQueueCache[K, V]) Resize(size int) (evicted int) {
c.lock.Lock()
defer c.lock.Unlock()
// Recalculate the sub-sizes
recentSize := int(float64(size) * c.recentRatio)
evictSize := int(float64(size) * c.ghostRatio)
c.size = size
c.recentSize = recentSize
// ensureSpace
diff := c.recent.Len() + c.frequent.Len() - size
if diff < 0 {
diff = 0
}
for i := 0; i < diff; i++ {
c.ensureSpace(true)
}
// Reallocate the LRUs
c.recent.Resize(size)
c.frequent.Resize(size)
c.recentEvict.Resize(evictSize)
return diff
}
// Keys returns a slice of the keys in the cache.
// The frequently used keys are first in the returned slice.
func (c *TwoQueueCache[K, V]) Keys() []K {
c.lock.RLock()
defer c.lock.RUnlock()
k1 := c.frequent.Keys()
k2 := c.recent.Keys()
return append(k1, k2...)
}
// Values returns a slice of the values in the cache.
// The frequently used values are first in the returned slice.
func (c *TwoQueueCache[K, V]) Values() []V {
c.lock.RLock()
defer c.lock.RUnlock()
v1 := c.frequent.Values()
v2 := c.recent.Values()
return append(v1, v2...)
}
// Remove removes the provided key from the cache.
func (c *TwoQueueCache[K, V]) Remove(key K) {
c.lock.Lock()
defer c.lock.Unlock()
if c.frequent.Remove(key) {
return
}
if c.recent.Remove(key) {
return
}
if c.recentEvict.Remove(key) {
return
}
}
// Purge is used to completely clear the cache.
func (c *TwoQueueCache[K, V]) Purge() {
c.lock.Lock()
defer c.lock.Unlock()
c.recent.Purge()
c.frequent.Purge()
c.recentEvict.Purge()
}
// Contains is used to check if the cache contains a key
// without updating recency or frequency.
func (c *TwoQueueCache[K, V]) Contains(key K) bool {
c.lock.RLock()
defer c.lock.RUnlock()
return c.frequent.Contains(key) || c.recent.Contains(key)
}
// Peek is used to inspect the cache value of a key
// without updating recency or frequency.
func (c *TwoQueueCache[K, V]) Peek(key K) (value V, ok bool) {
c.lock.RLock()
defer c.lock.RUnlock()
if val, ok := c.frequent.Peek(key); ok {
return val, ok
}
return c.recent.Peek(key)
}
+364
View File
@@ -0,0 +1,364 @@
Copyright (c) 2014 HashiCorp, Inc.
Mozilla Public License, version 2.0
1. Definitions
1.1. "Contributor"
means each individual or legal entity that creates, contributes to the
creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used by a
Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached the
notice in Exhibit A, the Executable Form of such Source Code Form, and
Modifications of such Source Code Form, in each case including portions
thereof.
1.5. "Incompatible With Secondary Licenses"
means
a. that the initial Contributor has attached the notice described in
Exhibit B to the Covered Software; or
b. that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the terms of
a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in a
separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible, whether
at the time of the initial grant or subsequently, any and all of the
rights conveyed by this License.
1.10. "Modifications"
means any of the following:
a. any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered Software; or
b. any new file in Source Code Form that contains any Covered Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the License,
by the making, using, selling, offering for sale, having made, import,
or transfer of either its Contributions or its Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU Lesser
General Public License, Version 2.1, the GNU Affero General Public
License, Version 3.0, or any later versions of those licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that controls, is
controlled by, or is under common control with You. For purposes of this
definition, "control" means (a) the power, direct or indirect, to cause
the direction or management of such entity, whether by contract or
otherwise, or (b) ownership of more than fifty percent (50%) of the
outstanding shares or beneficial ownership of such entity.
2. License Grants and Conditions
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
a. under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
b. under Patent Claims of such Contributor to make, use, sell, offer for
sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
a. for any code that a Contributor has removed from Covered Software; or
b. for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
c. under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights to
grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
Section 2.1.
3. Responsibilities
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
a. such Covered Software must also be made available in Source Code Form,
as described in Section 3.1, and You must inform recipients of the
Executable Form how they can obtain a copy of such Source Code Form by
reasonable means in a timely manner, at a charge no more than the cost
of distribution to the recipient; and
b. You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter the
recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty, or
limitations of liability) contained within the Source Code Form of the
Covered Software, except that You may alter any license notices to the
extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
If it is impossible for You to comply with any of the terms of this License
with respect to some or all of the Covered Software due to statute,
judicial order, or regulation then You must: (a) comply with the terms of
this License to the maximum extent possible; and (b) describe the
limitations and the code they affect. Such description must be placed in a
text file included with all distributions of the Covered Software under
this License. Except to the extent prohibited by statute or regulation,
such description must be sufficiently detailed for a recipient of ordinary
skill to be able to understand it.
5. Termination
5.1. The rights granted under this License will terminate automatically if You
fail to comply with any of its terms. However, if You become compliant,
then the rights granted under this License from a particular Contributor
are reinstated (a) provisionally, unless and until such Contributor
explicitly and finally terminates Your grants, and (b) on an ongoing
basis, if such Contributor fails to notify You of the non-compliance by
some reasonable means prior to 60 days after You have come back into
compliance. Moreover, Your grants from a particular Contributor are
reinstated on an ongoing basis if such Contributor notifies You of the
non-compliance by some reasonable means, this is the first time You have
received notice of non-compliance with this License from such
Contributor, and You become compliant prior to 30 days after Your receipt
of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
license agreements (excluding distributors and resellers) which have been
validly granted by You or Your distributors under this License prior to
termination shall survive termination.
6. Disclaimer of Warranty
Covered Software is provided under this License on an "as is" basis,
without warranty of any kind, either expressed, implied, or statutory,
including, without limitation, warranties that the Covered Software is free
of defects, merchantable, fit for a particular purpose or non-infringing.
The entire risk as to the quality and performance of the Covered Software
is with You. Should any Covered Software prove defective in any respect,
You (not any Contributor) assume the cost of any necessary servicing,
repair, or correction. This disclaimer of warranty constitutes an essential
part of this License. No use of any Covered Software is authorized under
this License except under this disclaimer.
7. Limitation of Liability
Under no circumstances and under no legal theory, whether tort (including
negligence), contract, or otherwise, shall any Contributor, or anyone who
distributes Covered Software as permitted above, be liable to You for any
direct, indirect, special, incidental, or consequential damages of any
character including, without limitation, damages for lost profits, loss of
goodwill, work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses, even if such party shall have been
informed of the possibility of such damages. This limitation of liability
shall not apply to liability for death or personal injury resulting from
such party's negligence to the extent applicable law prohibits such
limitation. Some jurisdictions do not allow the exclusion or limitation of
incidental or consequential damages, so this exclusion and limitation may
not apply to You.
8. Litigation
Any litigation relating to this License may be brought only in the courts
of a jurisdiction where the defendant maintains its principal place of
business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions. Nothing
in this Section shall prevent a party's ability to bring cross-claims or
counter-claims.
9. Miscellaneous
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides that
the language of a contract shall be construed against the drafter shall not
be used to construe this License against a Contributor.
10. Versions of the License
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses If You choose to distribute Source Code Form that is
Incompatible With Secondary Licenses under the terms of this version of
the License, the notice described in Exhibit B of this License must be
attached.
Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the
terms of the Mozilla Public License, v.
2.0. If a copy of the MPL was not
distributed with this file, You can
obtain one at
http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular file,
then You may include the notice in a location (such as a LICENSE file in a
relevant directory) where a recipient would be likely to look for such a
notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
This Source Code Form is "Incompatible
With Secondary Licenses", as defined by
the Mozilla Public License, v. 2.0.
+79
View File
@@ -0,0 +1,79 @@
golang-lru
==========
This provides the `lru` package which implements a fixed-size
thread safe LRU cache. It is based on the cache in Groupcache.
Documentation
=============
Full docs are available on [Go Packages](https://pkg.go.dev/github.com/hashicorp/golang-lru/v2)
LRU cache example
=================
```go
package main
import (
"fmt"
"github.com/hashicorp/golang-lru/v2"
)
func main() {
l, _ := lru.New[int, any](128)
for i := 0; i < 256; i++ {
l.Add(i, nil)
}
if l.Len() != 128 {
panic(fmt.Sprintf("bad len: %v", l.Len()))
}
}
```
Expirable LRU cache example
===========================
```go
package main
import (
"fmt"
"time"
"github.com/hashicorp/golang-lru/v2/expirable"
)
func main() {
// make cache with 10ms TTL and 5 max keys
cache := expirable.NewLRU[string, string](5, nil, time.Millisecond*10)
// set value under key1.
cache.Add("key1", "val1")
// get value under key1
r, ok := cache.Get("key1")
// check for OK value
if ok {
fmt.Printf("value before expiration is found: %v, value: %q\n", ok, r)
}
// wait for cache to expire
time.Sleep(time.Millisecond * 12)
// get value under key1 after key expiration
r, ok = cache.Get("key1")
fmt.Printf("value after expiration is found: %v, value: %q\n", ok, r)
// set value under key2, would evict old entry because it is already expired.
cache.Add("key2", "val2")
fmt.Printf("Cache len: %d\n", cache.Len())
// Output:
// value before expiration is found: true, value: "val1"
// value after expiration is found: false, value: ""
// Cache len: 1
}
```
+24
View File
@@ -0,0 +1,24 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Package lru provides three different LRU caches of varying sophistication.
//
// Cache is a simple LRU cache. It is based on the LRU implementation in
// groupcache: https://github.com/golang/groupcache/tree/master/lru
//
// TwoQueueCache tracks frequently used and recently used entries separately.
// This avoids a burst of accesses from taking out frequently used entries, at
// the cost of about 2x computational overhead and some extra bookkeeping.
//
// ARCCache is an adaptive replacement cache. It tracks recent evictions as well
// as recent usage in both the frequent and recent caches. Its computational
// overhead is comparable to TwoQueueCache, but the memory overhead is linear
// with the size of the cache.
//
// ARC has been patented by IBM, so do not use it if that is problematic for
// your program. For this reason, it is in a separate go module contained within
// this repository.
//
// All caches in this package take locks while operating, and are therefore
// thread-safe for consumers.
package lru
+142
View File
@@ -0,0 +1,142 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE_list file.
package internal
import "time"
// Entry is an LRU Entry
type Entry[K comparable, V any] struct {
// Next and previous pointers in the doubly-linked list of elements.
// To simplify the implementation, internally a list l is implemented
// as a ring, such that &l.root is both the next element of the last
// list element (l.Back()) and the previous element of the first list
// element (l.Front()).
next, prev *Entry[K, V]
// The list to which this element belongs.
list *LruList[K, V]
// The LRU Key of this element.
Key K
// The Value stored with this element.
Value V
// The time this element would be cleaned up, optional
ExpiresAt time.Time
// The expiry bucket item was put in, optional
ExpireBucket uint8
}
// PrevEntry returns the previous list element or nil.
func (e *Entry[K, V]) PrevEntry() *Entry[K, V] {
if p := e.prev; e.list != nil && p != &e.list.root {
return p
}
return nil
}
// LruList represents a doubly linked list.
// The zero Value for LruList is an empty list ready to use.
type LruList[K comparable, V any] struct {
root Entry[K, V] // sentinel list element, only &root, root.prev, and root.next are used
len int // current list Length excluding (this) sentinel element
}
// Init initializes or clears list l.
func (l *LruList[K, V]) Init() *LruList[K, V] {
l.root.next = &l.root
l.root.prev = &l.root
l.len = 0
return l
}
// NewList returns an initialized list.
func NewList[K comparable, V any]() *LruList[K, V] { return new(LruList[K, V]).Init() }
// Length returns the number of elements of list l.
// The complexity is O(1).
func (l *LruList[K, V]) Length() int { return l.len }
// Back returns the last element of list l or nil if the list is empty.
func (l *LruList[K, V]) Back() *Entry[K, V] {
if l.len == 0 {
return nil
}
return l.root.prev
}
// lazyInit lazily initializes a zero List Value.
func (l *LruList[K, V]) lazyInit() {
if l.root.next == nil {
l.Init()
}
}
// insert inserts e after at, increments l.len, and returns e.
func (l *LruList[K, V]) insert(e, at *Entry[K, V]) *Entry[K, V] {
e.prev = at
e.next = at.next
e.prev.next = e
e.next.prev = e
e.list = l
l.len++
return e
}
// insertValue is a convenience wrapper for insert(&Entry{Value: v, ExpiresAt: ExpiresAt}, at).
func (l *LruList[K, V]) insertValue(k K, v V, expiresAt time.Time, at *Entry[K, V]) *Entry[K, V] {
return l.insert(&Entry[K, V]{Value: v, Key: k, ExpiresAt: expiresAt}, at)
}
// Remove removes e from its list, decrements l.len
func (l *LruList[K, V]) Remove(e *Entry[K, V]) V {
e.prev.next = e.next
e.next.prev = e.prev
e.next = nil // avoid memory leaks
e.prev = nil // avoid memory leaks
e.list = nil
l.len--
return e.Value
}
// move moves e to next to at.
func (l *LruList[K, V]) move(e, at *Entry[K, V]) {
if e == at {
return
}
e.prev.next = e.next
e.next.prev = e.prev
e.prev = at
e.next = at.next
e.prev.next = e
e.next.prev = e
}
// PushFront inserts a new element e with value v at the front of list l and returns e.
func (l *LruList[K, V]) PushFront(k K, v V) *Entry[K, V] {
l.lazyInit()
return l.insertValue(k, v, time.Time{}, &l.root)
}
// PushFrontExpirable inserts a new expirable element e with Value v at the front of list l and returns e.
func (l *LruList[K, V]) PushFrontExpirable(k K, v V, expiresAt time.Time) *Entry[K, V] {
l.lazyInit()
return l.insertValue(k, v, expiresAt, &l.root)
}
// MoveToFront moves element e to the front of list l.
// If e is not an element of l, the list is not modified.
// The element must not be nil.
func (l *LruList[K, V]) MoveToFront(e *Entry[K, V]) {
if e.list != l || l.root.next == e {
return
}
// see comment in List.Remove about initialization of l
l.move(e, &l.root)
}
+250
View File
@@ -0,0 +1,250 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package lru
import (
"sync"
"github.com/hashicorp/golang-lru/v2/simplelru"
)
const (
// DefaultEvictedBufferSize defines the default buffer size to store evicted key/val
DefaultEvictedBufferSize = 16
)
// Cache is a thread-safe fixed size LRU cache.
type Cache[K comparable, V any] struct {
lru *simplelru.LRU[K, V]
evictedKeys []K
evictedVals []V
onEvictedCB func(k K, v V)
lock sync.RWMutex
}
// New creates an LRU of the given size.
func New[K comparable, V any](size int) (*Cache[K, V], error) {
return NewWithEvict[K, V](size, nil)
}
// NewWithEvict constructs a fixed size cache with the given eviction
// callback.
func NewWithEvict[K comparable, V any](size int, onEvicted func(key K, value V)) (c *Cache[K, V], err error) {
// create a cache with default settings
c = &Cache[K, V]{
onEvictedCB: onEvicted,
}
if onEvicted != nil {
c.initEvictBuffers()
onEvicted = c.onEvicted
}
c.lru, err = simplelru.NewLRU(size, onEvicted)
return
}
func (c *Cache[K, V]) initEvictBuffers() {
c.evictedKeys = make([]K, 0, DefaultEvictedBufferSize)
c.evictedVals = make([]V, 0, DefaultEvictedBufferSize)
}
// onEvicted save evicted key/val and sent in externally registered callback
// outside of critical section
func (c *Cache[K, V]) onEvicted(k K, v V) {
c.evictedKeys = append(c.evictedKeys, k)
c.evictedVals = append(c.evictedVals, v)
}
// Purge is used to completely clear the cache.
func (c *Cache[K, V]) Purge() {
var ks []K
var vs []V
c.lock.Lock()
c.lru.Purge()
if c.onEvictedCB != nil && len(c.evictedKeys) > 0 {
ks, vs = c.evictedKeys, c.evictedVals
c.initEvictBuffers()
}
c.lock.Unlock()
// invoke callback outside of critical section
if c.onEvictedCB != nil {
for i := 0; i < len(ks); i++ {
c.onEvictedCB(ks[i], vs[i])
}
}
}
// Add adds a value to the cache. Returns true if an eviction occurred.
func (c *Cache[K, V]) Add(key K, value V) (evicted bool) {
var k K
var v V
c.lock.Lock()
evicted = c.lru.Add(key, value)
if c.onEvictedCB != nil && evicted {
k, v = c.evictedKeys[0], c.evictedVals[0]
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
}
c.lock.Unlock()
if c.onEvictedCB != nil && evicted {
c.onEvictedCB(k, v)
}
return
}
// Get looks up a key's value from the cache.
func (c *Cache[K, V]) Get(key K) (value V, ok bool) {
c.lock.Lock()
value, ok = c.lru.Get(key)
c.lock.Unlock()
return value, ok
}
// Contains checks if a key is in the cache, without updating the
// recent-ness or deleting it for being stale.
func (c *Cache[K, V]) Contains(key K) bool {
c.lock.RLock()
containKey := c.lru.Contains(key)
c.lock.RUnlock()
return containKey
}
// Peek returns the key value (or undefined if not found) without updating
// the "recently used"-ness of the key.
func (c *Cache[K, V]) Peek(key K) (value V, ok bool) {
c.lock.RLock()
value, ok = c.lru.Peek(key)
c.lock.RUnlock()
return value, ok
}
// ContainsOrAdd checks if a key is in the cache without updating the
// recent-ness or deleting it for being stale, and if not, adds the value.
// Returns whether found and whether an eviction occurred.
func (c *Cache[K, V]) ContainsOrAdd(key K, value V) (ok, evicted bool) {
var k K
var v V
c.lock.Lock()
if c.lru.Contains(key) {
c.lock.Unlock()
return true, false
}
evicted = c.lru.Add(key, value)
if c.onEvictedCB != nil && evicted {
k, v = c.evictedKeys[0], c.evictedVals[0]
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
}
c.lock.Unlock()
if c.onEvictedCB != nil && evicted {
c.onEvictedCB(k, v)
}
return false, evicted
}
// PeekOrAdd checks if a key is in the cache without updating the
// recent-ness or deleting it for being stale, and if not, adds the value.
// Returns whether found and whether an eviction occurred.
func (c *Cache[K, V]) PeekOrAdd(key K, value V) (previous V, ok, evicted bool) {
var k K
var v V
c.lock.Lock()
previous, ok = c.lru.Peek(key)
if ok {
c.lock.Unlock()
return previous, true, false
}
evicted = c.lru.Add(key, value)
if c.onEvictedCB != nil && evicted {
k, v = c.evictedKeys[0], c.evictedVals[0]
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
}
c.lock.Unlock()
if c.onEvictedCB != nil && evicted {
c.onEvictedCB(k, v)
}
return
}
// Remove removes the provided key from the cache.
func (c *Cache[K, V]) Remove(key K) (present bool) {
var k K
var v V
c.lock.Lock()
present = c.lru.Remove(key)
if c.onEvictedCB != nil && present {
k, v = c.evictedKeys[0], c.evictedVals[0]
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
}
c.lock.Unlock()
if c.onEvictedCB != nil && present {
c.onEvictedCB(k, v)
}
return
}
// Resize changes the cache size.
func (c *Cache[K, V]) Resize(size int) (evicted int) {
var ks []K
var vs []V
c.lock.Lock()
evicted = c.lru.Resize(size)
if c.onEvictedCB != nil && evicted > 0 {
ks, vs = c.evictedKeys, c.evictedVals
c.initEvictBuffers()
}
c.lock.Unlock()
if c.onEvictedCB != nil && evicted > 0 {
for i := 0; i < len(ks); i++ {
c.onEvictedCB(ks[i], vs[i])
}
}
return evicted
}
// RemoveOldest removes the oldest item from the cache.
func (c *Cache[K, V]) RemoveOldest() (key K, value V, ok bool) {
var k K
var v V
c.lock.Lock()
key, value, ok = c.lru.RemoveOldest()
if c.onEvictedCB != nil && ok {
k, v = c.evictedKeys[0], c.evictedVals[0]
c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0]
}
c.lock.Unlock()
if c.onEvictedCB != nil && ok {
c.onEvictedCB(k, v)
}
return
}
// GetOldest returns the oldest entry
func (c *Cache[K, V]) GetOldest() (key K, value V, ok bool) {
c.lock.RLock()
key, value, ok = c.lru.GetOldest()
c.lock.RUnlock()
return
}
// Keys returns a slice of the keys in the cache, from oldest to newest.
func (c *Cache[K, V]) Keys() []K {
c.lock.RLock()
keys := c.lru.Keys()
c.lock.RUnlock()
return keys
}
// Values returns a slice of the values in the cache, from oldest to newest.
func (c *Cache[K, V]) Values() []V {
c.lock.RLock()
values := c.lru.Values()
c.lock.RUnlock()
return values
}
// Len returns the number of items in the cache.
func (c *Cache[K, V]) Len() int {
c.lock.RLock()
length := c.lru.Len()
c.lock.RUnlock()
return length
}
+29
View File
@@ -0,0 +1,29 @@
This license applies to simplelru/list.go
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+177
View File
@@ -0,0 +1,177 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package simplelru
import (
"errors"
"github.com/hashicorp/golang-lru/v2/internal"
)
// EvictCallback is used to get a callback when a cache entry is evicted
type EvictCallback[K comparable, V any] func(key K, value V)
// LRU implements a non-thread safe fixed size LRU cache
type LRU[K comparable, V any] struct {
size int
evictList *internal.LruList[K, V]
items map[K]*internal.Entry[K, V]
onEvict EvictCallback[K, V]
}
// NewLRU constructs an LRU of the given size
func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, V], error) {
if size <= 0 {
return nil, errors.New("must provide a positive size")
}
c := &LRU[K, V]{
size: size,
evictList: internal.NewList[K, V](),
items: make(map[K]*internal.Entry[K, V]),
onEvict: onEvict,
}
return c, nil
}
// Purge is used to completely clear the cache.
func (c *LRU[K, V]) Purge() {
for k, v := range c.items {
if c.onEvict != nil {
c.onEvict(k, v.Value)
}
delete(c.items, k)
}
c.evictList.Init()
}
// Add adds a value to the cache. Returns true if an eviction occurred.
func (c *LRU[K, V]) Add(key K, value V) (evicted bool) {
// Check for existing item
if ent, ok := c.items[key]; ok {
c.evictList.MoveToFront(ent)
ent.Value = value
return false
}
// Add new item
ent := c.evictList.PushFront(key, value)
c.items[key] = ent
evict := c.evictList.Length() > c.size
// Verify size not exceeded
if evict {
c.removeOldest()
}
return evict
}
// Get looks up a key's value from the cache.
func (c *LRU[K, V]) Get(key K) (value V, ok bool) {
if ent, ok := c.items[key]; ok {
c.evictList.MoveToFront(ent)
return ent.Value, true
}
return
}
// Contains checks if a key is in the cache, without updating the recent-ness
// or deleting it for being stale.
func (c *LRU[K, V]) Contains(key K) (ok bool) {
_, ok = c.items[key]
return ok
}
// Peek returns the key value (or undefined if not found) without updating
// the "recently used"-ness of the key.
func (c *LRU[K, V]) Peek(key K) (value V, ok bool) {
var ent *internal.Entry[K, V]
if ent, ok = c.items[key]; ok {
return ent.Value, true
}
return
}
// Remove removes the provided key from the cache, returning if the
// key was contained.
func (c *LRU[K, V]) Remove(key K) (present bool) {
if ent, ok := c.items[key]; ok {
c.removeElement(ent)
return true
}
return false
}
// RemoveOldest removes the oldest item from the cache.
func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) {
if ent := c.evictList.Back(); ent != nil {
c.removeElement(ent)
return ent.Key, ent.Value, true
}
return
}
// GetOldest returns the oldest entry
func (c *LRU[K, V]) GetOldest() (key K, value V, ok bool) {
if ent := c.evictList.Back(); ent != nil {
return ent.Key, ent.Value, true
}
return
}
// Keys returns a slice of the keys in the cache, from oldest to newest.
func (c *LRU[K, V]) Keys() []K {
keys := make([]K, c.evictList.Length())
i := 0
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
keys[i] = ent.Key
i++
}
return keys
}
// Values returns a slice of the values in the cache, from oldest to newest.
func (c *LRU[K, V]) Values() []V {
values := make([]V, len(c.items))
i := 0
for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() {
values[i] = ent.Value
i++
}
return values
}
// Len returns the number of items in the cache.
func (c *LRU[K, V]) Len() int {
return c.evictList.Length()
}
// Resize changes the cache size.
func (c *LRU[K, V]) Resize(size int) (evicted int) {
diff := c.Len() - size
if diff < 0 {
diff = 0
}
for i := 0; i < diff; i++ {
c.removeOldest()
}
c.size = size
return diff
}
// removeOldest removes the oldest item from the cache.
func (c *LRU[K, V]) removeOldest() {
if ent := c.evictList.Back(); ent != nil {
c.removeElement(ent)
}
}
// removeElement is used to remove a given list element from the cache
func (c *LRU[K, V]) removeElement(e *internal.Entry[K, V]) {
c.evictList.Remove(e)
delete(c.items, e.Key)
if c.onEvict != nil {
c.onEvict(e.Key, e.Value)
}
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Package simplelru provides simple LRU implementation based on build-in container/list.
package simplelru
// LRUCache is the interface for simple LRU cache.
type LRUCache[K comparable, V any] interface {
// Adds a value to the cache, returns true if an eviction occurred and
// updates the "recently used"-ness of the key.
Add(key K, value V) bool
// Returns key's value from the cache and
// updates the "recently used"-ness of the key. #value, isFound
Get(key K) (value V, ok bool)
// Checks if a key exists in cache without updating the recent-ness.
Contains(key K) (ok bool)
// Returns key's value without updating the "recently used"-ness of the key.
Peek(key K) (value V, ok bool)
// Removes a key from the cache.
Remove(key K) bool
// Removes the oldest entry from cache.
RemoveOldest() (K, V, bool)
// Returns the oldest entry from the cache. #key, value, isFound
GetOldest() (K, V, bool)
// Returns a slice of the keys in the cache, from oldest to newest.
Keys() []K
// Values returns a slice of the values in the cache, from oldest to newest.
Values() []V
// Returns the number of items in the cache.
Len() int
// Clears all cache entries.
Purge()
// Resizes cache, returning number evicted
Resize(int) int
}
+43
View File
@@ -0,0 +1,43 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
# Intellij
*.iml
.idea/
# VS Code
debug
debug_test
.vscode/
# Mac
.DS_Store
# go work
go.work
go.work.sum
# running the fuzzer locally may create this folder, lets ignore it
testdata/fuzz/
@@ -1,19 +1,19 @@
Copyright (c) 2016 Uber Technologies, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Copyright (c) 2018 Huan Du
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+472
View File
@@ -0,0 +1,472 @@
# SQL builder for Go
[![Go](https://github.com/huandu/go-sqlbuilder/workflows/Go/badge.svg)](https://github.com/huandu/go-sqlbuilder/actions)
[![GoDoc](https://godoc.org/github.com/huandu/go-sqlbuilder?status.svg)](https://pkg.go.dev/github.com/huandu/go-sqlbuilder)
[![Go Report](https://goreportcard.com/badge/github.com/huandu/go-sqlbuilder)](https://goreportcard.com/report/github.com/huandu/go-sqlbuilder)
[![Coverage Status](https://coveralls.io/repos/github/huandu/go-sqlbuilder/badge.svg?branch=master)](https://coveralls.io/github/huandu/go-sqlbuilder?branch=master)
- [Install](#install)
- [Usage](#usage)
- [Basic usage](#basic-usage)
- [Pre-defined SQL builders](#pre-defined-sql-builders)
- [Build `WHERE` clause](#build-where-clause)
- [Share `WHERE` clause among builders](#share-where-clause-among-builders)
- [Build SQL for different systems](#build-sql-for-different-systems)
- [Using `Struct` as a light weight ORM](#using-struct-as-a-light-weight-orm)
- [Nested SQL](#nested-sql)
- [Use `sql.Named` in a builder](#use-sqlnamed-in-a-builder)
- [Argument modifiers](#argument-modifiers)
- [Freestyle builder](#freestyle-builder)
- [Using special syntax to build SQL](#using-special-syntax-to-build-sql)
- [Interpolate `args` in the `sql`](#interpolate-args-in-the-sql)
- [License](#license)
The `sqlbuilder` package offers a comprehensive suite of SQL string concatenation utilities. It is designed to facilitate the construction of SQL statements compatible with Go's standard library `sql.DB` and `sql.Stmt` interfaces, focusing on optimizing the performance of SQL statement creation and minimizing memory usage.
The primary objective of this package's design was to craft a SQL construction library that operates independently of specific database drivers and business logic. It is tailored to accommodate the diverse needs of enterprise environments, including the use of custom database drivers, adherence to specialized operational standards, integration into heterogeneous systems, and handling of non-standard SQL in intricate scenarios. Following its open-source release, the package has undergone extensive testing within a large-scale enterprise context, successfully managing the workload of hundreds of millions of orders daily and nearly ten million transactions daily, thus highlighting its robust performance and scalability.
This package is not restricted to any particular database driver and does not automatically establish connections with any database systems. It does not presuppose the execution of the generated SQL, making it versatile for a broad spectrum of application scenarios that involve the construction of SQL-like statements. It is equally well-suited for further development aimed at creating more business-specific database interaction packages, ORMs, and similar tools.
## Install
Install this package by executing the following command:
```shell
go get github.com/huandu/go-sqlbuilder
```
## Usage
### Basic usage
We can rapidly construct SQL statements using this package.
```go
sql := sqlbuilder.Select("id", "name").From("demo.user").
Where("status = 1").Limit(10).
String()
fmt.Println(sql)
// Output:
// SELECT id, name FROM demo.user WHERE status = 1 LIMIT 10
```
In common scenarios, it is necessary to escape all user inputs. To achieve this, initialize a builder at the outset.
```go
sb := sqlbuilder.NewSelectBuilder()
sb.Select("id", "name", sb.As("COUNT(*)", "c"))
sb.From("user")
sb.Where(sb.In("status", 1, 2, 5))
sql, args := sb.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// SELECT id, name, COUNT(*) AS c FROM user WHERE status IN (?, ?, ?)
// [1 2 5]
```
### Pre-defined SQL builders
This package includes the following pre-defined builders. API documentation and usage examples are available in the `godoc` online documentation.
- [Struct](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Struct): Factory for creating builders based on struct definitions.
- [CreateTableBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#CreateTableBuilder): Builder for `CREATE TABLE`.
- [SelectBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#SelectBuilder): Builder for `SELECT`.
- [InsertBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#InsertBuilder): Builder for `INSERT`.
- [UpdateBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#UpdateBuilder): Builder for `UPDATE`.
- [DeleteBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#DeleteBuilder): Builder for `DELETE`.
- [UnionBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#UnionBuilder): Builder for `UNION` and `UNION ALL`.
- [CTEBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#CTEBuilder): Builder for Common Table Expression (CTE), e.g. `WITH name (col1, col2) AS (SELECT ...)`.
- [Buildf](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Buildf): Freestyle builder employing `fmt.Sprintf`-like syntax.
- [Build](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Build): Advanced freestyle builder utilizing special syntax as defined in [Args#Compile](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Args.Compile).
- [BuildNamed](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#BuildNamed): Advanced freestyle builder that uses `${key}` to reference values by key in a map.
A unique method, `SQL(sql string)`, is implemented across all statement builders, enabling the insertion of any arbitrary SQL segment into a builder during SQL construction. This feature is particularly beneficial for crafting SQL statements that incorporate non-standard syntax required by OLTP or OLAP systems.
```go
// Build a SQL to create a HIVE table.
sql := sqlbuilder.CreateTable("users").
SQL("PARTITION BY (year)").
SQL("AS").
SQL(
sqlbuilder.Select("columns[0] id", "columns[1] name", "columns[2] year").
From("`all-users.csv`").
String(),
).
String()
fmt.Println(sql)
// Output:
// CREATE TABLE users PARTITION BY (year) AS SELECT columns[0] id, columns[1] name, columns[2] year FROM `all-users.csv`
```
Below are several utility methods designed to address special cases.
- [Flatten](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Flatten) enables the recursive conversion of an array-like variable into a flat slice of `[]interface{}`. For example, invoking `Flatten([]interface{"foo", []int{2, 3}})` yields `[]interface{}{"foo", 2, 3}`. This method is compatible with builder methods such as `In`, `NotIn`, `Values`, etc., facilitating the conversion of a typed array into `[]interface{}` or the merging of inputs.
- [List](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#List) operates similarly to `Flatten`, with the exception that its return value is specifically intended for use as builder arguments. For example, `Buildf("my_func(%v)", List([]int{1, 2, 3})).Build()` generates SQL `my_func(?, ?, ?)` with arguments `[]interface{}{1, 2, 3}`.
- [Raw](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Raw) designates a string as a "raw string" within arguments. For instance, `Buildf("SELECT %v", Raw("NOW()")).Build()` results in SQL `SELECT NOW()`.
For detailed instructions on utilizing these builders, consult the [examples provided on GoDoc](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#pkg-examples).
### Build `WHERE` clause
`WHERE` clause is the most important part of a SQL. We can use `Where` method to add one or more conditions to a builder.
To simplify the construction of `WHERE` clauses, a utility type named `Cond` is provided for condition building. All builders that support `WHERE` clauses possess an anonymous `Cond` field, enabling the invocation of `Cond` methods on these builders.
```go
sb := sqlbuilder.Select("id").From("user")
sb.Where(
sb.In("status", 1, 2, 5),
sb.Or(
sb.Equal("name", "foo"),
sb.Like("email", "foo@%"),
),
)
sql, args := sb.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// SELECT id FROM user WHERE status IN (?, ?, ?) AND (name = ? OR email LIKE ?)
// [1 2 5 foo foo@%]
```
There are many methods for building conditions.
- [Cond.Equal](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Equal)/[Cond.E](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.E)/[Cond.EQ](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.EQ): `field = value`.
- [Cond.NotEqual](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotEqual)/[Cond.NE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NE)/[Cond.NEQ](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NEQ): `field <> value`.
- [Cond.GreaterThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GreaterThan)/[Cond.G](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.G)/[Cond.GT](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GT): `field > value`.
- [Cond.GreaterEqualThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GreaterEqualThan)/[Cond.GE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GE)/[Cond.GTE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.GTE): `field >= value`.
- [Cond.LessThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LessThan)/[Cond.L](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.L)/[Cond.LT](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LT): `field < value`.
- [Cond.LessEqualThan](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LessEqualThan)/[Cond.LE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LE)/[Cond.LTE](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.LTE): `field <= value`.
- [Cond.In](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.In): `field IN (value1, value2, ...)`.
- [Cond.NotIn](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotIn): `field NOT IN (value1, value2, ...)`.
- [Cond.Like](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Like): `field LIKE value`.
- [Cond.ILike](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.ILike): `field ILIKE value`.
- [Cond.NotLike](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotLike): `field NOT LIKE value`.
- [Cond.NotILike](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotILike): `field NOT ILIKE value`.
- [Cond.Between](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Between): `field BETWEEN lower AND upper`.
- [Cond.NotBetween](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotBetween): `field NOT BETWEEN lower AND upper`.
- [Cond.IsNull](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.IsNull): `field IS NULL`.
- [Cond.IsNotNull](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.IsNotNull): `field IS NOT NULL`.
- [Cond.Exists](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Exists): `EXISTS (subquery)`.
- [Cond.NotExists](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.NotExists): `NOT EXISTS (subquery)`.
- [Cond.Not](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Not): `NOT expr`.
- [Cond.Any](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Any): `field op ANY (value1, value2, ...)`.
- [Cond.All](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.All): `field op ALL (value1, value2, ...)`.
- [Cond.Some](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Some): `field op SOME (value1, value2, ...)`.
- [Cond.IsDistinctFrom](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.IsDistinctFrom) `field IS DISTINCT FROM value`.
- [Cond.IsNotDistinctFrom](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.IsNotDistinctFrom) `field IS NOT DISTINCT FROM value`.
- [Cond.Var](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Var): A placeholder for any value.
There are also some methods to combine conditions.
- [Cond.And](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.And): Combine conditions with `AND` operator.
- [Cond.Or](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Cond.Or): Combine conditions with `OR` operator.
### Share `WHERE` clause among builders
Due to the importance of the `WHERE` statement in SQL, we often need to continuously append conditions and even share some common `WHERE` conditions among different builders. Therefore, we abstract the `WHERE` statement into a `WhereClause` struct, which can be used to create reusable `WHERE` conditions.
The following example illustrates how to transfer a `WHERE` clause from a `SelectBuilder` to an `UpdateBuilder`.
```go
// Build a SQL to select a user from database.
sb := Select("name", "level").From("users")
sb.Where(
sb.Equal("id", 1234),
)
fmt.Println(sb)
ub := Update("users")
ub.Set(
ub.Add("level", 10),
)
// Set the WHERE clause of UPDATE to the WHERE clause of SELECT.
ub.WhereClause = sb.WhereClause
fmt.Println(ub)
// Output:
// SELECT name, level FROM users WHERE id = ?
// UPDATE users SET level = level + ? WHERE id = ?
```
Refer to the [WhereClause](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#WhereClause) examples to learn its usage.
### Build SQL for different systems
SQL syntax and parameter placeholders can differ across systems. To address these variations, this package introduces a concept termed "flavor".
Currently, flavors such as `MySQL`, `PostgreSQL`, `SQLite`, `SQLServer`, `CQL`, `ClickHouse`, `Presto`, `Oracle` and `Informix` are supported. Should there be a demand for additional flavors, please submit an issue or a pull request.
By default, all builders utilize `DefaultFlavor` for SQL construction, with `MySQL` as the default setting.
For greater readibility, `PostgreSQL.NewSelectBuilder()` can be used to instantiate a `SelectBuilder` with the `PostgreSQL` flavor. All builders can be created in this way.
### Using `Struct` as a light weight ORM
`Struct` encapsulates type information and struct fields, serving as a builder factory. Utilizing `Struct` methods, one can generate `SELECT`/`INSERT`/`UPDATE`/`DELETE` builders that are pre-configured for use with the struct, thereby conserving time and mitigating the risk of typographical errors in column name entries.
One can define a struct type and employ field tags to guide `Struct` in generating the appropriate builders.
```go
type ATable struct {
Field1 string // If a field doesn't has a tag, use "Field1" as column name in SQL.
Field2 int `db:"field2"` // Use "db" in field tag to set column name used in SQL.
Field3 int64 `db:"field3" fieldtag:"foo,bar"` // Set fieldtag to a field. We can call `WithTag` to include fields with tag or `WithoutTag` to exclude fields with tag.
Field4 int64 `db:"field4" fieldtag:"foo"` // If we use `s.WithTag("foo").Select("t")`, columnes of SELECT are "t.field3" and "t.field4".
Field5 string `db:"field5" fieldas:"f5_alias"` // Use "fieldas" in field tag to set a column alias (AS) used in SELECT.
Ignored int32 `db:"-"` // If we set field name as "-", Struct will ignore it.
unexported int // Unexported field is not visible to Struct.
Quoted string `db:"quoted" fieldopt:"withquote"` // Add quote to the field using back quote or double quote. See `Flavor#Quote`.
Empty uint `db:"empty" fieldopt:"omitempty"` // Omit the field in UPDATE if it is a nil or zero value.
// The `omitempty` can be written as a function.
// In this case, omit empty field `Tagged` when UPDATE for tag `tag1` and `tag3` but not `tag2`.
Tagged string `db:"tagged" fieldopt:"omitempty(tag1,tag3)" fieldtag:"tag1,tag2,tag3"`
// By default, the `SelectFrom("t")` will add the "t." to all names of fields matched tag.
// We can add dot to field name to disable this behavior.
FieldWithTableAlias string `db:"m.field"`
}
```
For detailed instructions on utilizing `Struct`, refer to the [examples](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#Struct).
Furthermore, `Struct` can be employed as a zero-configuration ORM. Unlike most ORM implementations that necessitate preliminary configurations for database connectivity, `Struct` operates without any configuration, functioning seamlessly with any SQL driver compatible with `database/sql`. `Struct` does not invoke any `database/sql` APIs; it solely generates the appropriate SQL statements with arguments for `DB#Query`/`DB#Exec` or an array of struct field addresses for `Rows#Scan`/`Row#Scan`.
The following example demonstrates the use of `Struct` as an ORM. It should be relatively straightforward for developers well-versed in `database/sql` APIs.
```go
type User struct {
ID int64 `db:"id" fieldtag:"pk"`
Name string `db:"name"`
Status int `db:"status"`
}
// A global variable for creating SQL builders.
// All methods of userStruct are thread-safe.
var userStruct = NewStruct(new(User))
func ExampleStruct() {
// Prepare SELECT query.
// SELECT user.id, user.name, user.status FROM user WHERE id = 1234
sb := userStruct.SelectFrom("user")
sb.Where(sb.Equal("id", 1234))
// Execute the query and scan the results into the user struct.
sql, args := sb.Build()
rows, _ := db.Query(sql, args...)
defer rows.Close()
// Scan row data and set value to user.
// Assuming the following data is retrieved:
//
// | id | name | status |
// |------|--------|--------|
// | 1234 | huandu | 1 |
var user User
rows.Scan(userStruct.Addr(&user)...)
fmt.Println(sql)
fmt.Println(args)
fmt.Printf("%#v", user)
// Output:
// SELECT user.id, user.name, user.status FROM user WHERE id = ?
// [1234]
// sqlbuilder.User{ID:1234, Name:"huandu", Status:1}
}
```
In numerous production environments, table column names adhere to the snake_case convention, e.g., `user_id`. Conversely, struct fields in Go are typically in CamelCase to maintain public accessibility and satisfy `golint`. Employing the `db` tag for each struct field can be redundant. To streamline this, a field mapper function can be utilized to establish a consistent rule for mapping struct field names to database column names.
The `DefaultFieldMapper` serves as a global field mapper function, tasked with the conversion of field names to a desired style. By default, it is set to `nil`, effectively performing no action. Recognizing that the majority of table column names follow the snake_case convention, one can assign `DefaultFieldMapper` to `sqlbuilder.SnakeCaseMapper`. For instances that deviate from this norm, a custom mapper can be assigned to a `Struct` via the `WithFieldMapper` method.
Here are important considerations regarding the field mapper:
- Field tag has precedence over field mapper function - thus, mapper is ignored if the `db` tag is set;
- Field mapper is called only once on a Struct when the Struct is used to create builder for the first time.
Refer to the [field mapper function sample](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#FieldMapperFunc) for an illustrative example.
### Nested SQL
Creating nested SQL is straightforward: simply use a builder as an argument for nesting.
Here is an illustrative example.
```go
sb := sqlbuilder.NewSelectBuilder()
fromSb := sqlbuilder.NewSelectBuilder()
statusSb := sqlbuilder.NewSelectBuilder()
sb.Select("id")
sb.From(sb.BuilderAs(fromSb, "user")))
sb.Where(sb.In("status", statusSb))
fromSb.Select("id").From("user").Where(fromSb.GreaterThan("level", 4))
statusSb.Select("status").From("config").Where(statusSb.Equal("state", 1))
sql, args := sb.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// SELECT id FROM (SELECT id FROM user WHERE level > ?) AS user WHERE status IN (SELECT status FROM config WHERE state = ?)
// [4 1]
```
### Use `sql.Named` in a builder
The `sql.Named` function, as defined in the `database/sql` package, facilitates the creation of named arguments within SQL statements. This feature is essential for scenarios where an argument needs to be reused multiple times within a single SQL statement. Incorporating named arguments into a builder is straightforward: treat them as regular arguments.
Here is a sample.
```go
now := time.Now().Unix()
start := sql.Named("start", now-86400)
end := sql.Named("end", now+86400)
sb := sqlbuilder.NewSelectBuilder()
sb.Select("name")
sb.From("user")
sb.Where(
sb.Between("created_at", start, end),
sb.GE("modified_at", start),
)
sql, args := sb.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// SELECT name FROM user WHERE created_at BETWEEN @start AND @end AND modified_at >= @start
// [{{} start 1514458225} {{} end 1514544625}]
```
### Argument modifiers
Several argument modifiers are available:
- `List(arg)` encapsulates a series of arguments. Given `arg` as a slice or array, for instance, a slice containing three integers, it compiles to `?, ?, ?` and is presented in the final arguments as three individual integers. This serves as a convenience tool, utilizable within `IN` expressions or within the `VALUES` clause of an `INSERT INTO` statement.
- `TupleNames(names)` and `Tuple(values)` facilitate the representation of tuple syntax in SQL. For usage examples, refer to [Tuple](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#example-Tuple).
- `Named(name, arg)` designates a named argument. Functionality is limited to `Build` or `BuildNamed`, where it defines a named placeholder using the syntax `${name}`.
- `Raw(expr)` designates `expr` as a plain string within SQL, as opposed to an argument. During the construction of a builder, raw expressions are directly embedded into the SQL string, omitting the need for `?` placeholders.
### Freestyle builder
A builder essentially serves as a means to log arguments. For constructing lengthy SQL statements that incorporate numerous special syntax elements (e.g., special comments intended for a database proxy), `Buildf` can be employed to format the SQL string using a syntax akin to `fmt.Sprintf`.
```go
sb := sqlbuilder.NewSelectBuilder()
sb.Select("id").From("user")
explain := sqlbuilder.Buildf("EXPLAIN %v LEFT JOIN SELECT * FROM banned WHERE state IN (%v, %v)", sb, 1, 2)
sql, args := explain.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// EXPLAIN SELECT id FROM user LEFT JOIN SELECT * FROM banned WHERE state IN (?, ?)
// [1 2]
```
### Using special syntax to build SQL
The `sqlbuilder` package incorporates special syntax for representing uncompiled SQL internally. To leverage this syntax for developing customized tools, the `Build` function can be utilized to compile it with the necessary arguments.
The format string employs special syntax for representing arguments:
- `$?` references successive arguments supplied in the function call, functioning similarly to `%v` in `fmt.Sprintf`.
- `$0`, `$1`, ..., `$n` reference the nth argument provided in the call; subsequent `$?` will then refer to arguments n+1 onwards.
- `${name}` references a named argument defined by `Named` using the specified `name`.
- `$$` represents a literal `"$"` character.
```go
sb := sqlbuilder.NewSelectBuilder()
sb.Select("id").From("user").Where(sb.In("status", 1, 2))
b := sqlbuilder.Build("EXPLAIN $? LEFT JOIN SELECT * FROM $? WHERE created_at > $? AND state IN (${states}) AND modified_at BETWEEN $2 AND $?",
sb, sqlbuilder.Raw("banned"), 1514458225, 1514544625, sqlbuilder.Named("states", sqlbuilder.List([]int{3, 4, 5})))
sql, args := b.Build()
fmt.Println(sql)
fmt.Println(args)
// Output:
// EXPLAIN SELECT id FROM user WHERE status IN (?, ?) LEFT JOIN SELECT * FROM banned WHERE created_at > ? AND state IN (?, ?, ?) AND modified_at BETWEEN ? AND ?
// [1 2 1514458225 3 4 5 1514458225 1514544625]
```
For scenarios where only the `${name}` syntax is required to reference named arguments, utilize `BuildNamed`. This function disables all special syntax except for `${name}` and `$$`.
### Interpolate `args` in the `sql`
Certain SQL-like drivers, such as those for Redis or Elasticsearch, do not implement the `StmtExecContext#ExecContext` method. These drivers encounter issues when `len(args) > 0`. The sole workaround is to interpolate `args` directly into the `sql` string and then execute the resulting query with the driver.
The interpolation feature in this package is designed to provide a "basically sufficient" level of functionality, rather than a capability that rivals the comprehensive features of various SQL drivers and DBMS systems.
_Security warning_: While efforts are made to escape special characters in interpolation methods, this approach remains less secure than using `Stmt` as implemented by SQL drivers.
This feature draws inspiration from the interpolation capabilities found in the `github.com/go-sql-driver/mysql` package.
Here is an example specifically for MySQL:
```go
sb := MySQL.NewSelectBuilder()
sb.Select("name").From("user").Where(
sb.NE("id", 1234),
sb.E("name", "Charmy Liu"),
sb.Like("desc", "%mother's day%"),
)
sql, args := sb.Build()
query, err := MySQL.Interpolate(sql, args)
fmt.Println(query)
fmt.Println(err)
// Output:
// SELECT name FROM user WHERE id <> 1234 AND name = 'Charmy Liu' AND desc LIKE '%mother\'s day%'
// <nil>
```
Here is an example for PostgreSQL, noting that dollar quoting is supported:
```go
// Only the last `$1` is interpolated.
// Others are not interpolated as they are inside dollar quote (the `$$`).
query, err := PostgreSQL.Interpolate(`
CREATE FUNCTION dup(in int, out f1 int, out f2 text) AS $$
SELECT $1, CAST($1 AS text) || ' is text'
$$
LANGUAGE SQL;
SELECT * FROM dup($1);`, []interface{}{42})
fmt.Println(query)
fmt.Println(err)
// Output:
//
// CREATE FUNCTION dup(in int, out f1 int, out f2 text) AS $$
// SELECT $1, CAST($1 AS text) || ' is text'
// $$
// LANGUAGE SQL;
//
// SELECT * FROM dup(42);
// <nil>
```
## License
This package is licensed under the MIT license. For more information, refer to the LICENSE file.
+366
View File
@@ -0,0 +1,366 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
import (
"database/sql"
"fmt"
"sort"
"strconv"
"strings"
)
// Args stores arguments associated with a SQL.
type Args struct {
// The default flavor used by `Args#Compile`
Flavor Flavor
indexBase int
argValues []interface{}
namedArgs map[string]int
sqlNamedArgs map[string]int
onlyNamed bool
}
func init() {
// Predefine some $n args to avoid additional memory allocation.
predefinedArgs = make([]string, 0, maxPredefinedArgs)
for i := 0; i < maxPredefinedArgs; i++ {
predefinedArgs = append(predefinedArgs, fmt.Sprintf("$%v", i))
}
}
const maxPredefinedArgs = 64
var predefinedArgs []string
// Add adds an arg to Args and returns a placeholder.
func (args *Args) Add(arg interface{}) string {
idx := args.add(arg)
if idx < maxPredefinedArgs {
return predefinedArgs[idx]
}
return fmt.Sprintf("$%v", idx)
}
func (args *Args) add(arg interface{}) int {
idx := len(args.argValues) + args.indexBase
switch a := arg.(type) {
case sql.NamedArg:
if args.sqlNamedArgs == nil {
args.sqlNamedArgs = map[string]int{}
}
if p, ok := args.sqlNamedArgs[a.Name]; ok {
arg = args.argValues[p]
break
}
args.sqlNamedArgs[a.Name] = idx
case namedArgs:
if args.namedArgs == nil {
args.namedArgs = map[string]int{}
}
if p, ok := args.namedArgs[a.name]; ok {
arg = args.argValues[p]
break
}
// Find out the real arg and add it to args.
idx = args.add(a.arg)
args.namedArgs[a.name] = idx
return idx
}
args.argValues = append(args.argValues, arg)
return idx
}
// Compile compiles builder's format to standard sql and returns associated args.
//
// The format string uses a special syntax to represent arguments.
//
// $? refers successive arguments passed in the call. It works similar as `%v` in `fmt.Sprintf`.
// $0 $1 ... $n refers nth-argument passed in the call. Next $? will use arguments n+1.
// ${name} refers a named argument created by `Named` with `name`.
// $$ is a "$" string.
func (args *Args) Compile(format string, initialValue ...interface{}) (query string, values []interface{}) {
return args.CompileWithFlavor(format, args.Flavor, initialValue...)
}
// CompileWithFlavor compiles builder's format to standard sql with flavor and returns associated args.
//
// See doc for `Compile` to learn details.
func (args *Args) CompileWithFlavor(format string, flavor Flavor, initialValue ...interface{}) (query string, values []interface{}) {
idx := strings.IndexRune(format, '$')
offset := 0
ctx := &argsCompileContext{
stringBuilder: newStringBuilder(),
Flavor: flavor,
Values: initialValue,
}
if ctx.Flavor == invalidFlavor {
ctx.Flavor = DefaultFlavor
}
for idx >= 0 && len(format) > 0 {
if idx > 0 {
ctx.WriteString(format[:idx])
}
format = format[idx+1:]
// Treat the $ at the end of format is a normal $ rune.
if len(format) == 0 {
ctx.WriteRune('$')
break
}
if r := format[0]; r == '$' {
ctx.WriteRune('$')
format = format[1:]
} else if r == '{' {
format = args.compileNamed(ctx, format)
} else if !args.onlyNamed && '0' <= r && r <= '9' {
format, offset = args.compileDigits(ctx, format, offset)
} else if !args.onlyNamed && r == '?' {
format, offset = args.compileSuccessive(ctx, format[1:], offset)
} else {
// For unknown $ expression format, treat it as a normal $ rune.
ctx.WriteRune('$')
}
idx = strings.IndexRune(format, '$')
}
if len(format) > 0 {
ctx.WriteString(format)
}
query = ctx.String()
values = args.mergeSQLNamedArgs(ctx)
return
}
// Value returns the value of the arg.
// The arg must be the value returned by `Add`.
func (args *Args) Value(arg string) interface{} {
_, values := args.Compile(arg)
if len(values) == 0 {
return nil
}
return values[0]
}
func (args *Args) compileNamed(ctx *argsCompileContext, format string) string {
i := 1
for ; i < len(format) && format[i] != '}'; i++ {
// Nothing.
}
// Invalid $ format. Ignore it.
if i == len(format) {
return format
}
name := format[1:i]
format = format[i+1:]
if p, ok := args.namedArgs[name]; ok {
format, _ = args.compileSuccessive(ctx, format, p-args.indexBase)
}
return format
}
func (args *Args) compileDigits(ctx *argsCompileContext, format string, offset int) (string, int) {
i := 1
for ; i < len(format) && '0' <= format[i] && format[i] <= '9'; i++ {
// Nothing.
}
digits := format[:i]
format = format[i:]
if pointer, err := strconv.Atoi(digits); err == nil {
return args.compileSuccessive(ctx, format, pointer-args.indexBase)
}
return format, offset
}
func (args *Args) compileSuccessive(ctx *argsCompileContext, format string, offset int) (string, int) {
if offset < 0 || offset >= len(args.argValues) {
ctx.WriteString("/* INVALID ARG $")
ctx.WriteString(strconv.Itoa(offset))
ctx.WriteString(" */")
return format, offset
}
arg := args.argValues[offset]
ctx.WriteValue(arg)
return format, offset + 1
}
func (args *Args) mergeSQLNamedArgs(ctx *argsCompileContext) []interface{} {
if len(args.sqlNamedArgs) == 0 && len(ctx.NamedArgs) == 0 {
return ctx.Values
}
values := ctx.Values
existingNames := make(map[string]struct{}, len(ctx.NamedArgs))
// Add all named args to values.
// Remove duplicated named args in this step.
for _, arg := range ctx.NamedArgs {
if _, ok := existingNames[arg.Name]; !ok {
existingNames[arg.Name] = struct{}{}
values = append(values, arg)
}
}
// Stabilize the sequence to make it easier to write test cases.
ints := make([]int, 0, len(args.sqlNamedArgs))
for n, p := range args.sqlNamedArgs {
if _, ok := existingNames[n]; ok {
continue
}
ints = append(ints, p)
}
sort.Ints(ints)
for _, i := range ints {
values = append(values, args.argValues[i])
}
return values
}
func parseNamedArgs(initialValue []interface{}) (values []interface{}, namedValues []sql.NamedArg) {
if len(initialValue) == 0 {
values = initialValue
return
}
// sql.NamedArgs must be placed at the end of the initial value.
size := len(initialValue)
i := size
for ; i > 0; i-- {
switch initialValue[i-1].(type) {
case sql.NamedArg:
continue
}
break
}
if i == size {
values = initialValue
return
}
values = initialValue[:i]
namedValues = make([]sql.NamedArg, 0, size-i)
for ; i < size; i++ {
namedValues = append(namedValues, initialValue[i].(sql.NamedArg))
}
return
}
type argsCompileContext struct {
*stringBuilder
Flavor Flavor
Values []interface{}
NamedArgs []sql.NamedArg
}
func (ctx *argsCompileContext) WriteValue(arg interface{}) {
switch a := arg.(type) {
case Builder:
s, values := a.BuildWithFlavor(ctx.Flavor, ctx.Values...)
ctx.WriteString(s)
// Add all values to ctx.
// Named args must be located at the end of values.
values, namedArgs := parseNamedArgs(values)
ctx.Values = values
ctx.NamedArgs = append(ctx.NamedArgs, namedArgs...)
case sql.NamedArg:
ctx.WriteRune('@')
ctx.WriteString(a.Name)
ctx.NamedArgs = append(ctx.NamedArgs, a)
case rawArgs:
ctx.WriteString(a.expr)
case listArgs:
if a.isTuple {
ctx.WriteRune('(')
}
if len(a.args) > 0 {
ctx.WriteValue(a.args[0])
}
for i := 1; i < len(a.args); i++ {
ctx.WriteString(", ")
ctx.WriteValue(a.args[i])
}
if a.isTuple {
ctx.WriteRune(')')
}
case condBuilder:
a.Builder(ctx)
default:
switch ctx.Flavor {
case MySQL, SQLite, CQL, ClickHouse, Presto, Informix, Doris:
ctx.WriteRune('?')
case PostgreSQL:
fmt.Fprintf(ctx, "$%d", len(ctx.Values)+1)
case SQLServer:
fmt.Fprintf(ctx, "@p%d", len(ctx.Values)+1)
case Oracle:
fmt.Fprintf(ctx, ":%d", len(ctx.Values)+1)
default:
panic(fmt.Errorf("Args.CompileWithFlavor: invalid flavor %v (%v)", ctx.Flavor, int(ctx.Flavor)))
}
ctx.Values = append(ctx.Values, arg)
}
}
func (ctx *argsCompileContext) WriteValues(values []interface{}, sep string) {
if len(values) == 0 {
return
}
ctx.WriteValue(values[0])
for _, v := range values[1:] {
ctx.WriteString(sep)
ctx.WriteValue(v)
}
}
+119
View File
@@ -0,0 +1,119 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
import (
"fmt"
)
// Builder is a general SQL builder.
// It's used by Args to create nested SQL like the `IN` expression in
// `SELECT * FROM t1 WHERE id IN (SELECT id FROM t2)`.
type Builder interface {
Build() (sql string, args []interface{})
BuildWithFlavor(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{})
Flavor() Flavor
}
type compiledBuilder struct {
args *Args
format string
}
var _ Builder = new(compiledBuilder)
func (cb *compiledBuilder) Build() (sql string, args []interface{}) {
return cb.args.Compile(cb.format)
}
func (cb *compiledBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
return cb.args.CompileWithFlavor(cb.format, flavor, initialArg...)
}
// Flavor returns flavor of builder
// Always returns DefaultFlavor
func (cb *compiledBuilder) Flavor() Flavor {
return cb.args.Flavor
}
type flavoredBuilder struct {
builder Builder
flavor Flavor
}
func (fb *flavoredBuilder) Build() (sql string, args []interface{}) {
return fb.builder.BuildWithFlavor(fb.flavor)
}
func (fb *flavoredBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
return fb.builder.BuildWithFlavor(flavor, initialArg...)
}
// Flavor returns flavor of builder
func (fb *flavoredBuilder) Flavor() Flavor {
return fb.flavor
}
// WithFlavor creates a new Builder based on builder with a default flavor.
func WithFlavor(builder Builder, flavor Flavor) Builder {
return &flavoredBuilder{
builder: builder,
flavor: flavor,
}
}
// Buildf creates a Builder from a format string using `fmt.Sprintf`-like syntax.
// As all arguments will be converted to a string internally, e.g. "$0",
// only `%v` and `%s` are valid.
func Buildf(format string, arg ...interface{}) Builder {
args := &Args{
Flavor: DefaultFlavor,
}
vars := make([]interface{}, 0, len(arg))
for _, a := range arg {
vars = append(vars, args.Add(a))
}
return &compiledBuilder{
args: args,
format: fmt.Sprintf(Escape(format), vars...),
}
}
// Build creates a Builder from a format string.
// The format string uses special syntax to represent arguments.
// See doc in `Args#Compile` for syntax details.
func Build(format string, arg ...interface{}) Builder {
args := &Args{
Flavor: DefaultFlavor,
}
for _, a := range arg {
args.Add(a)
}
return &compiledBuilder{
args: args,
format: format,
}
}
// BuildNamed creates a Builder from a format string.
// The format string uses `${key}` to refer the value of named by key.
func BuildNamed(format string, named map[string]interface{}) Builder {
args := &Args{
Flavor: DefaultFlavor,
onlyNamed: true,
}
for n, v := range named {
args.Add(Named(n, v))
}
return &compiledBuilder{
args: args,
format: format,
}
}
+650
View File
@@ -0,0 +1,650 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
const (
lparen = "("
rparen = ")"
opOR = " OR "
opAND = " AND "
opNOT = "NOT "
)
const minIndexBase = 256
// Cond provides several helper methods to build conditions.
type Cond struct {
Args *Args
}
// NewCond returns a new Cond.
func NewCond() *Cond {
return &Cond{
Args: &Args{
// Based on the discussion in #174, users may call this method to create
// `Cond` for building various conditions, which is a misuse, but we
// cannot completely prevent this error. To facilitate users in
// identifying the issue when they make mistakes and to avoid
// unexpected stackoverflows, the base index for `Args` is
// deliberately set to a larger non-zero value here. This can
// significantly reduce the likelihood of issues and allows for
// timely error notification to users.
indexBase: minIndexBase,
},
}
}
// Equal is used to construct the expression "field = value".
func (c *Cond) Equal(field string, value interface{}) string {
if len(field) == 0 {
return ""
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString(field)
ctx.WriteString(" = ")
ctx.WriteValue(value)
},
})
}
// E is an alias of Equal.
func (c *Cond) E(field string, value interface{}) string {
return c.Equal(field, value)
}
// EQ is an alias of Equal.
func (c *Cond) EQ(field string, value interface{}) string {
return c.Equal(field, value)
}
// NotEqual is used to construct the expression "field <> value".
func (c *Cond) NotEqual(field string, value interface{}) string {
if len(field) == 0 {
return ""
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString(field)
ctx.WriteString(" <> ")
ctx.WriteValue(value)
},
})
}
// NE is an alias of NotEqual.
func (c *Cond) NE(field string, value interface{}) string {
return c.NotEqual(field, value)
}
// NEQ is an alias of NotEqual.
func (c *Cond) NEQ(field string, value interface{}) string {
return c.NotEqual(field, value)
}
// GreaterThan is used to construct the expression "field > value".
func (c *Cond) GreaterThan(field string, value interface{}) string {
if len(field) == 0 {
return ""
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString(field)
ctx.WriteString(" > ")
ctx.WriteValue(value)
},
})
}
// G is an alias of GreaterThan.
func (c *Cond) G(field string, value interface{}) string {
return c.GreaterThan(field, value)
}
// GT is an alias of GreaterThan.
func (c *Cond) GT(field string, value interface{}) string {
return c.GreaterThan(field, value)
}
// GreaterEqualThan is used to construct the expression "field >= value".
func (c *Cond) GreaterEqualThan(field string, value interface{}) string {
if len(field) == 0 {
return ""
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString(field)
ctx.WriteString(" >= ")
ctx.WriteValue(value)
},
})
}
// GE is an alias of GreaterEqualThan.
func (c *Cond) GE(field string, value interface{}) string {
return c.GreaterEqualThan(field, value)
}
// GTE is an alias of GreaterEqualThan.
func (c *Cond) GTE(field string, value interface{}) string {
return c.GreaterEqualThan(field, value)
}
// LessThan is used to construct the expression "field < value".
func (c *Cond) LessThan(field string, value interface{}) string {
if len(field) == 0 {
return ""
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString(field)
ctx.WriteString(" < ")
ctx.WriteValue(value)
},
})
}
// L is an alias of LessThan.
func (c *Cond) L(field string, value interface{}) string {
return c.LessThan(field, value)
}
// LT is an alias of LessThan.
func (c *Cond) LT(field string, value interface{}) string {
return c.LessThan(field, value)
}
// LessEqualThan is used to construct the expression "field <= value".
func (c *Cond) LessEqualThan(field string, value interface{}) string {
if len(field) == 0 {
return ""
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString(field)
ctx.WriteString(" <= ")
ctx.WriteValue(value)
},
})
}
// LE is an alias of LessEqualThan.
func (c *Cond) LE(field string, value interface{}) string {
return c.LessEqualThan(field, value)
}
// LTE is an alias of LessEqualThan.
func (c *Cond) LTE(field string, value interface{}) string {
return c.LessEqualThan(field, value)
}
// In is used to construct the expression "field IN (value...)".
func (c *Cond) In(field string, values ...interface{}) string {
if len(field) == 0 {
return ""
}
// Empty values means "false".
if len(values) == 0 {
return "0 = 1"
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString(field)
ctx.WriteString(" IN (")
ctx.WriteValues(values, ", ")
ctx.WriteString(")")
},
})
}
// NotIn is used to construct the expression "field NOT IN (value...)".
func (c *Cond) NotIn(field string, values ...interface{}) string {
if len(field) == 0 {
return ""
}
// Empty values means "true".
if len(values) == 0 {
return "0 = 0"
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString(field)
ctx.WriteString(" NOT IN (")
ctx.WriteValues(values, ", ")
ctx.WriteString(")")
},
})
}
// Like is used to construct the expression "field LIKE value".
func (c *Cond) Like(field string, value interface{}) string {
if len(field) == 0 {
return ""
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString(field)
ctx.WriteString(" LIKE ")
ctx.WriteValue(value)
},
})
}
// ILike is used to construct the expression "field ILIKE value".
//
// When the database system does not support the ILIKE operator,
// the ILike method will return "LOWER(field) LIKE LOWER(value)"
// to simulate the behavior of the ILIKE operator.
func (c *Cond) ILike(field string, value interface{}) string {
if len(field) == 0 {
return ""
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
switch ctx.Flavor {
case PostgreSQL, SQLite:
ctx.WriteString(field)
ctx.WriteString(" ILIKE ")
ctx.WriteValue(value)
default:
// Use LOWER to simulate ILIKE.
ctx.WriteString("LOWER(")
ctx.WriteString(field)
ctx.WriteString(") LIKE LOWER(")
ctx.WriteValue(value)
ctx.WriteString(")")
}
},
})
}
// NotLike is used to construct the expression "field NOT LIKE value".
func (c *Cond) NotLike(field string, value interface{}) string {
if len(field) == 0 {
return ""
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString(field)
ctx.WriteString(" NOT LIKE ")
ctx.WriteValue(value)
},
})
}
// NotILike is used to construct the expression "field NOT ILIKE value".
//
// When the database system does not support the ILIKE operator,
// the NotILike method will return "LOWER(field) NOT LIKE LOWER(value)"
// to simulate the behavior of the ILIKE operator.
func (c *Cond) NotILike(field string, value interface{}) string {
if len(field) == 0 {
return ""
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
switch ctx.Flavor {
case PostgreSQL, SQLite:
ctx.WriteString(field)
ctx.WriteString(" NOT ILIKE ")
ctx.WriteValue(value)
default:
// Use LOWER to simulate ILIKE.
ctx.WriteString("LOWER(")
ctx.WriteString(field)
ctx.WriteString(") NOT LIKE LOWER(")
ctx.WriteValue(value)
ctx.WriteString(")")
}
},
})
}
// IsNull is used to construct the expression "field IS NULL".
func (c *Cond) IsNull(field string) string {
if len(field) == 0 {
return ""
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString(field)
ctx.WriteString(" IS NULL")
},
})
}
// IsNotNull is used to construct the expression "field IS NOT NULL".
func (c *Cond) IsNotNull(field string) string {
if len(field) == 0 {
return ""
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString(field)
ctx.WriteString(" IS NOT NULL")
},
})
}
// Between is used to construct the expression "field BETWEEN lower AND upper".
func (c *Cond) Between(field string, lower, upper interface{}) string {
if len(field) == 0 {
return ""
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString(field)
ctx.WriteString(" BETWEEN ")
ctx.WriteValue(lower)
ctx.WriteString(" AND ")
ctx.WriteValue(upper)
},
})
}
// NotBetween is used to construct the expression "field NOT BETWEEN lower AND upper".
func (c *Cond) NotBetween(field string, lower, upper interface{}) string {
if len(field) == 0 {
return ""
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString(field)
ctx.WriteString(" NOT BETWEEN ")
ctx.WriteValue(lower)
ctx.WriteString(" AND ")
ctx.WriteValue(upper)
},
})
}
// Or is used to construct the expression OR logic like "expr1 OR expr2 OR expr3".
func (c *Cond) Or(orExpr ...string) string {
orExpr = filterEmptyStrings(orExpr)
if len(orExpr) == 0 {
return ""
}
exprByteLen := estimateStringsBytes(orExpr)
if exprByteLen == 0 {
return ""
}
buf := newStringBuilder()
// Ensure that there is only 1 memory allocation.
size := len(lparen) + len(rparen) + (len(orExpr)-1)*len(opOR) + exprByteLen
buf.Grow(size)
buf.WriteString(lparen)
buf.WriteStrings(orExpr, opOR)
buf.WriteString(rparen)
return buf.String()
}
// And is used to construct the expression AND logic like "expr1 AND expr2 AND expr3".
func (c *Cond) And(andExpr ...string) string {
andExpr = filterEmptyStrings(andExpr)
if len(andExpr) == 0 {
return ""
}
exprByteLen := estimateStringsBytes(andExpr)
if exprByteLen == 0 {
return ""
}
buf := newStringBuilder()
// Ensure that there is only 1 memory allocation.
size := len(lparen) + len(rparen) + (len(andExpr)-1)*len(opAND) + exprByteLen
buf.Grow(size)
buf.WriteString(lparen)
buf.WriteStrings(andExpr, opAND)
buf.WriteString(rparen)
return buf.String()
}
// Not is used to construct the expression "NOT expr".
func (c *Cond) Not(notExpr string) string {
if len(notExpr) == 0 {
return ""
}
buf := newStringBuilder()
// Ensure that there is only 1 memory allocation.
size := len(opNOT) + len(notExpr)
buf.Grow(size)
buf.WriteString(opNOT)
buf.WriteString(notExpr)
return buf.String()
}
// Exists is used to construct the expression "EXISTS (subquery)".
func (c *Cond) Exists(subquery interface{}) string {
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString("EXISTS (")
ctx.WriteValue(subquery)
ctx.WriteString(")")
},
})
}
// NotExists is used to construct the expression "NOT EXISTS (subquery)".
func (c *Cond) NotExists(subquery interface{}) string {
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString("NOT EXISTS (")
ctx.WriteValue(subquery)
ctx.WriteString(")")
},
})
}
// Any is used to construct the expression "field op ANY (value...)".
func (c *Cond) Any(field, op string, values ...interface{}) string {
if len(field) == 0 || len(op) == 0 {
return ""
}
// Empty values means "false".
if len(values) == 0 {
return "0 = 1"
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString(field)
ctx.WriteString(" ")
ctx.WriteString(op)
ctx.WriteString(" ANY (")
ctx.WriteValues(values, ", ")
ctx.WriteString(")")
},
})
}
// All is used to construct the expression "field op ALL (value...)".
func (c *Cond) All(field, op string, values ...interface{}) string {
if len(field) == 0 || len(op) == 0 {
return ""
}
// Empty values means "false".
if len(values) == 0 {
return "0 = 1"
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString(field)
ctx.WriteString(" ")
ctx.WriteString(op)
ctx.WriteString(" ALL (")
ctx.WriteValues(values, ", ")
ctx.WriteString(")")
},
})
}
// Some is used to construct the expression "field op SOME (value...)".
func (c *Cond) Some(field, op string, values ...interface{}) string {
if len(field) == 0 || len(op) == 0 {
return ""
}
// Empty values means "false".
if len(values) == 0 {
return "0 = 1"
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
ctx.WriteString(field)
ctx.WriteString(" ")
ctx.WriteString(op)
ctx.WriteString(" SOME (")
ctx.WriteValues(values, ", ")
ctx.WriteString(")")
},
})
}
// IsDistinctFrom is used to construct the expression "field IS DISTINCT FROM value".
//
// When the database system does not support the IS DISTINCT FROM operator,
// the NotILike method will return "NOT field <=> value" for MySQL or a
// "CASE ... WHEN ... ELSE ... END" expression to simulate the behavior of
// the IS DISTINCT FROM operator.
func (c *Cond) IsDistinctFrom(field string, value interface{}) string {
if len(field) == 0 {
return ""
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
switch ctx.Flavor {
case PostgreSQL, SQLite, SQLServer:
ctx.WriteString(field)
ctx.WriteString(" IS DISTINCT FROM ")
ctx.WriteValue(value)
case MySQL:
ctx.WriteString("NOT ")
ctx.WriteString(field)
ctx.WriteString(" <=> ")
ctx.WriteValue(value)
default:
// CASE
// WHEN field IS NULL AND value IS NULL THEN 0
// WHEN field IS NOT NULL AND value IS NOT NULL AND field = value THEN 0
// ELSE 1
// END = 1
ctx.WriteString("CASE WHEN ")
ctx.WriteString(field)
ctx.WriteString(" IS NULL AND ")
ctx.WriteValue(value)
ctx.WriteString(" IS NULL THEN 0 WHEN ")
ctx.WriteString(field)
ctx.WriteString(" IS NOT NULL AND ")
ctx.WriteValue(value)
ctx.WriteString(" IS NOT NULL AND ")
ctx.WriteString(field)
ctx.WriteString(" = ")
ctx.WriteValue(value)
ctx.WriteString(" THEN 0 ELSE 1 END = 1")
}
},
})
}
// IsNotDistinctFrom is used to construct the expression "field IS NOT DISTINCT FROM value".
//
// When the database system does not support the IS NOT DISTINCT FROM operator,
// the NotILike method will return "field <=> value" for MySQL or a
// "CASE ... WHEN ... ELSE ... END" expression to simulate the behavior of
// the IS NOT DISTINCT FROM operator.
func (c *Cond) IsNotDistinctFrom(field string, value interface{}) string {
if len(field) == 0 {
return ""
}
return c.Var(condBuilder{
Builder: func(ctx *argsCompileContext) {
switch ctx.Flavor {
case PostgreSQL, SQLite, SQLServer:
ctx.WriteString(field)
ctx.WriteString(" IS NOT DISTINCT FROM ")
ctx.WriteValue(value)
case MySQL:
ctx.WriteString(field)
ctx.WriteString(" <=> ")
ctx.WriteValue(value)
default:
// CASE
// WHEN field IS NULL AND value IS NULL THEN 1
// WHEN field IS NOT NULL AND value IS NOT NULL AND field = value THEN 1
// ELSE 0
// END = 1
ctx.WriteString("CASE WHEN ")
ctx.WriteString(field)
ctx.WriteString(" IS NULL AND ")
ctx.WriteValue(value)
ctx.WriteString(" IS NULL THEN 1 WHEN ")
ctx.WriteString(field)
ctx.WriteString(" IS NOT NULL AND ")
ctx.WriteValue(value)
ctx.WriteString(" IS NOT NULL AND ")
ctx.WriteString(field)
ctx.WriteString(" = ")
ctx.WriteValue(value)
ctx.WriteString(" THEN 1 ELSE 0 END = 1")
}
},
})
}
// Var returns a placeholder for value.
func (c *Cond) Var(value interface{}) string {
return c.Args.Add(value)
}
type condBuilder struct {
Builder func(ctx *argsCompileContext)
}
func estimateStringsBytes(strs []string) (n int) {
for _, s := range strs {
n += len(s)
}
return
}
+175
View File
@@ -0,0 +1,175 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
import (
"strings"
)
const (
createTableMarkerInit injectionMarker = iota
createTableMarkerAfterCreate
createTableMarkerAfterDefine
createTableMarkerAfterOption
)
// NewCreateTableBuilder creates a new CREATE TABLE builder.
func NewCreateTableBuilder() *CreateTableBuilder {
return DefaultFlavor.NewCreateTableBuilder()
}
func newCreateTableBuilder() *CreateTableBuilder {
args := &Args{}
return &CreateTableBuilder{
verb: "CREATE TABLE",
args: args,
injection: newInjection(),
marker: createTableMarkerInit,
}
}
// CreateTableBuilder is a builder to build CREATE TABLE.
type CreateTableBuilder struct {
verb string
ifNotExists bool
table string
defs [][]string
options [][]string
args *Args
injection *injection
marker injectionMarker
}
var _ Builder = new(CreateTableBuilder)
// CreateTable sets the table name in CREATE TABLE.
func CreateTable(table string) *CreateTableBuilder {
return DefaultFlavor.NewCreateTableBuilder().CreateTable(table)
}
// CreateTable sets the table name in CREATE TABLE.
func (ctb *CreateTableBuilder) CreateTable(table string) *CreateTableBuilder {
ctb.table = Escape(table)
ctb.marker = createTableMarkerAfterCreate
return ctb
}
// CreateTempTable sets the table name and changes the verb of ctb to CREATE TEMPORARY TABLE.
func (ctb *CreateTableBuilder) CreateTempTable(table string) *CreateTableBuilder {
ctb.verb = "CREATE TEMPORARY TABLE"
ctb.table = Escape(table)
ctb.marker = createTableMarkerAfterCreate
return ctb
}
// IfNotExists adds IF NOT EXISTS before table name in CREATE TABLE.
func (ctb *CreateTableBuilder) IfNotExists() *CreateTableBuilder {
ctb.ifNotExists = true
return ctb
}
// Define adds definition of a column or index in CREATE TABLE.
func (ctb *CreateTableBuilder) Define(def ...string) *CreateTableBuilder {
ctb.defs = append(ctb.defs, def)
ctb.marker = createTableMarkerAfterDefine
return ctb
}
// Option adds a table option in CREATE TABLE.
func (ctb *CreateTableBuilder) Option(opt ...string) *CreateTableBuilder {
ctb.options = append(ctb.options, opt)
ctb.marker = createTableMarkerAfterOption
return ctb
}
// NumDefine returns the number of definitions in CREATE TABLE.
func (ctb *CreateTableBuilder) NumDefine() int {
return len(ctb.defs)
}
// String returns the compiled INSERT string.
func (ctb *CreateTableBuilder) String() string {
s, _ := ctb.Build()
return s
}
// Build returns compiled CREATE TABLE string and args.
// They can be used in `DB#Query` of package `database/sql` directly.
func (ctb *CreateTableBuilder) Build() (sql string, args []interface{}) {
return ctb.BuildWithFlavor(ctb.args.Flavor)
}
// BuildWithFlavor returns compiled CREATE TABLE string and args with flavor and initial args.
// They can be used in `DB#Query` of package `database/sql` directly.
func (ctb *CreateTableBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
buf := newStringBuilder()
ctb.injection.WriteTo(buf, createTableMarkerInit)
if len(ctb.verb) > 0 {
buf.WriteLeadingString(ctb.verb)
}
if ctb.ifNotExists {
buf.WriteLeadingString("IF NOT EXISTS")
}
if len(ctb.table) > 0 {
buf.WriteLeadingString(ctb.table)
}
ctb.injection.WriteTo(buf, createTableMarkerAfterCreate)
if len(ctb.defs) > 0 {
buf.WriteLeadingString("(")
defs := make([]string, 0, len(ctb.defs))
for _, def := range ctb.defs {
defs = append(defs, strings.Join(def, " "))
}
buf.WriteStrings(defs, ", ")
buf.WriteRune(')')
ctb.injection.WriteTo(buf, createTableMarkerAfterDefine)
}
if len(ctb.options) > 0 {
opts := make([]string, 0, len(ctb.options))
for _, opt := range ctb.options {
opts = append(opts, strings.Join(opt, " "))
}
buf.WriteLeadingString(strings.Join(opts, ", "))
ctb.injection.WriteTo(buf, createTableMarkerAfterOption)
}
return ctb.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
}
// SetFlavor sets the flavor of compiled sql.
func (ctb *CreateTableBuilder) SetFlavor(flavor Flavor) (old Flavor) {
old = ctb.args.Flavor
ctb.args.Flavor = flavor
return
}
// Flavor returns flavor of builder
func (ctb *CreateTableBuilder) Flavor() Flavor {
return ctb.args.Flavor
}
// Var returns a placeholder for value.
func (ctb *CreateTableBuilder) Var(arg interface{}) string {
return ctb.args.Add(arg)
}
// SQL adds an arbitrary sql to current position.
func (ctb *CreateTableBuilder) SQL(sql string) *CreateTableBuilder {
ctb.injection.SQL(ctb.marker, sql)
return ctb
}
+167
View File
@@ -0,0 +1,167 @@
// Copyright 2024 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
const (
cteMarkerInit injectionMarker = iota
cteMarkerAfterWith
)
// With creates a new CTE builder with default flavor.
func With(tables ...*CTEQueryBuilder) *CTEBuilder {
return DefaultFlavor.NewCTEBuilder().With(tables...)
}
// WithRecursive creates a new recursive CTE builder with default flavor.
func WithRecursive(tables ...*CTEQueryBuilder) *CTEBuilder {
return DefaultFlavor.NewCTEBuilder().WithRecursive(tables...)
}
func newCTEBuilder() *CTEBuilder {
return &CTEBuilder{
args: &Args{},
injection: newInjection(),
}
}
// CTEBuilder is a CTE (Common Table Expression) builder.
type CTEBuilder struct {
recursive bool
queries []*CTEQueryBuilder
queryBuilderVars []string
args *Args
injection *injection
marker injectionMarker
}
var _ Builder = new(CTEBuilder)
// With sets the CTE name and columns.
func (cteb *CTEBuilder) With(queries ...*CTEQueryBuilder) *CTEBuilder {
queryBuilderVars := make([]string, 0, len(queries))
for _, query := range queries {
queryBuilderVars = append(queryBuilderVars, cteb.args.Add(query))
}
cteb.queries = queries
cteb.queryBuilderVars = queryBuilderVars
cteb.marker = cteMarkerAfterWith
return cteb
}
// WithRecursive sets the CTE name and columns and turns on the RECURSIVE keyword.
func (cteb *CTEBuilder) WithRecursive(queries ...*CTEQueryBuilder) *CTEBuilder {
cteb.With(queries...).recursive = true
return cteb
}
// Select creates a new SelectBuilder to build a SELECT statement using this CTE.
func (cteb *CTEBuilder) Select(col ...string) *SelectBuilder {
sb := cteb.args.Flavor.NewSelectBuilder()
return sb.With(cteb).Select(col...)
}
// DeleteFrom creates a new DeleteBuilder to build a DELETE statement using this CTE.
func (cteb *CTEBuilder) DeleteFrom(table string) *DeleteBuilder {
db := cteb.args.Flavor.NewDeleteBuilder()
return db.With(cteb).DeleteFrom(table)
}
// Update creates a new UpdateBuilder to build an UPDATE statement using this CTE.
func (cteb *CTEBuilder) Update(table string) *UpdateBuilder {
ub := cteb.args.Flavor.NewUpdateBuilder()
return ub.With(cteb).Update(table)
}
// String returns the compiled CTE string.
func (cteb *CTEBuilder) String() string {
sql, _ := cteb.Build()
return sql
}
// Build returns compiled CTE string and args.
func (cteb *CTEBuilder) Build() (sql string, args []interface{}) {
return cteb.BuildWithFlavor(cteb.args.Flavor)
}
// BuildWithFlavor builds a CTE with the specified flavor and initial arguments.
func (cteb *CTEBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
buf := newStringBuilder()
cteb.injection.WriteTo(buf, cteMarkerInit)
if len(cteb.queryBuilderVars) > 0 {
buf.WriteLeadingString("WITH ")
if cteb.recursive {
buf.WriteString("RECURSIVE ")
}
buf.WriteStrings(cteb.queryBuilderVars, ", ")
}
cteb.injection.WriteTo(buf, cteMarkerAfterWith)
return cteb.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
}
// SetFlavor sets the flavor of compiled sql.
func (cteb *CTEBuilder) SetFlavor(flavor Flavor) (old Flavor) {
old = cteb.args.Flavor
cteb.args.Flavor = flavor
return
}
// Flavor returns flavor of builder
func (cteb *CTEBuilder) Flavor() Flavor {
return cteb.args.Flavor
}
// SQL adds an arbitrary sql to current position.
func (cteb *CTEBuilder) SQL(sql string) *CTEBuilder {
cteb.injection.SQL(cteb.marker, sql)
return cteb
}
// TableNames returns all table names in a CTE.
func (cteb *CTEBuilder) TableNames() []string {
if len(cteb.queryBuilderVars) == 0 {
return nil
}
tableNames := make([]string, 0, len(cteb.queries))
for _, query := range cteb.queries {
tableNames = append(tableNames, query.TableName())
}
return tableNames
}
// tableNamesForFrom returns a list of table names which should be automatically added to FROM clause.
// It's not public, as this feature is designed only for SelectBuilder/UpdateBuilder/DeleteBuilder right now.
func (cteb *CTEBuilder) tableNamesForFrom() []string {
cnt := 0
// ShouldAddToTableList() unlikely returns true.
// Count it before allocating any memory for better performance.
for _, query := range cteb.queries {
if query.ShouldAddToTableList() {
cnt++
}
}
if cnt == 0 {
return nil
}
tableNames := make([]string, 0, cnt)
for _, query := range cteb.queries {
if query.ShouldAddToTableList() {
tableNames = append(tableNames, query.TableName())
}
}
return tableNames
}
+141
View File
@@ -0,0 +1,141 @@
// Copyright 2024 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
const (
cteQueryMarkerInit injectionMarker = iota
cteQueryMarkerAfterTable
cteQueryMarkerAfterAs
)
// CTETable creates a new CTE query builder with default flavor, marking it as a table.
//
// The resulting CTE query can be used in a `SelectBuilder“, where its table name will be
// automatically included in the FROM clause.
func CTETable(name string, cols ...string) *CTEQueryBuilder {
return DefaultFlavor.NewCTEQueryBuilder().AddToTableList().Table(name, cols...)
}
// CTEQuery creates a new CTE query builder with default flavor.
func CTEQuery(name string, cols ...string) *CTEQueryBuilder {
return DefaultFlavor.NewCTEQueryBuilder().Table(name, cols...)
}
func newCTEQueryBuilder() *CTEQueryBuilder {
return &CTEQueryBuilder{
args: &Args{},
injection: newInjection(),
}
}
// CTEQueryBuilder is a builder to build one table in CTE (Common Table Expression).
type CTEQueryBuilder struct {
name string
cols []string
builderVar string
// if true, this query's table name will be automatically added to the table list
// in FROM clause of SELECT statement.
autoAddToTableList bool
args *Args
injection *injection
marker injectionMarker
}
var _ Builder = new(CTEQueryBuilder)
// CTETableBuilder is an alias of CTEQueryBuilder for backward compatibility.
//
// Deprecated: use CTEQueryBuilder instead.
type CTETableBuilder = CTEQueryBuilder
// Table sets the table name and columns in a CTE table.
func (ctetb *CTEQueryBuilder) Table(name string, cols ...string) *CTEQueryBuilder {
ctetb.name = name
ctetb.cols = cols
ctetb.marker = cteQueryMarkerAfterTable
return ctetb
}
// As sets the builder to select data.
func (ctetb *CTEQueryBuilder) As(builder Builder) *CTEQueryBuilder {
ctetb.builderVar = ctetb.args.Add(builder)
ctetb.marker = cteQueryMarkerAfterAs
return ctetb
}
// AddToTableList sets flag to add table name to table list in FROM clause of SELECT statement.
func (ctetb *CTEQueryBuilder) AddToTableList() *CTEQueryBuilder {
ctetb.autoAddToTableList = true
return ctetb
}
// ShouldAddToTableList returns flag to add table name to table list in FROM clause of SELECT statement.
func (ctetb *CTEQueryBuilder) ShouldAddToTableList() bool {
return ctetb.autoAddToTableList
}
// String returns the compiled CTE string.
func (ctetb *CTEQueryBuilder) String() string {
sql, _ := ctetb.Build()
return sql
}
// Build returns compiled CTE string and args.
func (ctetb *CTEQueryBuilder) Build() (sql string, args []interface{}) {
return ctetb.BuildWithFlavor(ctetb.args.Flavor)
}
// BuildWithFlavor builds a CTE with the specified flavor and initial arguments.
func (ctetb *CTEQueryBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
buf := newStringBuilder()
ctetb.injection.WriteTo(buf, cteQueryMarkerInit)
if ctetb.name != "" {
buf.WriteLeadingString(ctetb.name)
if len(ctetb.cols) > 0 {
buf.WriteLeadingString("(")
buf.WriteStrings(ctetb.cols, ", ")
buf.WriteString(")")
}
ctetb.injection.WriteTo(buf, cteQueryMarkerAfterTable)
}
if ctetb.builderVar != "" {
buf.WriteLeadingString("AS (")
buf.WriteString(ctetb.builderVar)
buf.WriteRune(')')
ctetb.injection.WriteTo(buf, cteQueryMarkerAfterAs)
}
return ctetb.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
}
// SetFlavor sets the flavor of compiled sql.
func (ctetb *CTEQueryBuilder) SetFlavor(flavor Flavor) (old Flavor) {
old = ctetb.args.Flavor
ctetb.args.Flavor = flavor
return
}
// Flavor returns flavor of builder
func (ctetb *CTEQueryBuilder) Flavor() Flavor {
return ctetb.args.Flavor
}
// SQL adds an arbitrary sql to current position.
func (ctetb *CTEQueryBuilder) SQL(sql string) *CTEQueryBuilder {
ctetb.injection.SQL(ctetb.marker, sql)
return ctetb
}
// TableName returns the CTE table name.
func (ctetb *CTEQueryBuilder) TableName() string {
return ctetb.name
}
+259
View File
@@ -0,0 +1,259 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
const (
deleteMarkerInit injectionMarker = iota
deleteMarkerAfterWith
deleteMarkerAfterDeleteFrom
deleteMarkerAfterWhere
deleteMarkerAfterOrderBy
deleteMarkerAfterLimit
deleteMarkerAfterReturning
)
// NewDeleteBuilder creates a new DELETE builder.
func NewDeleteBuilder() *DeleteBuilder {
return DefaultFlavor.NewDeleteBuilder()
}
func newDeleteBuilder() *DeleteBuilder {
args := &Args{}
proxy := &whereClauseProxy{}
return &DeleteBuilder{
whereClauseProxy: proxy,
whereClauseExpr: args.Add(proxy),
Cond: Cond{
Args: args,
},
args: args,
injection: newInjection(),
}
}
// DeleteBuilder is a builder to build DELETE.
type DeleteBuilder struct {
*WhereClause
Cond
whereClauseProxy *whereClauseProxy
whereClauseExpr string
cteBuilderVar string
cteBuilder *CTEBuilder
tables []string
orderByCols []string
order string
limitVar string
returning []string
args *Args
injection *injection
marker injectionMarker
}
var _ Builder = new(DeleteBuilder)
// DeleteFrom sets table name in DELETE.
func DeleteFrom(table ...string) *DeleteBuilder {
return DefaultFlavor.NewDeleteBuilder().DeleteFrom(table...)
}
// With sets WITH clause (the Common Table Expression) before DELETE.
func (db *DeleteBuilder) With(builder *CTEBuilder) *DeleteBuilder {
db.marker = deleteMarkerAfterWith
db.cteBuilderVar = db.Var(builder)
db.cteBuilder = builder
return db
}
// DeleteFrom sets table name in DELETE.
func (db *DeleteBuilder) DeleteFrom(table ...string) *DeleteBuilder {
db.tables = table
db.marker = deleteMarkerAfterDeleteFrom
return db
}
// TableNames returns all table names in this DELETE statement.
func (db *DeleteBuilder) TableNames() []string {
var additionalTableNames []string
if db.cteBuilder != nil {
additionalTableNames = db.cteBuilder.tableNamesForFrom()
}
var tableNames []string
if len(db.tables) > 0 && len(additionalTableNames) > 0 {
tableNames = make([]string, len(db.tables)+len(additionalTableNames))
copy(tableNames, db.tables)
copy(tableNames[len(db.tables):], additionalTableNames)
} else if len(db.tables) > 0 {
tableNames = db.tables
} else if len(additionalTableNames) > 0 {
tableNames = additionalTableNames
}
return tableNames
}
// Where sets expressions of WHERE in DELETE.
func (db *DeleteBuilder) Where(andExpr ...string) *DeleteBuilder {
if len(andExpr) == 0 || estimateStringsBytes(andExpr) == 0 {
return db
}
if db.WhereClause == nil {
db.WhereClause = NewWhereClause()
}
db.WhereClause.AddWhereExpr(db.args, andExpr...)
db.marker = deleteMarkerAfterWhere
return db
}
// AddWhereClause adds all clauses in the whereClause to SELECT.
func (db *DeleteBuilder) AddWhereClause(whereClause *WhereClause) *DeleteBuilder {
if db.WhereClause == nil {
db.WhereClause = NewWhereClause()
}
db.WhereClause.AddWhereClause(whereClause)
return db
}
// OrderBy sets columns of ORDER BY in DELETE.
func (db *DeleteBuilder) OrderBy(col ...string) *DeleteBuilder {
db.orderByCols = col
db.marker = deleteMarkerAfterOrderBy
return db
}
// Asc sets order of ORDER BY to ASC.
func (db *DeleteBuilder) Asc() *DeleteBuilder {
db.order = "ASC"
db.marker = deleteMarkerAfterOrderBy
return db
}
// Desc sets order of ORDER BY to DESC.
func (db *DeleteBuilder) Desc() *DeleteBuilder {
db.order = "DESC"
db.marker = deleteMarkerAfterOrderBy
return db
}
// Limit sets the LIMIT in DELETE.
func (db *DeleteBuilder) Limit(limit int) *DeleteBuilder {
if limit < 0 {
db.limitVar = ""
return db
}
db.limitVar = db.Var(limit)
db.marker = deleteMarkerAfterLimit
return db
}
// Returning sets returning columns.
// For DBMS that doesn't support RETURNING, e.g. MySQL, it will be ignored.
func (db *DeleteBuilder) Returning(col ...string) *DeleteBuilder {
db.returning = col
db.marker = deleteMarkerAfterReturning
return db
}
// String returns the compiled DELETE string.
func (db *DeleteBuilder) String() string {
s, _ := db.Build()
return s
}
// Build returns compiled DELETE string and args.
// They can be used in `DB#Query` of package `database/sql` directly.
func (db *DeleteBuilder) Build() (sql string, args []interface{}) {
return db.BuildWithFlavor(db.args.Flavor)
}
// BuildWithFlavor returns compiled DELETE string and args with flavor and initial args.
// They can be used in `DB#Query` of package `database/sql` directly.
func (db *DeleteBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
buf := newStringBuilder()
db.injection.WriteTo(buf, deleteMarkerInit)
if db.cteBuilder != nil {
buf.WriteLeadingString(db.cteBuilderVar)
db.injection.WriteTo(buf, deleteMarkerAfterWith)
}
tableNames := db.TableNames()
if len(tableNames) > 0 {
buf.WriteLeadingString("DELETE FROM ")
buf.WriteStrings(tableNames, ", ")
}
db.injection.WriteTo(buf, deleteMarkerAfterDeleteFrom)
if db.WhereClause != nil {
db.whereClauseProxy.WhereClause = db.WhereClause
defer func() {
db.whereClauseProxy.WhereClause = nil
}()
buf.WriteLeadingString(db.whereClauseExpr)
db.injection.WriteTo(buf, deleteMarkerAfterWhere)
}
if len(db.orderByCols) > 0 {
buf.WriteLeadingString("ORDER BY ")
buf.WriteStrings(db.orderByCols, ", ")
if db.order != "" {
buf.WriteRune(' ')
buf.WriteString(db.order)
}
db.injection.WriteTo(buf, deleteMarkerAfterOrderBy)
}
if len(db.limitVar) > 0 {
buf.WriteLeadingString("LIMIT ")
buf.WriteString(db.limitVar)
db.injection.WriteTo(buf, deleteMarkerAfterLimit)
}
if flavor == PostgreSQL || flavor == SQLite {
if len(db.returning) > 0 {
buf.WriteLeadingString("RETURNING ")
buf.WriteStrings(db.returning, ", ")
}
db.injection.WriteTo(buf, deleteMarkerAfterReturning)
}
return db.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
}
// SetFlavor sets the flavor of compiled sql.
func (db *DeleteBuilder) SetFlavor(flavor Flavor) (old Flavor) {
old = db.args.Flavor
db.args.Flavor = flavor
return
}
// Flavor returns flavor of builder
func (db *DeleteBuilder) Flavor() Flavor {
return db.args.Flavor
}
// SQL adds an arbitrary sql to current position.
func (db *DeleteBuilder) SQL(sql string) *DeleteBuilder {
db.injection.SQL(db.marker, sql)
return db
}
+5
View File
@@ -0,0 +1,5 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
// Package sqlbuilder is a flexible and powerful tool to build SQL string and associated args.
package sqlbuilder
+46
View File
@@ -0,0 +1,46 @@
package sqlbuilder
import (
"reflect"
"github.com/huandu/xstrings"
)
var (
// DefaultFieldMapper is the default field name to table column name mapper func.
// It's nil by default which means field name will be kept as it is.
//
// If a Struct has its own mapper func, the DefaultFieldMapper is ignored in this Struct.
// Field tag has precedence over all kinds of field mapper functions.
//
// Field mapper is called only once on a Struct when the Struct is used to create builder for the first time.
DefaultFieldMapper FieldMapperFunc
// DefaultGetAlias is the default alias and dbtag get func
DefaultGetAlias GetAliasFunc
)
func init() {
DefaultGetAlias = func(field *reflect.StructField) (alias string, dbtag string) {
dbtag = field.Tag.Get(DBTag)
alias = dbtag
return
}
}
// FieldMapperFunc is a func to map struct field names to column names,
// which will be used in query as columns.
type FieldMapperFunc func(name string) string
// SnakeCaseMapper is a field mapper which can convert field name from CamelCase to snake_case.
//
// For instance, it will convert "MyField" to "my_field".
//
// SnakeCaseMapper uses package "xstrings" to do the conversion.
// See https://pkg.go.dev/github.com/huandu/xstrings#ToSnakeCase for conversion rules.
func SnakeCaseMapper(field string) string {
return xstrings.ToSnakeCase(field)
}
// GetAliasFunc is a func to get alias and dbtag
type GetAliasFunc func(field *reflect.StructField) (alias string, dbtag string)
+210
View File
@@ -0,0 +1,210 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
import (
"errors"
"fmt"
)
// Supported flavors.
const (
invalidFlavor Flavor = iota
MySQL
PostgreSQL
SQLite
SQLServer
CQL
ClickHouse
Presto
Oracle
Informix
Doris
)
var (
// DefaultFlavor is the default flavor for all builders.
DefaultFlavor = MySQL
)
var (
// ErrInterpolateNotImplemented means the method or feature is not implemented right now.
ErrInterpolateNotImplemented = errors.New("go-sqlbuilder: interpolation for this flavor is not implemented")
// ErrInterpolateMissingArgs means there are some args missing in query, so it's not possible to
// prepare a query with such args.
ErrInterpolateMissingArgs = errors.New("go-sqlbuilder: not enough args when interpolating")
// ErrInterpolateUnsupportedArgs means that some types of the args are not supported.
ErrInterpolateUnsupportedArgs = errors.New("go-sqlbuilder: unsupported args when interpolating")
)
// Flavor is the flag to control the format of compiled sql.
type Flavor int
// String returns the name of f.
func (f Flavor) String() string {
switch f {
case MySQL:
return "MySQL"
case PostgreSQL:
return "PostgreSQL"
case SQLite:
return "SQLite"
case SQLServer:
return "SQLServer"
case CQL:
return "CQL"
case ClickHouse:
return "ClickHouse"
case Presto:
return "Presto"
case Oracle:
return "Oracle"
case Informix:
return "Informix"
case Doris:
return "Doris"
}
return "<invalid>"
}
// Interpolate parses sql returned by `Args#Compile` or `Builder`,
// and interpolate args to replace placeholders in the sql.
//
// If there are some args missing in sql, e.g. the number of placeholders are larger than len(args),
// returns ErrMissingArgs error.
func (f Flavor) Interpolate(sql string, args []interface{}) (string, error) {
switch f {
case MySQL:
return mysqlInterpolate(sql, args...)
case PostgreSQL:
return postgresqlInterpolate(sql, args...)
case SQLite:
return sqliteInterpolate(sql, args...)
case SQLServer:
return sqlserverInterpolate(sql, args...)
case CQL:
return cqlInterpolate(sql, args...)
case ClickHouse:
return clickhouseInterpolate(sql, args...)
case Presto:
return prestoInterpolate(sql, args...)
case Oracle:
return oracleInterpolate(sql, args...)
case Informix:
return informixInterpolate(sql, args...)
case Doris:
return dorisInterpolate(sql, args...)
}
return "", ErrInterpolateNotImplemented
}
// NewCreateTableBuilder creates a new CREATE TABLE builder with flavor.
func (f Flavor) NewCreateTableBuilder() *CreateTableBuilder {
b := newCreateTableBuilder()
b.SetFlavor(f)
return b
}
// NewDeleteBuilder creates a new DELETE builder with flavor.
func (f Flavor) NewDeleteBuilder() *DeleteBuilder {
b := newDeleteBuilder()
b.SetFlavor(f)
return b
}
// NewInsertBuilder creates a new INSERT builder with flavor.
func (f Flavor) NewInsertBuilder() *InsertBuilder {
b := newInsertBuilder()
b.SetFlavor(f)
return b
}
// NewSelectBuilder creates a new SELECT builder with flavor.
func (f Flavor) NewSelectBuilder() *SelectBuilder {
b := newSelectBuilder()
b.SetFlavor(f)
return b
}
// NewUpdateBuilder creates a new UPDATE builder with flavor.
func (f Flavor) NewUpdateBuilder() *UpdateBuilder {
b := newUpdateBuilder()
b.SetFlavor(f)
return b
}
// NewUnionBuilder creates a new UNION builder with flavor.
func (f Flavor) NewUnionBuilder() *UnionBuilder {
b := newUnionBuilder()
b.SetFlavor(f)
return b
}
// NewCTEBuilder creates a new CTE builder with flavor.
func (f Flavor) NewCTEBuilder() *CTEBuilder {
b := newCTEBuilder()
b.SetFlavor(f)
return b
}
// NewCTETableBuilder creates a new CTE table builder with flavor.
func (f Flavor) NewCTEQueryBuilder() *CTEQueryBuilder {
b := newCTEQueryBuilder()
b.SetFlavor(f)
return b
}
// Quote adds quote for name to make sure the name can be used safely
// as table name or field name.
//
// - For MySQL, use back quote (`) to quote name;
// - For PostgreSQL, SQL Server and SQLite, use double quote (") to quote name.
func (f Flavor) Quote(name string) string {
switch f {
case MySQL, ClickHouse, Doris:
return fmt.Sprintf("`%s`", name)
case PostgreSQL, SQLServer, SQLite, Presto, Oracle, Informix:
return fmt.Sprintf(`"%s"`, name)
case CQL:
return fmt.Sprintf("'%s'", name)
}
return name
}
// PrepareInsertIgnore prepares the insert builder to build insert ignore SQL statement based on the sql flavor
func (f Flavor) PrepareInsertIgnore(table string, ib *InsertBuilder) {
switch ib.args.Flavor {
case MySQL, Oracle:
ib.verb = "INSERT IGNORE"
case PostgreSQL:
// see https://www.postgresql.org/docs/current/sql-insert.html
ib.verb = "INSERT"
// add sql statement at the end after values, i.e. INSERT INTO ... ON CONFLICT DO NOTHING
ib.marker = insertMarkerAfterValues
ib.SQL("ON CONFLICT DO NOTHING")
case SQLite:
// see https://www.sqlite.org/lang_insert.html
ib.verb = "INSERT OR IGNORE"
case ClickHouse, CQL, SQLServer, Presto, Informix, Doris:
// All other databases do not support insert ignore
ib.verb = "INSERT"
default:
// panic if the db flavor is not supported
panic(fmt.Errorf("unsupported db flavor: %s", ib.args.Flavor.String()))
}
// Set the table and reset the marker right after insert into
ib.table = Escape(table)
ib.marker = insertMarkerAfterInsertInto
}
+37
View File
@@ -0,0 +1,37 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
// injection is a helper type to manage injected SQLs in all builders.
type injection struct {
markerSQLs map[injectionMarker][]string
}
type injectionMarker int
// newInjection creates a new injection.
func newInjection() *injection {
return &injection{
markerSQLs: map[injectionMarker][]string{},
}
}
// SQL adds sql to injection's sql list.
// All sqls inside injection is ordered by marker in ascending order.
func (injection *injection) SQL(marker injectionMarker, sql string) {
injection.markerSQLs[marker] = append(injection.markerSQLs[marker], sql)
}
// WriteTo joins all SQL strings at the same marker value with blank (" ")
// and writes the joined value to buf.
func (injection *injection) WriteTo(buf *stringBuilder, marker injectionMarker) {
sqls := injection.markerSQLs[marker]
if len(sqls) == 0 {
return
}
buf.WriteLeadingString("")
buf.WriteStrings(sqls, " ")
}
+246
View File
@@ -0,0 +1,246 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
import (
"fmt"
"strings"
)
const (
insertMarkerInit injectionMarker = iota
insertMarkerAfterInsertInto
insertMarkerAfterCols
insertMarkerAfterValues
insertMarkerAfterSelect
insertMarkerAfterReturning
)
// NewInsertBuilder creates a new INSERT builder.
func NewInsertBuilder() *InsertBuilder {
return DefaultFlavor.NewInsertBuilder()
}
func newInsertBuilder() *InsertBuilder {
args := &Args{}
return &InsertBuilder{
verb: "INSERT",
args: args,
injection: newInjection(),
}
}
// InsertBuilder is a builder to build INSERT.
type InsertBuilder struct {
verb string
table string
cols []string
values [][]string
returning []string
args *Args
injection *injection
marker injectionMarker
sbHolder string
}
var _ Builder = new(InsertBuilder)
// InsertInto sets table name in INSERT.
func InsertInto(table string) *InsertBuilder {
return DefaultFlavor.NewInsertBuilder().InsertInto(table)
}
// InsertInto sets table name in INSERT.
func (ib *InsertBuilder) InsertInto(table string) *InsertBuilder {
ib.table = Escape(table)
ib.marker = insertMarkerAfterInsertInto
return ib
}
// InsertIgnoreInto sets table name in INSERT IGNORE.
func InsertIgnoreInto(table string) *InsertBuilder {
return DefaultFlavor.NewInsertBuilder().InsertIgnoreInto(table)
}
// InsertIgnoreInto sets table name in INSERT IGNORE.
func (ib *InsertBuilder) InsertIgnoreInto(table string) *InsertBuilder {
ib.args.Flavor.PrepareInsertIgnore(table, ib)
return ib
}
// ReplaceInto sets table name and changes the verb of ib to REPLACE.
// REPLACE INTO is a MySQL extension to the SQL standard.
func ReplaceInto(table string) *InsertBuilder {
return DefaultFlavor.NewInsertBuilder().ReplaceInto(table)
}
// ReplaceInto sets table name and changes the verb of ib to REPLACE.
// REPLACE INTO is a MySQL extension to the SQL standard.
func (ib *InsertBuilder) ReplaceInto(table string) *InsertBuilder {
ib.verb = "REPLACE"
ib.table = Escape(table)
ib.marker = insertMarkerAfterInsertInto
return ib
}
// Cols sets columns in INSERT.
func (ib *InsertBuilder) Cols(col ...string) *InsertBuilder {
ib.cols = EscapeAll(col...)
ib.marker = insertMarkerAfterCols
return ib
}
// Select returns a new SelectBuilder to build a SELECT statement inside the INSERT INTO.
func (isb *InsertBuilder) Select(col ...string) *SelectBuilder {
sb := Select(col...)
isb.sbHolder = isb.args.Add(sb)
return sb
}
// Values adds a list of values for a row in INSERT.
func (ib *InsertBuilder) Values(value ...interface{}) *InsertBuilder {
placeholders := make([]string, 0, len(value))
for _, v := range value {
placeholders = append(placeholders, ib.args.Add(v))
}
ib.values = append(ib.values, placeholders)
ib.marker = insertMarkerAfterValues
return ib
}
// Returning sets returning columns.
// For DBMS that doesn't support RETURNING, e.g. MySQL, it will be ignored.
func (ib *InsertBuilder) Returning(col ...string) *InsertBuilder {
ib.returning = col
ib.marker = insertMarkerAfterReturning
return ib
}
// NumValue returns the number of values to insert.
func (ib *InsertBuilder) NumValue() int {
return len(ib.values)
}
// String returns the compiled INSERT string.
func (ib *InsertBuilder) String() string {
s, _ := ib.Build()
return s
}
// Build returns compiled INSERT string and args.
// They can be used in `DB#Query` of package `database/sql` directly.
func (ib *InsertBuilder) Build() (sql string, args []interface{}) {
return ib.BuildWithFlavor(ib.args.Flavor)
}
// BuildWithFlavor returns compiled INSERT string and args with flavor and initial args.
// They can be used in `DB#Query` of package `database/sql` directly.
func (ib *InsertBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
buf := newStringBuilder()
ib.injection.WriteTo(buf, insertMarkerInit)
if len(ib.values) > 1 && ib.args.Flavor == Oracle {
buf.WriteLeadingString(ib.verb)
buf.WriteString(" ALL")
for _, v := range ib.values {
if len(ib.table) > 0 {
buf.WriteString(" INTO ")
buf.WriteString(ib.table)
}
ib.injection.WriteTo(buf, insertMarkerAfterInsertInto)
if len(ib.cols) > 0 {
buf.WriteLeadingString("(")
buf.WriteStrings(ib.cols, ", ")
buf.WriteString(")")
ib.injection.WriteTo(buf, insertMarkerAfterCols)
}
buf.WriteLeadingString("VALUES ")
values := make([]string, 0, len(ib.values))
values = append(values, fmt.Sprintf("(%v)", strings.Join(v, ", ")))
buf.WriteStrings(values, ", ")
}
buf.WriteString(" SELECT 1 from DUAL")
ib.injection.WriteTo(buf, insertMarkerAfterValues)
return ib.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
}
if len(ib.table) > 0 {
buf.WriteLeadingString(ib.verb)
buf.WriteString(" INTO ")
buf.WriteString(ib.table)
}
ib.injection.WriteTo(buf, insertMarkerAfterInsertInto)
if len(ib.cols) > 0 {
buf.WriteLeadingString("(")
buf.WriteStrings(ib.cols, ", ")
buf.WriteString(")")
ib.injection.WriteTo(buf, insertMarkerAfterCols)
}
if ib.sbHolder != "" {
buf.WriteString(" ")
buf.WriteString(ib.sbHolder)
ib.injection.WriteTo(buf, insertMarkerAfterSelect)
} else if len(ib.values) > 0 {
buf.WriteLeadingString("VALUES ")
values := make([]string, 0, len(ib.values))
for _, v := range ib.values {
values = append(values, fmt.Sprintf("(%v)", strings.Join(v, ", ")))
}
buf.WriteStrings(values, ", ")
}
ib.injection.WriteTo(buf, insertMarkerAfterValues)
if flavor == PostgreSQL || flavor == SQLite {
if len(ib.returning) > 0 {
buf.WriteLeadingString("RETURNING ")
buf.WriteStrings(ib.returning, ", ")
}
ib.injection.WriteTo(buf, insertMarkerAfterReturning)
}
return ib.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
}
// SetFlavor sets the flavor of compiled sql.
func (ib *InsertBuilder) SetFlavor(flavor Flavor) (old Flavor) {
old = ib.args.Flavor
ib.args.Flavor = flavor
return
}
// Flavor returns flavor of builder
func (ib *InsertBuilder) Flavor() Flavor {
return ib.args.Flavor
}
// Var returns a placeholder for value.
func (ib *InsertBuilder) Var(arg interface{}) string {
return ib.args.Add(arg)
}
// SQL adds an arbitrary sql to current position.
func (ib *InsertBuilder) SQL(sql string) *InsertBuilder {
ib.injection.SQL(ib.marker, sql)
return ib
}
+827
View File
@@ -0,0 +1,827 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
import (
"database/sql/driver"
"fmt"
"reflect"
"strconv"
"time"
"unicode"
"unicode/utf8"
"unsafe"
)
// mysqlInterpolate parses query and replace all "?" with encoded args.
// If there are more "?" than len(args), returns ErrMissingArgs.
// Otherwise, if there are less "?" than len(args), the redundant args are omitted.
func mysqlInterpolate(query string, args ...interface{}) (string, error) {
return mysqlLikeInterpolate(MySQL, query, args...)
}
func mysqlLikeInterpolate(flavor Flavor, query string, args ...interface{}) (string, error) {
// Roughly estimate the size to avoid useless memory allocation and copy.
buf := make([]byte, 0, len(query)+len(args)*20)
var quote rune
var err error
cnt := 0
max := len(args)
escaping := false
offset := 0
target := query
r, sz := utf8.DecodeRuneInString(target)
for ; sz != 0; r, sz = utf8.DecodeRuneInString(target) {
offset += sz
target = query[offset:]
if escaping {
escaping = false
continue
}
switch r {
case '?':
if quote != 0 {
continue
}
if cnt >= max {
return "", ErrInterpolateMissingArgs
}
buf = append(buf, query[:offset-sz]...)
buf, err = encodeValue(buf, args[cnt], flavor)
if err != nil {
return "", err
}
query = target
offset = 0
cnt++
case '\'':
if quote == '\'' {
quote = 0
continue
}
if quote == 0 {
quote = '\''
}
case '"':
if quote == '"' {
quote = 0
continue
}
if quote == 0 {
quote = '"'
}
case '`':
if quote == '`' {
quote = 0
continue
}
if quote == 0 {
quote = '`'
}
case '\\':
if quote != 0 {
escaping = true
}
}
}
buf = append(buf, query...)
return *(*string)(unsafe.Pointer(&buf)), nil
}
// postgresqlInterpolate parses query and replace all "$*" with encoded args.
// If there are more "$*" than len(args), returns ErrMissingArgs.
// Otherwise, if there are less "$*" than len(args), the redundant args are omitted.
func postgresqlInterpolate(query string, args ...interface{}) (string, error) {
// Roughly estimate the size to avoid useless memory allocation and copy.
buf := make([]byte, 0, len(query)+len(args)*20)
var quote rune
var dollarQuote string
var err error
var idx int64
max := len(args)
escaping := false
offset := 0
target := query
r, sz := utf8.DecodeRuneInString(target)
for ; sz != 0; r, sz = utf8.DecodeRuneInString(target) {
offset += sz
target = query[offset:]
if escaping {
escaping = false
continue
}
switch r {
case '$':
if quote != 0 {
if quote != '$' {
continue
}
// Try to find the end of dollar quote.
pos := offset
for r, sz = utf8.DecodeRuneInString(target); sz != 0 && r != '$'; r, sz = utf8.DecodeRuneInString(target) {
pos += sz
target = query[pos:]
}
if sz == 0 {
break
}
if r == '$' {
dq := query[offset : pos+sz]
offset = pos
target = query[offset:]
if dq == dollarQuote {
quote = 0
dollarQuote = ""
offset += sz
target = query[offset:]
}
continue
}
continue
}
oldSz := sz
pos := offset
r, sz = utf8.DecodeRuneInString(target)
if '1' <= r && r <= '9' {
// A placeholder is found.
pos += sz
target = query[pos:]
for r, sz = utf8.DecodeRuneInString(target); sz != 0 && '0' <= r && r <= '9'; r, sz = utf8.DecodeRuneInString(target) {
pos += sz
target = query[pos:]
}
idx, err = strconv.ParseInt(query[offset:pos], 10, strconv.IntSize)
if err != nil {
return "", err
}
if int(idx) >= max+1 {
return "", ErrInterpolateMissingArgs
}
buf = append(buf, query[:offset-oldSz]...)
buf, err = encodeValue(buf, args[idx-1], PostgreSQL)
if err != nil {
return "", err
}
query = target
offset = 0
if sz == 0 {
break
}
continue
}
// Try to find the beginning of dollar quote.
for ; sz != 0 && r != '$' && unicode.IsLetter(r); r, sz = utf8.DecodeRuneInString(target) {
pos += sz
target = query[pos:]
}
if sz == 0 {
break
}
if !unicode.IsLetter(r) && r != '$' {
continue
}
pos += sz
quote = '$'
dollarQuote = query[offset:pos]
offset = pos
target = query[offset:]
case '\'':
if quote == '\'' {
// PostgreSQL uses two single quotes to represent one single quote.
r, sz = utf8.DecodeRuneInString(target)
if r == '\'' {
offset += sz
target = query[offset:]
continue
}
quote = 0
continue
}
if quote == 0 {
quote = '\''
}
case '"':
if quote == '"' {
quote = 0
continue
}
if quote == 0 {
quote = '"'
}
case '\\':
if quote == '\'' || quote == '"' {
escaping = true
}
}
}
buf = append(buf, query...)
return *(*string)(unsafe.Pointer(&buf)), nil
}
// sqlserverInterpolate parses query and replace all "@p*" with encoded args.
// If there are more "@p*" than len(args), returns ErrMissingArgs.
// Otherwise, if there are less "@p*" than len(args), the redundant args are omitted.
func sqlserverInterpolate(query string, args ...interface{}) (string, error) {
// Roughly estimate the size to avoid useless memory allocation and copy.
buf := make([]byte, 0, len(query)+len(args)*20)
var quote rune
var err error
var idx int64
max := len(args)
escaping := false
offset := 0
target := query
r, sz := utf8.DecodeRuneInString(target)
for ; sz != 0; r, sz = utf8.DecodeRuneInString(target) {
offset += sz
target = query[offset:]
if escaping {
escaping = false
continue
}
switch r {
case '@':
if quote != 0 {
continue
}
oldSz := sz
pos := offset
r, sz = utf8.DecodeRuneInString(target)
// Only parameters starting with @p or @P are interpolated.
if r != 'p' && r != 'P' {
continue
}
pos += sz
target = query[pos:]
r, sz = utf8.DecodeRuneInString(target)
if '1' <= r && r <= '9' {
// A placeholder is found.
pos += sz
target = query[pos:]
for r, sz = utf8.DecodeRuneInString(target); sz != 0 && '0' <= r && r <= '9'; r, sz = utf8.DecodeRuneInString(target) {
pos += sz
target = query[pos:]
}
idx, err = strconv.ParseInt(query[offset+1:pos], 10, strconv.IntSize)
if err != nil {
return "", err
}
if int(idx) >= max+1 {
return "", ErrInterpolateMissingArgs
}
buf = append(buf, query[:offset-oldSz]...)
buf, err = encodeValue(buf, args[idx-1], SQLServer)
if err != nil {
return "", err
}
query = target
offset = 0
if sz == 0 {
break
}
continue
}
case '\'':
if quote == '\'' {
quote = 0
continue
}
if quote == 0 {
quote = '\''
}
case '"':
if quote == '"' {
quote = 0
continue
}
if quote == 0 {
quote = '"'
}
case '\\':
if quote != 0 {
escaping = true
}
}
}
buf = append(buf, query...)
return *(*string)(unsafe.Pointer(&buf)), nil
}
// mysqlInterpolate works the same as MySQL interpolating.
func sqliteInterpolate(query string, args ...interface{}) (string, error) {
return mysqlLikeInterpolate(SQLite, query, args...)
}
// cqlInterpolate works the same as MySQL interpolating.
func cqlInterpolate(query string, args ...interface{}) (string, error) {
return mysqlLikeInterpolate(CQL, query, args...)
}
func clickhouseInterpolate(query string, args ...interface{}) (string, error) {
return mysqlLikeInterpolate(ClickHouse, query, args...)
}
func prestoInterpolate(query string, args ...interface{}) (string, error) {
return mysqlLikeInterpolate(Presto, query, args...)
}
func informixInterpolate(query string, args ...interface{}) (string, error) {
return mysqlLikeInterpolate(Informix, query, args...)
}
func dorisInterpolate(query string, args ...interface{}) (string, error) {
return mysqlLikeInterpolate(Doris, query, args...)
}
// oraclelInterpolate parses query and replace all ":*" with encoded args.
// If there are more ":*" than len(args), returns ErrMissingArgs.
// Otherwise, if there are less ":*" than len(args), the redundant args are omitted.
func oracleInterpolate(query string, args ...interface{}) (string, error) {
// Roughly estimate the size to avoid useless memory allocation and copy.
buf := make([]byte, 0, len(query)+len(args)*20)
var quote rune
var dollarQuote string
var err error
var idx int64
max := len(args)
escaping := false
offset := 0
target := query
r, sz := utf8.DecodeRuneInString(target)
for ; sz != 0; r, sz = utf8.DecodeRuneInString(target) {
offset += sz
target = query[offset:]
if escaping {
escaping = false
continue
}
switch r {
case ':':
if quote != 0 {
if quote != ':' {
continue
}
// Try to find the end of dollar quote.
pos := offset
for r, sz = utf8.DecodeRuneInString(target); sz != 0 && r != ':'; r, sz = utf8.DecodeRuneInString(target) {
pos += sz
target = query[pos:]
}
if sz == 0 {
break
}
if r == ':' {
dq := query[offset : pos+sz]
offset = pos
target = query[offset:]
if dq == dollarQuote {
quote = 0
dollarQuote = ""
offset += sz
target = query[offset:]
}
continue
}
continue
}
oldSz := sz
pos := offset
r, sz = utf8.DecodeRuneInString(target)
if '1' <= r && r <= '9' {
// A placeholder is found.
pos += sz
target = query[pos:]
for r, sz = utf8.DecodeRuneInString(target); sz != 0 && '0' <= r && r <= '9'; r, sz = utf8.DecodeRuneInString(target) {
pos += sz
target = query[pos:]
}
idx, err = strconv.ParseInt(query[offset:pos], 10, strconv.IntSize)
if err != nil {
return "", err
}
if int(idx) >= max+1 {
return "", ErrInterpolateMissingArgs
}
buf = append(buf, query[:offset-oldSz]...)
buf, err = encodeValue(buf, args[idx-1], Oracle)
if err != nil {
return "", err
}
query = target
offset = 0
if sz == 0 {
break
}
continue
}
// Try to find the beginning of dollar quote.
for ; sz != 0 && r != ':' && unicode.IsLetter(r); r, sz = utf8.DecodeRuneInString(target) {
pos += sz
target = query[pos:]
}
if sz == 0 {
break
}
if !unicode.IsLetter(r) && r != ':' {
continue
}
pos += sz
quote = ':'
dollarQuote = query[offset:pos]
offset = pos
target = query[offset:]
case '\'':
if quote == '\'' {
// PostgreSQL uses two single quotes to represent one single quote.
r, sz = utf8.DecodeRuneInString(target)
if r == '\'' {
offset += sz
target = query[offset:]
continue
}
quote = 0
continue
}
if quote == 0 {
quote = '\''
}
case '"':
if quote == '"' {
quote = 0
continue
}
if quote == 0 {
quote = '"'
}
case '\\':
if quote == '\'' || quote == '"' {
escaping = true
}
}
}
buf = append(buf, query...)
return *(*string)(unsafe.Pointer(&buf)), nil
}
func encodeValue(buf []byte, arg interface{}, flavor Flavor) ([]byte, error) {
switch v := arg.(type) {
case nil:
buf = append(buf, "NULL"...)
case driver.Valuer:
if val, err := v.Value(); err != nil {
return nil, err
} else {
return encodeValue(buf, val, flavor)
}
case time.Time:
if v.IsZero() {
buf = append(buf, "'0000-00-00'"...)
break
}
// In SQL standard, the precision of fractional seconds in time literal is up to 6 digits.
// Round up v.
v = v.Add(500 * time.Nanosecond)
switch flavor {
case MySQL, ClickHouse, Informix, Doris:
buf = append(buf, v.Format("'2006-01-02 15:04:05.999999'")...)
case PostgreSQL:
buf = append(buf, v.Format("'2006-01-02 15:04:05.999999 MST'")...)
case SQLite:
buf = append(buf, v.Format("'2006-01-02 15:04:05.000'")...)
case SQLServer:
buf = append(buf, v.Format("'2006-01-02 15:04:05.999999 Z07:00'")...)
case CQL:
buf = append(buf, v.Format("'2006-01-02 15:04:05.999999Z0700'")...)
case Presto:
buf = append(buf, v.Format("'2006-01-02 15:04:05.000'")...)
case Oracle:
buf = append(buf, "to_timestamp('"...)
buf = append(buf, v.Format("2006-01-02 15:04:05.999999")...)
buf = append(buf, "', 'YYYY-MM-DD HH24:MI:SS.FF')"...)
}
default:
primative := reflect.ValueOf(arg)
// Handle typed nil values (e.g. (*string)(nil), (*time.Time)(nil))
// This check must come before fmt.Stringer check since nil pointers may implement interfaces
if !primative.IsValid() || (primative.Kind() == reflect.Ptr && primative.IsNil()) {
buf = append(buf, "NULL"...)
return buf, nil
}
// Check for fmt.Stringer after nil pointer check
if stringer, ok := arg.(fmt.Stringer); ok {
buf = quoteStringValue(buf, stringer.String(), flavor)
return buf, nil
}
switch k := primative.Kind(); k {
case reflect.Bool:
switch flavor {
case Oracle:
if primative.Bool() {
buf = append(buf, '1')
} else {
buf = append(buf, '0')
}
default:
if primative.Bool() {
buf = append(buf, "TRUE"...)
} else {
buf = append(buf, "FALSE"...)
}
}
case reflect.Int:
buf = strconv.AppendInt(buf, primative.Int(), 10)
case reflect.Int8:
buf = strconv.AppendInt(buf, primative.Int(), 10)
case reflect.Int16:
buf = strconv.AppendInt(buf, primative.Int(), 10)
case reflect.Int32:
buf = strconv.AppendInt(buf, primative.Int(), 10)
case reflect.Int64:
buf = strconv.AppendInt(buf, primative.Int(), 10)
case reflect.Uint:
buf = strconv.AppendUint(buf, primative.Uint(), 10)
case reflect.Uint8:
buf = strconv.AppendUint(buf, primative.Uint(), 10)
case reflect.Uint16:
buf = strconv.AppendUint(buf, primative.Uint(), 10)
case reflect.Uint32:
buf = strconv.AppendUint(buf, primative.Uint(), 10)
case reflect.Uint64:
buf = strconv.AppendUint(buf, primative.Uint(), 10)
case reflect.Float32:
buf = strconv.AppendFloat(buf, primative.Float(), 'g', -1, 32)
case reflect.Float64:
buf = strconv.AppendFloat(buf, primative.Float(), 'g', -1, 64)
case reflect.String:
buf = quoteStringValue(buf, primative.String(), flavor)
case reflect.Slice, reflect.Array:
if k == reflect.Slice && primative.IsNil() {
buf = append(buf, "NULL"...)
break
}
if elem := primative.Type().Elem(); elem.Kind() != reflect.Uint8 {
return nil, ErrInterpolateUnsupportedArgs
}
var data []byte
// Bytes() will panic if primative is an array and cannot be addressed.
// Copy all bytes to data as a fallback.
if k == reflect.Array && !primative.CanAddr() {
l := primative.Len()
data = make([]byte, l)
for i := 0; i < l; i++ {
data[i] = byte(primative.Index(i).Uint())
}
} else {
data = primative.Bytes()
}
switch flavor {
case MySQL:
buf = append(buf, "_binary"...)
buf = quoteStringValue(buf, *(*string)(unsafe.Pointer(&data)), flavor)
case PostgreSQL:
buf = append(buf, "E'\\\\x"...)
buf = appendHex(buf, data)
buf = append(buf, "'::bytea"...)
case SQLite:
buf = append(buf, "X'"...)
buf = appendHex(buf, data)
buf = append(buf, '\'')
case SQLServer, CQL:
buf = append(buf, "0x"...)
buf = appendHex(buf, data)
case ClickHouse:
buf = append(buf, "unhex('"...)
buf = appendHex(buf, data)
buf = append(buf, "')"...)
case Presto:
buf = append(buf, "from_hex('"...)
buf = appendHex(buf, data)
buf = append(buf, "')"...)
case Oracle:
buf = append(buf, "hextoraw('"...)
buf = appendHex(buf, data)
buf = append(buf, "')"...)
default:
return nil, ErrInterpolateUnsupportedArgs
}
default:
return nil, ErrInterpolateUnsupportedArgs
}
}
return buf, nil
}
var hexDigits = [16]byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}
func appendHex(buf, v []byte) []byte {
for _, b := range v {
buf = append(buf, hexDigits[(b>>4)&0xF], hexDigits[b&0xF])
}
return buf
}
func quoteStringValue(buf []byte, s string, flavor Flavor) []byte {
switch flavor {
case PostgreSQL:
buf = append(buf, 'E')
case SQLServer:
buf = append(buf, 'N')
}
buf = append(buf, '\'')
r, sz := utf8.DecodeRuneInString(s)
for ; sz != 0; r, sz = utf8.DecodeRuneInString(s) {
switch r {
case '\x00':
buf = append(buf, "\\0"...)
case '\b':
buf = append(buf, "\\b"...)
case '\n':
buf = append(buf, "\\n"...)
case '\r':
buf = append(buf, "\\r"...)
case '\t':
buf = append(buf, "\\t"...)
case '\x1a':
buf = append(buf, "\\Z"...)
case '\'':
if flavor == CQL {
buf = append(buf, "''"...)
} else {
buf = append(buf, "\\'"...)
}
case '"':
buf = append(buf, "\\\""...)
case '\\':
buf = append(buf, "\\\\"...)
default:
buf = append(buf, s[:sz]...)
}
s = s[sz:]
}
buf = append(buf, '\'')
return buf
}
+125
View File
@@ -0,0 +1,125 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
import (
"reflect"
"strings"
)
// Escape replaces `$` with `$$` in ident.
func Escape(ident string) string {
return strings.Replace(ident, "$", "$$", -1)
}
// EscapeAll replaces `$` with `$$` in all strings of ident.
func EscapeAll(ident ...string) []string {
escaped := make([]string, 0, len(ident))
for _, i := range ident {
escaped = append(escaped, Escape(i))
}
return escaped
}
// Flatten recursively extracts values in slices and returns
// a flattened []interface{} with all values.
// If slices is not a slice, return `[]interface{}{slices}`.
func Flatten(slices interface{}) (flattened []interface{}) {
v := reflect.ValueOf(slices)
slices, flattened = flatten(v)
if slices != nil {
return []interface{}{slices}
}
return flattened
}
func flatten(v reflect.Value) (elem interface{}, flattened []interface{}) {
k := v.Kind()
for k == reflect.Interface {
v = v.Elem()
k = v.Kind()
}
if k != reflect.Slice && k != reflect.Array {
if !v.IsValid() || !v.CanInterface() {
return
}
elem = v.Interface()
return elem, nil
}
for i, l := 0, v.Len(); i < l; i++ {
e, f := flatten(v.Index(i))
if e == nil {
flattened = append(flattened, f...)
} else {
flattened = append(flattened, e)
}
}
return
}
type rawArgs struct {
expr string
}
// Raw marks the expr as a raw value which will not be added to args.
func Raw(expr string) interface{} {
return rawArgs{expr}
}
type listArgs struct {
args []interface{}
isTuple bool
}
// List marks arg as a list of data.
// If arg is `[]int{1, 2, 3}`, it will be compiled to `?, ?, ?` with args `[1 2 3]`.
func List(arg interface{}) interface{} {
return listArgs{
args: Flatten(arg),
}
}
// Tuple wraps values into a tuple and can be used as a single value.
func Tuple(values ...interface{}) interface{} {
return listArgs{
args: values,
isTuple: true,
}
}
// TupleNames joins names with tuple format.
// The names is not escaped. Use `EscapeAll` to escape them if necessary.
func TupleNames(names ...string) string {
buf := newStringBuilder()
buf.WriteRune('(')
buf.WriteStrings(names, ", ")
buf.WriteRune(')')
return buf.String()
}
type namedArgs struct {
name string
arg interface{}
}
// Named creates a named argument.
// Unlike `sql.Named`, this named argument works only with `Build` or `BuildNamed` for convenience
// and will be replaced to a `?` after `Compile`.
func Named(name string, arg interface{}) interface{} {
return namedArgs{
name: name,
arg: arg,
}
}
+599
View File
@@ -0,0 +1,599 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
import (
"fmt"
"strings"
)
const (
selectMarkerInit injectionMarker = iota
selectMarkerAfterWith
selectMarkerAfterSelect
selectMarkerAfterFrom
selectMarkerAfterJoin
selectMarkerAfterWhere
selectMarkerAfterGroupBy
selectMarkerAfterOrderBy
selectMarkerAfterLimit
selectMarkerAfterFor
)
// JoinOption is the option in JOIN.
type JoinOption string
// Join options.
const (
FullJoin JoinOption = "FULL"
FullOuterJoin JoinOption = "FULL OUTER"
InnerJoin JoinOption = "INNER"
LeftJoin JoinOption = "LEFT"
LeftOuterJoin JoinOption = "LEFT OUTER"
RightJoin JoinOption = "RIGHT"
RightOuterJoin JoinOption = "RIGHT OUTER"
)
// NewSelectBuilder creates a new SELECT builder.
func NewSelectBuilder() *SelectBuilder {
return DefaultFlavor.NewSelectBuilder()
}
func newSelectBuilder() *SelectBuilder {
args := &Args{}
proxy := &whereClauseProxy{}
return &SelectBuilder{
whereClauseProxy: proxy,
whereClauseExpr: args.Add(proxy),
Cond: Cond{
Args: args,
},
args: args,
injection: newInjection(),
}
}
// SelectBuilder is a builder to build SELECT.
type SelectBuilder struct {
*WhereClause
Cond
whereClauseProxy *whereClauseProxy
whereClauseExpr string
cteBuilderVar string
cteBuilder *CTEBuilder
distinct bool
tables []string
selectCols []string
joinOptions []JoinOption
joinTables []string
joinExprs [][]string
havingExprs []string
groupByCols []string
orderByCols []string
order string
limitVar string
offsetVar string
forWhat string
args *Args
injection *injection
marker injectionMarker
}
var _ Builder = new(SelectBuilder)
// Select sets columns in SELECT.
func Select(col ...string) *SelectBuilder {
return DefaultFlavor.NewSelectBuilder().Select(col...)
}
// TableNames returns all table names in this SELECT statement.
func (sb *SelectBuilder) TableNames() []string {
var additionalTableNames []string
if sb.cteBuilder != nil {
additionalTableNames = sb.cteBuilder.tableNamesForFrom()
}
var tableNames []string
if len(sb.tables) > 0 && len(additionalTableNames) > 0 {
tableNames = make([]string, len(sb.tables)+len(additionalTableNames))
copy(tableNames, sb.tables)
copy(tableNames[len(sb.tables):], additionalTableNames)
} else if len(sb.tables) > 0 {
tableNames = sb.tables
} else if len(additionalTableNames) > 0 {
tableNames = additionalTableNames
}
return tableNames
}
// With sets WITH clause (the Common Table Expression) before SELECT.
func (sb *SelectBuilder) With(builder *CTEBuilder) *SelectBuilder {
sb.marker = selectMarkerAfterWith
sb.cteBuilderVar = sb.Var(builder)
sb.cteBuilder = builder
return sb
}
// Select sets columns in SELECT.
func (sb *SelectBuilder) Select(col ...string) *SelectBuilder {
sb.selectCols = col
sb.marker = selectMarkerAfterSelect
return sb
}
// SelectMore adds more columns in SELECT.
func (sb *SelectBuilder) SelectMore(col ...string) *SelectBuilder {
sb.selectCols = append(sb.selectCols, col...)
sb.marker = selectMarkerAfterSelect
return sb
}
// Distinct marks this SELECT as DISTINCT.
func (sb *SelectBuilder) Distinct() *SelectBuilder {
sb.distinct = true
sb.marker = selectMarkerAfterSelect
return sb
}
// From sets table names in SELECT.
func (sb *SelectBuilder) From(table ...string) *SelectBuilder {
sb.tables = table
sb.marker = selectMarkerAfterFrom
return sb
}
// Join sets expressions of JOIN in SELECT.
//
// It builds a JOIN expression like
//
// JOIN table ON onExpr[0] AND onExpr[1] ...
func (sb *SelectBuilder) Join(table string, onExpr ...string) *SelectBuilder {
sb.marker = selectMarkerAfterJoin
return sb.JoinWithOption("", table, onExpr...)
}
// JoinWithOption sets expressions of JOIN with an option.
//
// It builds a JOIN expression like
//
// option JOIN table ON onExpr[0] AND onExpr[1] ...
//
// Here is a list of supported options.
// - FullJoin: FULL JOIN
// - FullOuterJoin: FULL OUTER JOIN
// - InnerJoin: INNER JOIN
// - LeftJoin: LEFT JOIN
// - LeftOuterJoin: LEFT OUTER JOIN
// - RightJoin: RIGHT JOIN
// - RightOuterJoin: RIGHT OUTER JOIN
func (sb *SelectBuilder) JoinWithOption(option JoinOption, table string, onExpr ...string) *SelectBuilder {
sb.joinOptions = append(sb.joinOptions, option)
sb.joinTables = append(sb.joinTables, table)
sb.joinExprs = append(sb.joinExprs, onExpr)
sb.marker = selectMarkerAfterJoin
return sb
}
// Where sets expressions of WHERE in SELECT.
func (sb *SelectBuilder) Where(andExpr ...string) *SelectBuilder {
if len(andExpr) == 0 || estimateStringsBytes(andExpr) == 0 {
return sb
}
if sb.WhereClause == nil {
sb.WhereClause = NewWhereClause()
}
sb.WhereClause.AddWhereExpr(sb.args, andExpr...)
sb.marker = selectMarkerAfterWhere
return sb
}
// AddWhereClause adds all clauses in the whereClause to SELECT.
func (sb *SelectBuilder) AddWhereClause(whereClause *WhereClause) *SelectBuilder {
if sb.WhereClause == nil {
sb.WhereClause = NewWhereClause()
}
sb.WhereClause.AddWhereClause(whereClause)
return sb
}
// Having sets expressions of HAVING in SELECT.
func (sb *SelectBuilder) Having(andExpr ...string) *SelectBuilder {
sb.havingExprs = append(sb.havingExprs, andExpr...)
sb.marker = selectMarkerAfterGroupBy
return sb
}
// GroupBy sets columns of GROUP BY in SELECT.
func (sb *SelectBuilder) GroupBy(col ...string) *SelectBuilder {
sb.groupByCols = append(sb.groupByCols, col...)
sb.marker = selectMarkerAfterGroupBy
return sb
}
// OrderBy sets columns of ORDER BY in SELECT.
func (sb *SelectBuilder) OrderBy(col ...string) *SelectBuilder {
sb.orderByCols = append(sb.orderByCols, col...)
sb.marker = selectMarkerAfterOrderBy
return sb
}
// Asc sets order of ORDER BY to ASC.
func (sb *SelectBuilder) Asc() *SelectBuilder {
sb.order = "ASC"
sb.marker = selectMarkerAfterOrderBy
return sb
}
// Desc sets order of ORDER BY to DESC.
func (sb *SelectBuilder) Desc() *SelectBuilder {
sb.order = "DESC"
sb.marker = selectMarkerAfterOrderBy
return sb
}
// Limit sets the LIMIT in SELECT.
func (sb *SelectBuilder) Limit(limit int) *SelectBuilder {
if limit < 0 {
sb.limitVar = ""
return sb
}
sb.limitVar = sb.Var(limit)
sb.marker = selectMarkerAfterLimit
return sb
}
// Offset sets the LIMIT offset in SELECT.
func (sb *SelectBuilder) Offset(offset int) *SelectBuilder {
if offset < 0 {
sb.offsetVar = ""
return sb
}
sb.offsetVar = sb.Var(offset)
sb.marker = selectMarkerAfterLimit
return sb
}
// ForUpdate adds FOR UPDATE at the end of SELECT statement.
func (sb *SelectBuilder) ForUpdate() *SelectBuilder {
sb.forWhat = "UPDATE"
sb.marker = selectMarkerAfterFor
return sb
}
// ForShare adds FOR SHARE at the end of SELECT statement.
func (sb *SelectBuilder) ForShare() *SelectBuilder {
sb.forWhat = "SHARE"
sb.marker = selectMarkerAfterFor
return sb
}
// As returns an AS expression.
func (sb *SelectBuilder) As(name, alias string) string {
return fmt.Sprintf("%s AS %s", name, alias)
}
// BuilderAs returns an AS expression wrapping a complex SQL.
// According to SQL syntax, SQL built by builder is surrounded by parens.
func (sb *SelectBuilder) BuilderAs(builder Builder, alias string) string {
return fmt.Sprintf("(%s) AS %s", sb.Var(builder), alias)
}
// LateralAs returns a LATERAL derived table expression wrapping a complex SQL.
func (sb *SelectBuilder) LateralAs(builder Builder, alias string) string {
return fmt.Sprintf("LATERAL (%s) AS %s", sb.Var(builder), alias)
}
// NumCol returns the number of columns to select.
func (sb *SelectBuilder) NumCol() int {
return len(sb.selectCols)
}
// String returns the compiled SELECT string.
func (sb *SelectBuilder) String() string {
s, _ := sb.Build()
return s
}
// Build returns compiled SELECT string and args.
// They can be used in `DB#Query` of package `database/sql` directly.
func (sb *SelectBuilder) Build() (sql string, args []interface{}) {
return sb.BuildWithFlavor(sb.args.Flavor)
}
// BuildWithFlavor returns compiled SELECT string and args with flavor and initial args.
// They can be used in `DB#Query` of package `database/sql` directly.
func (sb *SelectBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
buf := newStringBuilder()
sb.injection.WriteTo(buf, selectMarkerInit)
oraclePage := flavor == Oracle && (len(sb.limitVar) > 0 || len(sb.offsetVar) > 0)
if sb.cteBuilderVar != "" {
buf.WriteLeadingString(sb.cteBuilderVar)
sb.injection.WriteTo(buf, selectMarkerAfterWith)
}
if len(sb.selectCols) > 0 {
buf.WriteLeadingString("SELECT ")
if sb.distinct {
buf.WriteString("DISTINCT ")
}
if oraclePage {
var selectCols = make([]string, 0, len(sb.selectCols))
for i := range sb.selectCols {
cols := strings.SplitN(sb.selectCols[i], ".", 2)
if len(cols) == 1 {
selectCols = append(selectCols, cols[0])
} else {
selectCols = append(selectCols, cols[1])
}
}
buf.WriteStrings(selectCols, ", ")
} else {
buf.WriteStrings(sb.selectCols, ", ")
}
}
sb.injection.WriteTo(buf, selectMarkerAfterSelect)
if oraclePage {
if len(sb.selectCols) > 0 {
buf.WriteLeadingString("FROM (SELECT ")
if sb.distinct {
buf.WriteString("DISTINCT ")
}
var selectCols = make([]string, 0, len(sb.selectCols)+1)
selectCols = append(selectCols, "ROWNUM r")
for i := range sb.selectCols {
cols := strings.SplitN(sb.selectCols[i], ".", 2)
if len(cols) == 1 {
selectCols = append(selectCols, cols[0])
} else {
selectCols = append(selectCols, cols[1])
}
}
buf.WriteStrings(selectCols, ", ")
buf.WriteLeadingString("FROM (SELECT ")
buf.WriteStrings(sb.selectCols, ", ")
}
}
tableNames := sb.TableNames()
if len(tableNames) > 0 {
buf.WriteLeadingString("FROM ")
buf.WriteStrings(tableNames, ", ")
}
sb.injection.WriteTo(buf, selectMarkerAfterFrom)
for i := range sb.joinTables {
if option := sb.joinOptions[i]; option != "" {
buf.WriteLeadingString(string(option))
}
buf.WriteLeadingString("JOIN ")
buf.WriteString(sb.joinTables[i])
if exprs := filterEmptyStrings(sb.joinExprs[i]); len(exprs) > 0 {
buf.WriteString(" ON ")
buf.WriteStrings(exprs, " AND ")
}
}
if len(sb.joinTables) > 0 {
sb.injection.WriteTo(buf, selectMarkerAfterJoin)
}
if sb.WhereClause != nil {
sb.whereClauseProxy.WhereClause = sb.WhereClause
defer func() {
sb.whereClauseProxy.WhereClause = nil
}()
buf.WriteLeadingString(sb.whereClauseExpr)
sb.injection.WriteTo(buf, selectMarkerAfterWhere)
}
if len(sb.groupByCols) > 0 {
buf.WriteLeadingString("GROUP BY ")
buf.WriteStrings(sb.groupByCols, ", ")
if havingExprs := filterEmptyStrings(sb.havingExprs); len(havingExprs) > 0 {
buf.WriteString(" HAVING ")
buf.WriteStrings(havingExprs, " AND ")
}
sb.injection.WriteTo(buf, selectMarkerAfterGroupBy)
}
if len(sb.orderByCols) > 0 {
buf.WriteLeadingString("ORDER BY ")
buf.WriteStrings(sb.orderByCols, ", ")
if sb.order != "" {
buf.WriteRune(' ')
buf.WriteString(sb.order)
}
sb.injection.WriteTo(buf, selectMarkerAfterOrderBy)
}
switch flavor {
case MySQL, SQLite, ClickHouse:
if len(sb.limitVar) > 0 {
buf.WriteLeadingString("LIMIT ")
buf.WriteString(sb.limitVar)
if len(sb.offsetVar) > 0 {
buf.WriteLeadingString("OFFSET ")
buf.WriteString(sb.offsetVar)
}
}
case CQL:
if len(sb.limitVar) > 0 {
buf.WriteLeadingString("LIMIT ")
buf.WriteString(sb.limitVar)
}
case PostgreSQL:
if len(sb.limitVar) > 0 {
buf.WriteLeadingString("LIMIT ")
buf.WriteString(sb.limitVar)
}
if len(sb.offsetVar) > 0 {
buf.WriteLeadingString("OFFSET ")
buf.WriteString(sb.offsetVar)
}
case Presto:
// There might be a hidden constraint in Presto requiring offset to be set before limit.
// The select statement documentation (https://prestodb.io/docs/current/sql/select.html)
// puts offset before limit, and Trino, which is based on Presto, seems
// to require this specific order.
if len(sb.offsetVar) > 0 {
buf.WriteLeadingString("OFFSET ")
buf.WriteString(sb.offsetVar)
}
if len(sb.limitVar) > 0 {
buf.WriteLeadingString("LIMIT ")
buf.WriteString(sb.limitVar)
}
case SQLServer:
// If ORDER BY is not set, sort column #1 by default.
// It's required to make OFFSET...FETCH work.
if len(sb.orderByCols) == 0 && (len(sb.limitVar) > 0 || len(sb.offsetVar) > 0) {
buf.WriteLeadingString("ORDER BY 1")
}
if len(sb.offsetVar) > 0 {
buf.WriteLeadingString("OFFSET ")
buf.WriteString(sb.offsetVar)
buf.WriteString(" ROWS")
}
if len(sb.limitVar) > 0 {
if len(sb.offsetVar) == 0 {
buf.WriteLeadingString("OFFSET 0 ROWS")
}
buf.WriteLeadingString("FETCH NEXT ")
buf.WriteString(sb.limitVar)
buf.WriteString(" ROWS ONLY")
}
case Oracle:
if oraclePage {
buf.WriteString(") ")
if len(sb.tables) > 0 {
buf.WriteStrings(sb.tables, ", ")
}
buf.WriteString(") WHERE ")
if len(sb.limitVar) > 0 {
buf.WriteString("r BETWEEN ")
if len(sb.offsetVar) > 0 {
buf.WriteString(sb.offsetVar)
buf.WriteString(" + 1 AND ")
buf.WriteString(sb.limitVar)
buf.WriteString(" + ")
buf.WriteString(sb.offsetVar)
} else {
buf.WriteString("1 AND ")
buf.WriteString(sb.limitVar)
buf.WriteString(" + 1")
}
} else {
// As oraclePage is true, sb.offsetVar must not be empty.
buf.WriteString("r >= ")
buf.WriteString(sb.offsetVar)
buf.WriteString(" + 1")
}
}
case Informix:
// [SKIP N] FIRST M
// M must be greater than 0
if len(sb.limitVar) > 0 {
if len(sb.offsetVar) > 0 {
buf.WriteLeadingString("SKIP ")
buf.WriteString(sb.offsetVar)
}
buf.WriteLeadingString("FIRST ")
buf.WriteString(sb.limitVar)
}
case Doris:
// #192: Doris doesn't support ? in OFFSET and LIMIT.
if len(sb.limitVar) > 0 {
buf.WriteLeadingString("LIMIT ")
buf.WriteString(fmt.Sprint(sb.args.Value(sb.limitVar)))
if len(sb.offsetVar) > 0 {
buf.WriteLeadingString("OFFSET ")
buf.WriteString(fmt.Sprint(sb.args.Value(sb.offsetVar)))
}
}
}
if len(sb.limitVar) > 0 {
sb.injection.WriteTo(buf, selectMarkerAfterLimit)
}
if sb.forWhat != "" {
buf.WriteLeadingString("FOR ")
buf.WriteString(sb.forWhat)
sb.injection.WriteTo(buf, selectMarkerAfterFor)
}
return sb.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
}
// SetFlavor sets the flavor of compiled sql.
func (sb *SelectBuilder) SetFlavor(flavor Flavor) (old Flavor) {
old = sb.args.Flavor
sb.args.Flavor = flavor
return
}
// Flavor returns flavor of builder
func (sb *SelectBuilder) Flavor() Flavor {
return sb.args.Flavor
}
// SQL adds an arbitrary sql to current position.
func (sb *SelectBuilder) SQL(sql string) *SelectBuilder {
sb.injection.SQL(sb.marker, sql)
return sb
}
+103
View File
@@ -0,0 +1,103 @@
// Copyright 2023 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
import (
"io"
"strings"
)
type stringBuilder struct {
builder *strings.Builder
}
var _ io.Writer = new(stringBuilder)
func newStringBuilder() *stringBuilder {
return &stringBuilder{
builder: &strings.Builder{},
}
}
// WriteLeadingString writes s to internal buffer.
// If it's not the first time to write the string, a blank (" ") will be written before s.
func (sb *stringBuilder) WriteLeadingString(s string) {
if sb.builder.Len() > 0 {
sb.builder.WriteString(" ")
}
sb.builder.WriteString(s)
}
func (sb *stringBuilder) WriteString(s string) {
sb.builder.WriteString(s)
}
func (sb *stringBuilder) WriteStrings(ss []string, sep string) {
if len(ss) == 0 {
return
}
firstAdded := false
if len(ss[0]) != 0 {
sb.WriteString(ss[0])
firstAdded = true
}
for _, s := range ss[1:] {
if len(s) != 0 {
if firstAdded {
sb.WriteString(sep)
}
sb.WriteString(s)
firstAdded = true
}
}
}
func (sb *stringBuilder) WriteRune(r rune) {
sb.builder.WriteRune(r)
}
func (sb *stringBuilder) Write(data []byte) (int, error) {
return sb.builder.Write(data)
}
func (sb *stringBuilder) String() string {
return sb.builder.String()
}
func (sb *stringBuilder) Reset() {
sb.builder.Reset()
}
func (sb *stringBuilder) Grow(n int) {
sb.builder.Grow(n)
}
// filterEmptyStrings removes empty strings from ss.
// As ss rarely contains empty strings, filterEmptyStrings tries to avoid allocation if possible.
func filterEmptyStrings(ss []string) []string {
emptyStrings := 0
for _, s := range ss {
if len(s) == 0 {
emptyStrings++
}
}
if emptyStrings == 0 {
return ss
}
filtered := make([]string, 0, len(ss)-emptyStrings)
for _, s := range ss {
if len(s) != 0 {
filtered = append(filtered, s)
}
}
return filtered
}
+815
View File
@@ -0,0 +1,815 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
import (
"database/sql/driver"
"math"
"reflect"
"regexp"
"sort"
"strings"
)
var (
// DBTag is the struct tag to describe the name for a field in struct.
DBTag = "db"
// FieldTag is the struct tag to describe the tag name for a field in struct.
// Use "," to separate different tags.
FieldTag = "fieldtag"
// FieldOpt is the options for a struct field.
// As db column can contain "," in theory, field options should be provided in a separated tag.
FieldOpt = "fieldopt"
// FieldAs is the column alias (AS) for a struct field.
FieldAs = "fieldas"
)
const (
fieldOptWithQuote = "withquote"
fieldOptOmitEmpty = "omitempty"
optName = "optName"
optParams = "optParams"
)
var optRegex = regexp.MustCompile(`(?P<` + optName + `>\w+)(\((?P<` + optParams + `>.*)\))?`)
var typeOfSQLDriverValuer = reflect.TypeOf((*driver.Valuer)(nil)).Elem()
// Struct represents a struct type.
//
// All methods in Struct are thread-safe.
// We can define a global variable to hold a Struct and use it in any goroutine.
type Struct struct {
Flavor Flavor
structType reflect.Type
structFieldsParser structFieldsParser
withTags []string
withoutTags []string
}
var emptyStruct Struct
// NewStruct analyzes type information in structValue
// and creates a new Struct with all structValue fields.
// If structValue is not a struct, NewStruct returns a dummy Struct.
func NewStruct(structValue interface{}) *Struct {
t := reflect.TypeOf(structValue)
t = dereferencedType(t)
if t.Kind() != reflect.Struct {
return &emptyStruct
}
return &Struct{
Flavor: DefaultFlavor,
structType: t,
structFieldsParser: makeDefaultFieldsParser(t),
}
}
// For sets the default flavor of s and returns a shadow copy of s.
// The original s.Flavor is not changed.
func (s *Struct) For(flavor Flavor) *Struct {
c := *s
c.Flavor = flavor
return &c
}
// WithFieldMapper returns a new Struct based on s with custom field mapper.
// The original s is not changed.
func (s *Struct) WithFieldMapper(mapper FieldMapperFunc) *Struct {
if s.structType == nil {
return &emptyStruct
}
c := *s
c.structFieldsParser = makeCustomFieldsParser(s.structType, mapper)
return &c
}
// WithTag sets included tag(s) for all builder methods.
// For instance, calling s.WithTag("tag").SelectFrom("t") is to select all fields tagged with "tag" from table "t".
//
// If multiple tags are provided, fields tagged with any of them are included.
// That is, s.WithTag("tag1", "tag2").SelectFrom("t") is to select all fields tagged with "tag1" or "tag2" from table "t".
func (s *Struct) WithTag(tags ...string) *Struct {
if len(tags) == 0 {
return s
}
c := *s
c.mergeWithTags(tags)
return &c
}
func (s *Struct) mergeWithTags(with []string) {
newTags := make([]int, 0, len(with))
withTags := s.withTags
withoutTags := s.withoutTags
if len(withoutTags) == 0 {
for i, tag := range with {
if tag == "" {
continue
}
if !hasTag(withTags, tag) {
newTags = append(newTags, i)
}
}
} else {
for i, tag := range with {
if tag == "" {
continue
}
if !hasTag(withTags, tag) {
if !hasTag(withoutTags, tag) {
newTags = append(newTags, i)
}
}
}
}
if len(newTags) == 0 {
return
}
// Merge with tags.
withTags = make([]string, 0, len(s.withTags)+len(newTags))
withTags = append(withTags, s.withTags...)
for _, idx := range newTags {
withTags = append(withTags, with[idx])
}
sort.Strings(withTags)
withTags = removeDuplicatedTags(withTags)
s.withTags = withTags
}
// WithoutTag sets excluded tag(s) for all builder methods.
// For instance, calling s.WithoutTag("tag").SelectFrom("t") is to select all fields except those tagged with "tag" from table "t".
//
// If multiple tags are provided, fields tagged with any of them are excluded.
// That is, s.WithoutTag("tag1", "tag2").SelectFrom("t") is to exclude any field tagged with "tag1" or "tag2" from table "t".
func (s *Struct) WithoutTag(tags ...string) *Struct {
if len(tags) == 0 {
return s
}
c := *s
c.mergeWithoutTags(tags)
return &c
}
func (s *Struct) mergeWithoutTags(without []string) {
withTags := s.withTags
withoutTags := s.withoutTags
if len(withoutTags) == 0 {
withoutTags = make([]string, len(without))
copy(withoutTags, without)
} else {
newTags := make([]int, 0, len(without))
for i, tag := range without {
if tag == "" {
continue
}
if !hasTag(withoutTags, tag) {
newTags = append(newTags, i)
}
}
if len(newTags) == 0 {
return
}
// Merge without tags.
tags := make([]string, 0, len(withoutTags)+len(newTags))
tags = append(tags, withoutTags...)
for _, idx := range newTags {
tags = append(tags, without[idx])
}
withoutTags = tags
}
sort.Strings(withoutTags)
withoutTags = removeDuplicatedTags(withoutTags)
// Filter out useless tags in s.withTags.
kept := make([]int, 0, len(withTags))
for i, tag := range withTags {
if !hasTag(withoutTags, tag) {
kept = append(kept, i)
}
}
if len(kept) > 0 {
filteredTags := make([]string, 0, len(kept))
for _, i := range kept {
filteredTags = append(filteredTags, withTags[i])
}
withTags = filteredTags
} else {
withTags = nil
}
// Update with and without tags.
s.withTags = withTags
s.withoutTags = withoutTags
}
func hasTag(tags []string, tag string) bool {
if len(tags) == 0 {
return false
}
i := sort.SearchStrings(tags, tag)
return i < len(tags) && tags[i] == tag
}
func removeDuplicatedTags(tags []string) []string {
if len(tags) <= 1 {
return tags
}
// Unlikely to find any duplicates.
hasDupes := false
for i := 1; i < len(tags); i++ {
if tags[i] == tags[i-1] {
hasDupes = true
break
}
}
if !hasDupes {
return tags
}
unique := make([]string, 0, len(tags))
unique = append(unique, tags[0])
for i := 1; i < len(tags); i++ {
if tags[i] != tags[i-1] {
unique = append(unique, tags[i])
}
}
return unique
}
// SelectFrom creates a new `SelectBuilder` with table name.
// By default, all exported fields of the s are listed as columns in SELECT.
//
// Caller is responsible to set WHERE condition to find right record.
func (s *Struct) SelectFrom(table string) *SelectBuilder {
return s.selectFromWithTags(table, s.withTags, s.withoutTags)
}
// SelectFromForTag creates a new `SelectBuilder` with table name for a specified tag.
// By default, all fields of the s tagged with tag are listed as columns in SELECT.
//
// Caller is responsible to set WHERE condition to find right record.
//
// Deprecated: It's recommended to use s.WithTag(tag).SelectFrom(...) instead of calling this method.
// The former one is more readable and can be chained with other methods.
func (s *Struct) SelectFromForTag(table string, tag string) (sb *SelectBuilder) {
return s.selectFromWithTags(table, []string{tag}, nil)
}
func (s *Struct) selectFromWithTags(table string, with, without []string) (sb *SelectBuilder) {
sfs := s.structFieldsParser()
tagged := sfs.FilterTags(with, without)
sb = s.Flavor.NewSelectBuilder()
sb.From(table)
if tagged == nil {
sb.Select("*")
return
}
buf := newStringBuilder()
cols := make([]string, 0, len(tagged.ForRead))
tableAlias := parseTableAlias(table)
for _, sf := range tagged.ForRead {
if s.Flavor != CQL && !strings.ContainsRune(sf.Alias, '.') {
buf.WriteString(tableAlias)
buf.WriteRune('.')
}
buf.WriteString(sf.NameForSelect(s.Flavor))
cols = append(cols, buf.String())
buf.Reset()
}
sb.Select(cols...)
return sb
}
func parseTableAlias(table string) string {
idx := strings.LastIndex(table, " ")
if idx == -1 {
return table
}
return table[idx+1:]
}
// Update creates a new `UpdateBuilder` with table name.
// By default, all exported fields of the s is assigned in UPDATE with the field values from value.
// If value's type is not the same as that of s, Update returns a dummy `UpdateBuilder` with table name.
//
// Caller is responsible to set WHERE condition to match right record.
func (s *Struct) Update(table string, value interface{}) *UpdateBuilder {
return s.updateWithTags(table, s.withTags, s.withoutTags, value)
}
// UpdateForTag creates a new `UpdateBuilder` with table name.
// By default, all fields of the s tagged with tag is assigned in UPDATE with the field values from value.
// If value's type is not the same as that of s, UpdateForTag returns a dummy `UpdateBuilder` with table name.
//
// Caller is responsible to set WHERE condition to match right record.
//
// Deprecated: It's recommended to use s.WithTag(tag).Update(...) instead of calling this method.
// The former one is more readable and can be chained with other methods.
func (s *Struct) UpdateForTag(table string, tag string, value interface{}) *UpdateBuilder {
return s.updateWithTags(table, []string{tag}, nil, value)
}
func (s *Struct) updateWithTags(table string, with, without []string, value interface{}) *UpdateBuilder {
sfs := s.structFieldsParser()
tagged := sfs.FilterTags(with, without)
ub := s.Flavor.NewUpdateBuilder()
ub.Update(table)
if tagged == nil {
return ub
}
v := reflect.ValueOf(value)
v = dereferencedValue(v)
if v.Type() != s.structType {
return ub
}
assignments := make([]string, 0, len(tagged.ForWrite))
for _, sf := range tagged.ForWrite {
name := sf.Name
val := v.FieldByName(name)
if isEmptyValue(val) {
if sf.ShouldOmitEmpty(with...) {
continue
}
} else {
val = dereferencedFieldValue(val)
}
data := val.Interface()
assignments = append(assignments, ub.Assign(sf.Quote(s.Flavor), data))
}
ub.Set(assignments...)
return ub
}
// InsertInto creates a new `InsertBuilder` with table name using verb INSERT INTO.
// By default, all exported fields of s are set as columns by calling `InsertBuilder#Cols`,
// and value is added as a list of values by calling `InsertBuilder#Values`.
//
// InsertInto never returns any error.
// If the type of any item in value is not expected, it will be ignored.
// If value is an empty slice, `InsertBuilder#Values` will not be called.
func (s *Struct) InsertInto(table string, value ...interface{}) *InsertBuilder {
ib := s.Flavor.NewInsertBuilder()
ib.InsertInto(table)
s.buildColsAndValuesForTag(ib, s.withTags, s.withoutTags, value...)
return ib
}
// InsertIgnoreInto creates a new `InsertBuilder` with table name using verb INSERT IGNORE INTO.
// By default, all exported fields of s are set as columns by calling `InsertBuilder#Cols`,
// and value is added as a list of values by calling `InsertBuilder#Values`.
//
// InsertIgnoreInto never returns any error.
// If the type of any item in value is not expected, it will be ignored.
// If value is an empty slice, `InsertBuilder#Values` will not be called.
func (s *Struct) InsertIgnoreInto(table string, value ...interface{}) *InsertBuilder {
ib := s.Flavor.NewInsertBuilder()
ib.InsertIgnoreInto(table)
s.buildColsAndValuesForTag(ib, s.withTags, s.withoutTags, value...)
return ib
}
// ReplaceInto creates a new `InsertBuilder` with table name using verb REPLACE INTO.
// By default, all exported fields of s are set as columns by calling `InsertBuilder#Cols`,
// and value is added as a list of values by calling `InsertBuilder#Values`.
//
// ReplaceInto never returns any error.
// If the type of any item in value is not expected, it will be ignored.
// If value is an empty slice, `InsertBuilder#Values` will not be called.
func (s *Struct) ReplaceInto(table string, value ...interface{}) *InsertBuilder {
ib := s.Flavor.NewInsertBuilder()
ib.ReplaceInto(table)
s.buildColsAndValuesForTag(ib, s.withTags, s.withoutTags, value...)
return ib
}
// buildColsAndValuesForTag uses ib to set exported fields tagged with tag as columns
// and add value as a list of values.
func (s *Struct) buildColsAndValuesForTag(ib *InsertBuilder, with, without []string, value ...interface{}) {
sfs := s.structFieldsParser()
tagged := sfs.FilterTags(with, without)
if tagged == nil {
return
}
vs := make([]reflect.Value, 0, len(value))
for _, item := range value {
v := reflect.ValueOf(item)
v = dereferencedFieldValue(v)
if v.Type() == s.structType {
vs = append(vs, v)
}
}
if len(vs) == 0 {
return
}
cols := make([]string, 0, len(tagged.ForWrite))
values := make([][]interface{}, len(vs))
nilCols := make([]int, 0, len(tagged.ForWrite))
for _, sf := range tagged.ForWrite {
cols = append(cols, sf.Quote(s.Flavor))
name := sf.Name
shouldOmitEmpty := sf.ShouldOmitEmpty(with...)
nilCnt := 0
for i, v := range vs {
val := v.FieldByName(name)
if isEmptyValue(val) && shouldOmitEmpty {
nilCnt++
}
val = dereferencedFieldValue(val)
if val.IsValid() {
values[i] = append(values[i], val.Interface())
} else {
values[i] = append(values[i], nil)
}
}
nilCols = append(nilCols, nilCnt)
}
// Try to filter out nil values if possible.
filteredCols := make([]string, 0, len(cols))
filteredValues := make([][]interface{}, len(values))
for i, cnt := range nilCols {
// If all values are nil in a column, ignore the column completely.
if cnt == len(values) {
continue
}
filteredCols = append(filteredCols, cols[i])
for n, value := range values {
filteredValues[n] = append(filteredValues[n], value[i])
}
}
ib.Cols(filteredCols...)
for _, value := range filteredValues {
ib.Values(value...)
}
}
// InsertIntoForTag creates a new `InsertBuilder` with table name using verb INSERT INTO.
// By default, exported fields tagged with tag are set as columns by calling `InsertBuilder#Cols`,
// and value is added as a list of values by calling `InsertBuilder#Values`.
//
// InsertIntoForTag never returns any error.
// If the type of any item in value is not expected, it will be ignored.
// If value is an empty slice, `InsertBuilder#Values` will not be called.
//
// Deprecated: It's recommended to use s.WithTag(tag).InsertInto(...) instead of calling this method.
// The former one is more readable and can be chained with other methods.
func (s *Struct) InsertIntoForTag(table string, tag string, value ...interface{}) *InsertBuilder {
ib := s.Flavor.NewInsertBuilder()
ib.InsertInto(table)
s.buildColsAndValuesForTag(ib, []string{tag}, nil, value...)
return ib
}
// InsertIgnoreIntoForTag creates a new `InsertBuilder` with table name using verb INSERT IGNORE INTO.
// By default, exported fields tagged with tag are set as columns by calling `InsertBuilder#Cols`,
// and value is added as a list of values by calling `InsertBuilder#Values`.
//
// InsertIgnoreIntoForTag never returns any error.
// If the type of any item in value is not expected, it will be ignored.
// If value is an empty slice, `InsertBuilder#Values` will not be called.
//
// Deprecated: It's recommended to use s.WithTag(tag).InsertIgnoreInto(...) instead of calling this method.
// The former one is more readable and can be chained with other methods.
func (s *Struct) InsertIgnoreIntoForTag(table string, tag string, value ...interface{}) *InsertBuilder {
ib := s.Flavor.NewInsertBuilder()
ib.InsertIgnoreInto(table)
s.buildColsAndValuesForTag(ib, []string{tag}, nil, value...)
return ib
}
// ReplaceIntoForTag creates a new `InsertBuilder` with table name using verb REPLACE INTO.
// By default, exported fields tagged with tag are set as columns by calling `InsertBuilder#Cols`,
// and value is added as a list of values by calling `InsertBuilder#Values`.
//
// ReplaceIntoForTag never returns any error.
// If the type of any item in value is not expected, it will be ignored.
// If value is an empty slice, `InsertBuilder#Values` will not be called.
//
// Deprecated: It's recommended to use s.WithTag(tag).ReplaceInto(...) instead of calling this method.
// The former one is more readable and can be chained with other methods.
func (s *Struct) ReplaceIntoForTag(table string, tag string, value ...interface{}) *InsertBuilder {
ib := s.Flavor.NewInsertBuilder()
ib.ReplaceInto(table)
s.buildColsAndValuesForTag(ib, []string{tag}, nil, value...)
return ib
}
// DeleteFrom creates a new `DeleteBuilder` with table name.
//
// Caller is responsible to set WHERE condition to match right record.
func (s *Struct) DeleteFrom(table string) *DeleteBuilder {
db := s.Flavor.NewDeleteBuilder()
db.DeleteFrom(table)
return db
}
// Addr takes address of all exported fields of the s from the st.
// The returned result can be used in `Row#Scan` directly.
func (s *Struct) Addr(st interface{}) []interface{} {
return s.addrWithTags(s.withTags, s.withoutTags, st)
}
// AddrForTag takes address of all fields of the s tagged with tag from the st.
// The returned value can be used in `Row#Scan` directly.
//
// If tag is not defined in s in advance, returns nil.
//
// Deprecated: It's recommended to use s.WithTag(tag).Addr(...) instead of calling this method.
// The former one is more readable and can be chained with other methods.
func (s *Struct) AddrForTag(tag string, st interface{}) []interface{} {
return s.addrWithTags([]string{tag}, nil, st)
}
func (s *Struct) addrWithTags(with, without []string, st interface{}) []interface{} {
sfs := s.structFieldsParser()
tagged := sfs.FilterTags(with, without)
if tagged == nil {
return nil
}
return s.addrWithFields(tagged.ForRead, st)
}
// AddrWithCols takes address of all columns defined in cols from the st.
// The returned value can be used in `Row#Scan` directly.
func (s *Struct) AddrWithCols(cols []string, st interface{}) []interface{} {
sfs := s.structFieldsParser()
tagged := sfs.FilterTags(s.withTags, s.withoutTags)
if tagged == nil {
return nil
}
fields := tagged.Cols(cols)
if fields == nil {
return nil
}
return s.addrWithFields(fields, st)
}
func (s *Struct) addrWithFields(fields []*structField, st interface{}) []interface{} {
v := reflect.ValueOf(st)
v = dereferencedValue(v)
if v.Type() != s.structType {
return nil
}
addrs := make([]interface{}, 0, len(fields))
for _, sf := range fields {
name := sf.Name
data := v.FieldByName(name).Addr().Interface()
addrs = append(addrs, data)
}
return addrs
}
// Columns returns column names of s for all exported struct fields.
func (s *Struct) Columns() []string {
return s.columnsWithTags(s.withTags, s.withoutTags)
}
// ColumnsForTag returns column names of the s tagged with tag.
//
// Deprecated: It's recommended to use s.WithTag(tag).Columns(...) instead of calling this method.
// The former one is more readable and can be chained with other methods.
func (s *Struct) ColumnsForTag(tag string) (cols []string) {
return s.columnsWithTags([]string{tag}, nil)
}
func (s *Struct) columnsWithTags(with, without []string) (cols []string) {
sfs := s.structFieldsParser()
tagged := sfs.FilterTags(with, without)
if tagged == nil {
return
}
cols = make([]string, 0, len(tagged.ForWrite))
for _, sf := range tagged.ForWrite {
cols = append(cols, sf.Alias)
}
return
}
// Values returns a shadow copy of all exported fields in st.
func (s *Struct) Values(st interface{}) []interface{} {
return s.valuesWithTags(s.withTags, s.withoutTags, st)
}
// ValuesForTag returns a shadow copy of all fields tagged with tag in st.
//
// Deprecated: It's recommended to use s.WithTag(tag).Values(...) instead of calling this method.
// The former one is more readable and can be chained with other methods.
func (s *Struct) ValuesForTag(tag string, value interface{}) (values []interface{}) {
return s.valuesWithTags([]string{tag}, nil, value)
}
func (s *Struct) valuesWithTags(with, without []string, value interface{}) (values []interface{}) {
sfs := s.structFieldsParser()
tagged := sfs.FilterTags(with, without)
if tagged == nil {
return
}
v := reflect.ValueOf(value)
v = dereferencedValue(v)
if v.Type() != s.structType {
return
}
values = make([]interface{}, 0, len(tagged.ForWrite))
for _, sf := range tagged.ForWrite {
name := sf.Name
data := v.FieldByName(name).Interface()
values = append(values, data)
}
return
}
// ForeachRead foreach tags.
func (s *Struct) ForeachRead(trans func(dbtag string, isQuoted bool, field reflect.StructField)) {
s.foreachReadWithTags(s.withTags, s.withoutTags, trans)
}
func (s *Struct) foreachReadWithTags(with, without []string, trans func(dbtag string, isQuoted bool, field reflect.StructField)) {
sfs := s.structFieldsParser()
tagged := sfs.FilterTags(with, without)
if tagged == nil {
return
}
for _, sf := range tagged.ForRead {
trans(sf.DBTag, sf.IsQuoted, sf.Field)
}
}
// ForeachWrite foreach tags.
func (s *Struct) ForeachWrite(trans func(dbtag string, isQuoted bool, field reflect.StructField)) {
s.foreachWriteWithTags(s.withTags, s.withoutTags, trans)
}
func (s *Struct) foreachWriteWithTags(with, without []string, trans func(dbtag string, isQuoted bool, field reflect.StructField)) {
sfs := s.structFieldsParser()
tagged := sfs.FilterTags(with, without)
if tagged == nil {
return
}
for _, sf := range tagged.ForWrite {
trans(sf.DBTag, sf.IsQuoted, sf.Field)
}
}
func dereferencedType(t reflect.Type) reflect.Type {
for k := t.Kind(); k == reflect.Ptr || k == reflect.Interface; k = t.Kind() {
t = t.Elem()
}
return t
}
func dereferencedValue(v reflect.Value) reflect.Value {
for k := v.Kind(); k == reflect.Ptr || k == reflect.Interface; k = v.Kind() {
v = v.Elem()
}
return v
}
func dereferencedFieldValue(v reflect.Value) reflect.Value {
for k := v.Kind(); k == reflect.Ptr || k == reflect.Interface; k = v.Kind() {
if v.Type().Implements(typeOfSQLDriverValuer) {
break
}
v = v.Elem()
}
return v
}
// isEmptyValue checks if v is zero.
// Following code is borrowed from `IsZero` method in `reflect.Value` since Go 1.13.
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return math.Float64bits(v.Float()) == 0
case reflect.Complex64, reflect.Complex128:
c := v.Complex()
return math.Float64bits(real(c)) == 0 && math.Float64bits(imag(c)) == 0
case reflect.Array:
for i := 0; i < v.Len(); i++ {
if !isEmptyValue(v.Index(i)) {
return false
}
}
return true
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer:
return v.IsNil()
case reflect.String:
return v.Len() == 0
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
if !isEmptyValue(v.Field(i)) {
return false
}
}
return true
}
return false
}
+351
View File
@@ -0,0 +1,351 @@
package sqlbuilder
import (
"fmt"
"reflect"
"strings"
"sync"
)
type structFields struct {
noTag *structTaggedFields
tagged map[string]*structTaggedFields
}
type structTaggedFields struct {
// All columns for SELECT.
ForRead []*structField
colsForRead map[string]*structField
// All columns which can be used in INSERT and UPDATE.
ForWrite []*structField
colsForWrite map[string]struct{}
}
type structField struct {
Name string
Alias string
As string
Tags []string
IsQuoted bool
DBTag string
Field reflect.StructField
omitEmptyTags omitEmptyTagMap
}
type structFieldsParser func() *structFields
func makeDefaultFieldsParser(t reflect.Type) structFieldsParser {
return makeFieldsParser(t, nil, true)
}
func makeCustomFieldsParser(t reflect.Type, mapper FieldMapperFunc) structFieldsParser {
return makeFieldsParser(t, mapper, false)
}
func makeFieldsParser(t reflect.Type, mapper FieldMapperFunc, useDefault bool) structFieldsParser {
var once sync.Once
sfs := &structFields{
noTag: makeStructTaggedFields(),
tagged: map[string]*structTaggedFields{},
}
return func() *structFields {
once.Do(func() {
if useDefault {
mapper = DefaultFieldMapper
}
sfs.parse(t, mapper, "")
})
return sfs
}
}
func (sfs *structFields) parse(t reflect.Type, mapper FieldMapperFunc, prefix string) {
l := t.NumField()
var anonymous []reflect.StructField
for i := 0; i < l; i++ {
field := t.Field(i)
// Skip unexported fields that are not embedded structs.
if field.PkgPath != "" && !field.Anonymous {
continue
}
if field.Anonymous {
ft := field.Type
// If field is an anonymous struct or pointer to struct, parse it later.
if k := ft.Kind(); k == reflect.Struct || (k == reflect.Ptr && ft.Elem().Kind() == reflect.Struct) {
anonymous = append(anonymous, field)
continue
}
}
// Parse DBTag.
alias, dbtag := DefaultGetAlias(&field)
if alias == "-" {
continue
}
if alias == "" {
alias = field.Name
if mapper != nil {
alias = mapper(alias)
}
}
// Parse FieldOpt.
fieldopt := field.Tag.Get(FieldOpt)
opts := optRegex.FindAllString(fieldopt, -1)
isQuoted := false
omitEmptyTags := omitEmptyTagMap{}
for _, opt := range opts {
optMap := getOptMatchedMap(opt)
switch optMap[optName] {
case fieldOptOmitEmpty:
tags := getTagsFromOptParams(optMap[optParams])
for _, tag := range tags {
omitEmptyTags[tag] = struct{}{}
}
case fieldOptWithQuote:
isQuoted = true
}
}
// Parse FieldAs.
fieldas := field.Tag.Get(FieldAs)
// Parse FieldTag.
fieldtag := field.Tag.Get(FieldTag)
tags := splitTags(fieldtag)
// Make struct field.
structField := &structField{
Name: field.Name,
Alias: alias,
As: fieldas,
Tags: tags,
IsQuoted: isQuoted,
DBTag: dbtag,
Field: field,
omitEmptyTags: omitEmptyTags,
}
// Make sure all fields can be added to noTag without conflict.
sfs.noTag.Add(structField)
for _, tag := range tags {
sfs.taggedFields(tag).Add(structField)
}
}
for _, field := range anonymous {
ft := dereferencedType(field.Type)
sfs.parse(ft, mapper, prefix+field.Name+".")
}
}
func (sfs *structFields) FilterTags(with, without []string) *structTaggedFields {
if len(with) == 0 && len(without) == 0 {
return sfs.noTag
}
// Simply return the tagged fields.
if len(with) == 1 && len(without) == 0 {
return sfs.tagged[with[0]]
}
// Find out all with and without fields.
taggedFields := makeStructTaggedFields()
filteredReadFields := make(map[string]struct{}, len(sfs.noTag.colsForRead))
for _, tag := range without {
if field, ok := sfs.tagged[tag]; ok {
for k := range field.colsForRead {
filteredReadFields[k] = struct{}{}
}
}
}
if len(with) == 0 {
for _, field := range sfs.noTag.ForRead {
k := field.Key()
if _, ok := filteredReadFields[k]; !ok {
taggedFields.Add(field)
}
}
} else {
for _, tag := range with {
if fields, ok := sfs.tagged[tag]; ok {
for _, field := range fields.ForRead {
k := field.Key()
if _, ok := filteredReadFields[k]; !ok {
taggedFields.Add(field)
}
}
}
}
}
return taggedFields
}
func (sfs *structFields) taggedFields(tag string) *structTaggedFields {
fields, ok := sfs.tagged[tag]
if !ok {
fields = makeStructTaggedFields()
sfs.tagged[tag] = fields
}
return fields
}
func makeStructTaggedFields() *structTaggedFields {
return &structTaggedFields{
colsForRead: map[string]*structField{},
colsForWrite: map[string]struct{}{},
}
}
// Add a new field to stfs.
// If field's key exists in stfs.fields, the field is ignored.
func (stfs *structTaggedFields) Add(field *structField) {
key := field.Key()
if _, ok := stfs.colsForRead[key]; !ok {
stfs.colsForRead[key] = field
stfs.ForRead = append(stfs.ForRead, field)
}
key = field.Alias
if _, ok := stfs.colsForWrite[key]; !ok {
stfs.colsForWrite[key] = struct{}{}
stfs.ForWrite = append(stfs.ForWrite, field)
}
}
// Cols returns the fields whose key is one of cols.
// If any column in cols doesn't exist in sfs.fields, returns nil.
func (stfs *structTaggedFields) Cols(cols []string) []*structField {
fields := make([]*structField, 0, len(cols))
for _, col := range cols {
field := stfs.colsForRead[col]
if field == nil {
return nil
}
fields = append(fields, field)
}
return fields
}
// Key returns the key name to identify a field.
func (sf *structField) Key() string {
if sf.As != "" {
return sf.As
}
if sf.Alias != "" {
return sf.Alias
}
return sf.Name
}
// NameForSelect returns the name for SELECT.
func (sf *structField) NameForSelect(flavor Flavor) string {
if sf.As == "" {
return sf.Quote(flavor)
}
return fmt.Sprintf("%s AS %s", sf.Quote(flavor), sf.As)
}
// Quote the Alias in sf with flavor.
func (sf *structField) Quote(flavor Flavor) string {
if !sf.IsQuoted {
return sf.Alias
}
return flavor.Quote(sf.Alias)
}
// ShouldOmitEmpty returns true only if any one of tags is in the omitted tags map.
func (sf *structField) ShouldOmitEmpty(tags ...string) (ret bool) {
omit := sf.omitEmptyTags
if len(omit) == 0 {
return
}
// Always check default tag.
if _, ret = omit[""]; ret {
return
}
for _, tag := range tags {
if _, ret = omit[tag]; ret {
return
}
}
return
}
type omitEmptyTagMap map[string]struct{}
func getOptMatchedMap(opt string) (res map[string]string) {
res = map[string]string{}
sm := optRegex.FindStringSubmatch(opt)
for i, name := range optRegex.SubexpNames() {
if name != "" {
res[name] = sm[i]
}
}
return
}
func getTagsFromOptParams(opts string) (tags []string) {
tags = splitTags(opts)
if len(tags) == 0 {
tags = append(tags, "")
}
return
}
func splitTags(fieldtag string) (tags []string) {
parts := strings.Split(fieldtag, ",")
for _, v := range parts {
tag := strings.TrimSpace(v)
if tag == "" {
continue
}
tags = append(tags, tag)
}
return
}
+224
View File
@@ -0,0 +1,224 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
const (
unionDistinct = " UNION " // Default union type is DISTINCT.
unionAll = " UNION ALL "
)
const (
unionMarkerInit injectionMarker = iota
unionMarkerAfterUnion
unionMarkerAfterOrderBy
unionMarkerAfterLimit
)
// NewUnionBuilder creates a new UNION builder.
func NewUnionBuilder() *UnionBuilder {
return DefaultFlavor.NewUnionBuilder()
}
func newUnionBuilder() *UnionBuilder {
return &UnionBuilder{
args: &Args{},
injection: newInjection(),
}
}
// UnionBuilder is a builder to build UNION.
type UnionBuilder struct {
opt string
builderVars []string
orderByCols []string
order string
limitVar string
offsetVar string
args *Args
injection *injection
marker injectionMarker
}
var _ Builder = new(UnionBuilder)
// Union unions all builders together using UNION operator.
func Union(builders ...Builder) *UnionBuilder {
return DefaultFlavor.NewUnionBuilder().Union(builders...)
}
// Union unions all builders together using UNION operator.
func (ub *UnionBuilder) Union(builders ...Builder) *UnionBuilder {
return ub.union(unionDistinct, builders...)
}
// UnionAll unions all builders together using UNION ALL operator.
func UnionAll(builders ...Builder) *UnionBuilder {
return DefaultFlavor.NewUnionBuilder().UnionAll(builders...)
}
// UnionAll unions all builders together using UNION ALL operator.
func (ub *UnionBuilder) UnionAll(builders ...Builder) *UnionBuilder {
return ub.union(unionAll, builders...)
}
func (ub *UnionBuilder) union(opt string, builders ...Builder) *UnionBuilder {
builderVars := make([]string, 0, len(builders))
for _, b := range builders {
builderVars = append(builderVars, ub.Var(b))
}
ub.opt = opt
ub.builderVars = builderVars
ub.marker = unionMarkerAfterUnion
return ub
}
// OrderBy sets columns of ORDER BY in SELECT.
func (ub *UnionBuilder) OrderBy(col ...string) *UnionBuilder {
ub.orderByCols = col
ub.marker = unionMarkerAfterOrderBy
return ub
}
// Asc sets order of ORDER BY to ASC.
func (ub *UnionBuilder) Asc() *UnionBuilder {
ub.order = "ASC"
ub.marker = unionMarkerAfterOrderBy
return ub
}
// Desc sets order of ORDER BY to DESC.
func (ub *UnionBuilder) Desc() *UnionBuilder {
ub.order = "DESC"
ub.marker = unionMarkerAfterOrderBy
return ub
}
// Limit sets the LIMIT in SELECT.
func (ub *UnionBuilder) Limit(limit int) *UnionBuilder {
if limit < 0 {
ub.limitVar = ""
return ub
}
ub.limitVar = ub.Var(limit)
ub.marker = unionMarkerAfterLimit
return ub
}
// Offset sets the LIMIT offset in SELECT.
func (ub *UnionBuilder) Offset(offset int) *UnionBuilder {
if offset < 0 {
ub.offsetVar = ""
return ub
}
ub.offsetVar = ub.Var(offset)
ub.marker = unionMarkerAfterLimit
return ub
}
// String returns the compiled SELECT string.
func (ub *UnionBuilder) String() string {
s, _ := ub.Build()
return s
}
// Build returns compiled SELECT string and args.
// They can be used in `DB#Query` of package `database/sql` directly.
func (ub *UnionBuilder) Build() (sql string, args []interface{}) {
return ub.BuildWithFlavor(ub.args.Flavor)
}
// BuildWithFlavor returns compiled SELECT string and args with flavor and initial args.
// They can be used in `DB#Query` of package `database/sql` directly.
func (ub *UnionBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
buf := newStringBuilder()
ub.injection.WriteTo(buf, unionMarkerInit)
if len(ub.builderVars) > 0 {
needParen := flavor != SQLite
if needParen {
buf.WriteLeadingString("(")
buf.WriteString(ub.builderVars[0])
buf.WriteRune(')')
} else {
buf.WriteLeadingString(ub.builderVars[0])
}
for _, b := range ub.builderVars[1:] {
buf.WriteString(ub.opt)
if needParen {
buf.WriteRune('(')
}
buf.WriteString(b)
if needParen {
buf.WriteRune(')')
}
}
}
ub.injection.WriteTo(buf, unionMarkerAfterUnion)
if len(ub.orderByCols) > 0 {
buf.WriteLeadingString("ORDER BY ")
buf.WriteStrings(ub.orderByCols, ", ")
if ub.order != "" {
buf.WriteRune(' ')
buf.WriteString(ub.order)
}
ub.injection.WriteTo(buf, unionMarkerAfterOrderBy)
}
if len(ub.limitVar) > 0 {
buf.WriteLeadingString("LIMIT ")
buf.WriteString(ub.limitVar)
}
if ((MySQL == flavor || Informix == flavor) && len(ub.limitVar) > 0) || PostgreSQL == flavor {
if len(ub.offsetVar) > 0 {
buf.WriteLeadingString("OFFSET ")
buf.WriteString(ub.offsetVar)
}
}
if len(ub.limitVar) > 0 {
ub.injection.WriteTo(buf, unionMarkerAfterLimit)
}
return ub.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
}
// SetFlavor sets the flavor of compiled sql.
func (ub *UnionBuilder) SetFlavor(flavor Flavor) (old Flavor) {
old = ub.args.Flavor
ub.args.Flavor = flavor
return
}
// Flavor returns flavor of builder
func (ub *UnionBuilder) Flavor() Flavor {
return ub.args.Flavor
}
// Var returns a placeholder for value.
func (ub *UnionBuilder) Var(arg interface{}) string {
return ub.args.Add(arg)
}
// SQL adds an arbitrary sql to current position.
func (ub *UnionBuilder) SQL(sql string) *UnionBuilder {
ub.injection.SQL(ub.marker, sql)
return ub
}
+351
View File
@@ -0,0 +1,351 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
import (
"fmt"
)
const (
updateMarkerInit injectionMarker = iota
updateMarkerAfterWith
updateMarkerAfterUpdate
updateMarkerAfterSet
updateMarkerAfterWhere
updateMarkerAfterOrderBy
updateMarkerAfterLimit
updateMarkerAfterReturning
)
// NewUpdateBuilder creates a new UPDATE builder.
func NewUpdateBuilder() *UpdateBuilder {
return DefaultFlavor.NewUpdateBuilder()
}
func newUpdateBuilder() *UpdateBuilder {
args := &Args{}
proxy := &whereClauseProxy{}
return &UpdateBuilder{
whereClauseProxy: proxy,
whereClauseExpr: args.Add(proxy),
Cond: Cond{
Args: args,
},
args: args,
injection: newInjection(),
}
}
// UpdateBuilder is a builder to build UPDATE.
type UpdateBuilder struct {
*WhereClause
Cond
whereClauseProxy *whereClauseProxy
whereClauseExpr string
cteBuilderVar string
cteBuilder *CTEBuilder
tables []string
assignments []string
orderByCols []string
order string
limitVar string
returning []string
args *Args
injection *injection
marker injectionMarker
}
var _ Builder = new(UpdateBuilder)
// Update sets table name in UPDATE.
func Update(table ...string) *UpdateBuilder {
return DefaultFlavor.NewUpdateBuilder().Update(table...)
}
// With sets WITH clause (the Common Table Expression) before UPDATE.
func (ub *UpdateBuilder) With(builder *CTEBuilder) *UpdateBuilder {
ub.marker = updateMarkerAfterWith
ub.cteBuilderVar = ub.Var(builder)
ub.cteBuilder = builder
return ub
}
// Update sets table name in UPDATE.
func (ub *UpdateBuilder) Update(table ...string) *UpdateBuilder {
ub.tables = table
ub.marker = updateMarkerAfterUpdate
return ub
}
// TableNames returns all table names in this UPDATE statement.
func (ub *UpdateBuilder) TableNames() (tableNames []string) {
var additionalTableNames []string
if ub.cteBuilder != nil {
additionalTableNames = ub.cteBuilder.tableNamesForFrom()
}
if len(ub.tables) > 0 && len(additionalTableNames) > 0 {
tableNames = make([]string, len(ub.tables)+len(additionalTableNames))
copy(tableNames, ub.tables)
copy(tableNames[len(ub.tables):], additionalTableNames)
} else if len(ub.tables) > 0 {
tableNames = ub.tables
} else if len(additionalTableNames) > 0 {
tableNames = additionalTableNames
}
return tableNames
}
// Set sets the assignments in SET.
func (ub *UpdateBuilder) Set(assignment ...string) *UpdateBuilder {
ub.assignments = assignment
ub.marker = updateMarkerAfterSet
return ub
}
// SetMore appends the assignments in SET.
func (ub *UpdateBuilder) SetMore(assignment ...string) *UpdateBuilder {
ub.assignments = append(ub.assignments, assignment...)
ub.marker = updateMarkerAfterSet
return ub
}
// Where sets expressions of WHERE in UPDATE.
func (ub *UpdateBuilder) Where(andExpr ...string) *UpdateBuilder {
if len(andExpr) == 0 || estimateStringsBytes(andExpr) == 0 {
return ub
}
if ub.WhereClause == nil {
ub.WhereClause = NewWhereClause()
}
ub.WhereClause.AddWhereExpr(ub.args, andExpr...)
ub.marker = updateMarkerAfterWhere
return ub
}
// AddWhereClause adds all clauses in the whereClause to SELECT.
func (ub *UpdateBuilder) AddWhereClause(whereClause *WhereClause) *UpdateBuilder {
if ub.WhereClause == nil {
ub.WhereClause = NewWhereClause()
}
ub.WhereClause.AddWhereClause(whereClause)
return ub
}
// Assign represents SET "field = value" in UPDATE.
func (ub *UpdateBuilder) Assign(field string, value interface{}) string {
return fmt.Sprintf("%s = %s", Escape(field), ub.args.Add(value))
}
// Incr represents SET "field = field + 1" in UPDATE.
func (ub *UpdateBuilder) Incr(field string) string {
f := Escape(field)
return fmt.Sprintf("%s = %s + 1", f, f)
}
// Decr represents SET "field = field - 1" in UPDATE.
func (ub *UpdateBuilder) Decr(field string) string {
f := Escape(field)
return fmt.Sprintf("%s = %s - 1", f, f)
}
// Add represents SET "field = field + value" in UPDATE.
func (ub *UpdateBuilder) Add(field string, value interface{}) string {
f := Escape(field)
return fmt.Sprintf("%s = %s + %s", f, f, ub.args.Add(value))
}
// Sub represents SET "field = field - value" in UPDATE.
func (ub *UpdateBuilder) Sub(field string, value interface{}) string {
f := Escape(field)
return fmt.Sprintf("%s = %s - %s", f, f, ub.args.Add(value))
}
// Mul represents SET "field = field * value" in UPDATE.
func (ub *UpdateBuilder) Mul(field string, value interface{}) string {
f := Escape(field)
return fmt.Sprintf("%s = %s * %s", f, f, ub.args.Add(value))
}
// Div represents SET "field = field / value" in UPDATE.
func (ub *UpdateBuilder) Div(field string, value interface{}) string {
f := Escape(field)
return fmt.Sprintf("%s = %s / %s", f, f, ub.args.Add(value))
}
// OrderBy sets columns of ORDER BY in UPDATE.
func (ub *UpdateBuilder) OrderBy(col ...string) *UpdateBuilder {
ub.orderByCols = col
ub.marker = updateMarkerAfterOrderBy
return ub
}
// Asc sets order of ORDER BY to ASC.
func (ub *UpdateBuilder) Asc() *UpdateBuilder {
ub.order = "ASC"
ub.marker = updateMarkerAfterOrderBy
return ub
}
// Desc sets order of ORDER BY to DESC.
func (ub *UpdateBuilder) Desc() *UpdateBuilder {
ub.order = "DESC"
ub.marker = updateMarkerAfterOrderBy
return ub
}
// Limit sets the LIMIT in UPDATE.
func (ub *UpdateBuilder) Limit(limit int) *UpdateBuilder {
if limit < 0 {
ub.limitVar = ""
return ub
}
ub.limitVar = ub.Var(limit)
ub.marker = updateMarkerAfterLimit
return ub
}
// Returning sets returning columns.
// For DBMS that doesn't support RETURNING, e.g. MySQL, it will be ignored.
func (ub *UpdateBuilder) Returning(col ...string) *UpdateBuilder {
ub.returning = col
ub.marker = updateMarkerAfterReturning
return ub
}
// NumAssignment returns the number of assignments to update.
func (ub *UpdateBuilder) NumAssignment() int {
return len(ub.assignments)
}
// String returns the compiled UPDATE string.
func (ub *UpdateBuilder) String() string {
s, _ := ub.Build()
return s
}
// Build returns compiled UPDATE string and args.
// They can be used in `DB#Query` of package `database/sql` directly.
func (ub *UpdateBuilder) Build() (sql string, args []interface{}) {
return ub.BuildWithFlavor(ub.args.Flavor)
}
// BuildWithFlavor returns compiled UPDATE string and args with flavor and initial args.
// They can be used in `DB#Query` of package `database/sql` directly.
func (ub *UpdateBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
buf := newStringBuilder()
ub.injection.WriteTo(buf, updateMarkerInit)
if ub.cteBuilder != nil {
buf.WriteLeadingString(ub.cteBuilderVar)
ub.injection.WriteTo(buf, updateMarkerAfterWith)
}
switch flavor {
case MySQL:
// CTE table names should be written after UPDATE keyword in MySQL.
tableNames := ub.TableNames()
if len(tableNames) > 0 {
buf.WriteLeadingString("UPDATE ")
buf.WriteStrings(tableNames, ", ")
}
default:
if len(ub.tables) > 0 {
buf.WriteLeadingString("UPDATE ")
buf.WriteStrings(ub.tables, ", ")
}
}
ub.injection.WriteTo(buf, updateMarkerAfterUpdate)
if assignments := filterEmptyStrings(ub.assignments); len(assignments) > 0 {
buf.WriteLeadingString("SET ")
buf.WriteStrings(assignments, ", ")
}
ub.injection.WriteTo(buf, updateMarkerAfterSet)
if flavor != MySQL {
// For ISO SQL, CTE table names should be written after FROM keyword.
if ub.cteBuilder != nil {
cteTableNames := ub.cteBuilder.tableNamesForFrom()
if len(cteTableNames) > 0 {
buf.WriteLeadingString("FROM ")
buf.WriteStrings(cteTableNames, ", ")
}
}
}
if ub.WhereClause != nil {
ub.whereClauseProxy.WhereClause = ub.WhereClause
defer func() {
ub.whereClauseProxy.WhereClause = nil
}()
buf.WriteLeadingString(ub.whereClauseExpr)
ub.injection.WriteTo(buf, updateMarkerAfterWhere)
}
if len(ub.orderByCols) > 0 {
buf.WriteLeadingString("ORDER BY ")
buf.WriteStrings(ub.orderByCols, ", ")
if ub.order != "" {
buf.WriteLeadingString(ub.order)
}
ub.injection.WriteTo(buf, updateMarkerAfterOrderBy)
}
if len(ub.limitVar) > 0 {
buf.WriteLeadingString("LIMIT ")
buf.WriteString(ub.limitVar)
ub.injection.WriteTo(buf, updateMarkerAfterLimit)
}
if flavor == PostgreSQL || flavor == SQLite {
if len(ub.returning) > 0 {
buf.WriteLeadingString("RETURNING ")
buf.WriteStrings(ub.returning, ", ")
}
ub.injection.WriteTo(buf, updateMarkerAfterReturning)
}
return ub.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
}
// SetFlavor sets the flavor of compiled sql.
func (ub *UpdateBuilder) SetFlavor(flavor Flavor) (old Flavor) {
old = ub.args.Flavor
ub.args.Flavor = flavor
return
}
// Flavor returns flavor of builder
func (ub *UpdateBuilder) Flavor() Flavor {
return ub.args.Flavor
}
// SQL adds an arbitrary sql to current position.
func (ub *UpdateBuilder) SQL(sql string) *UpdateBuilder {
ub.injection.SQL(ub.marker, sql)
return ub
}
+140
View File
@@ -0,0 +1,140 @@
// Copyright 2018 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package sqlbuilder
// WhereClause is a Builder for WHERE clause.
// All builders which support `WHERE` clause have an anonymous `WhereClause` field,
// in which the conditions are stored.
//
// WhereClause can be shared among multiple builders.
// However, it is not thread-safe.
type WhereClause struct {
flavor Flavor
clauses []clause
}
var _ Builder = new(WhereClause)
// NewWhereClause creates a new WhereClause.
func NewWhereClause() *WhereClause {
return &WhereClause{}
}
// CopyWhereClause creates a copy of the whereClause.
func CopyWhereClause(whereClause *WhereClause) *WhereClause {
clauses := make([]clause, len(whereClause.clauses))
copy(clauses, whereClause.clauses)
return &WhereClause{
flavor: whereClause.flavor,
clauses: clauses,
}
}
type clause struct {
args *Args
andExprs []string
}
func (c *clause) Build(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
exprs := filterEmptyStrings(c.andExprs)
if len(exprs) == 0 {
return
}
buf := newStringBuilder()
buf.WriteStrings(exprs, " AND ")
sql, args = c.args.CompileWithFlavor(buf.String(), flavor, initialArg...)
return
}
// whereClauseProxy is a proxy for WhereClause.
// It's useful when the WhereClause in a build can be changed.
type whereClauseProxy struct {
*WhereClause
}
var _ Builder = new(whereClauseProxy)
// BuildWithFlavor builds a WHERE clause with the specified flavor and initial arguments.
func (wc *WhereClause) BuildWithFlavor(flavor Flavor, initialArg ...interface{}) (sql string, args []interface{}) {
if wc == nil || len(wc.clauses) == 0 {
return "", nil
}
buf := newStringBuilder()
buf.WriteLeadingString("WHERE ")
sql, args = wc.clauses[0].Build(flavor, initialArg...)
buf.WriteString(sql)
for _, clause := range wc.clauses[1:] {
buf.WriteString(" AND ")
sql, args = clause.Build(flavor, args...)
buf.WriteString(sql)
}
return buf.String(), args
}
// Build returns compiled WHERE clause string and args.
func (wc *WhereClause) Build() (sql string, args []interface{}) {
return wc.BuildWithFlavor(wc.flavor)
}
// SetFlavor sets the flavor of compiled sql.
// When the WhereClause belongs to a builder, the flavor of the builder will be used when building SQL.
func (wc *WhereClause) SetFlavor(flavor Flavor) (old Flavor) {
old = wc.flavor
wc.flavor = flavor
return
}
// Flavor returns flavor of clause
func (wc *WhereClause) Flavor() Flavor {
return wc.flavor
}
// AddWhereExpr adds an AND expression to WHERE clause with the specified arguments.
func (wc *WhereClause) AddWhereExpr(args *Args, andExpr ...string) *WhereClause {
if len(andExpr) == 0 {
return wc
}
andExprsBytesLen := estimateStringsBytes(andExpr)
if andExprsBytesLen == 0 {
return wc
}
// Merge with last clause if possible.
if len(wc.clauses) > 0 {
lastClause := &wc.clauses[len(wc.clauses)-1]
if lastClause.args == args {
lastClause.andExprs = append(lastClause.andExprs, andExpr...)
return wc
}
}
wc.clauses = append(wc.clauses, clause{
args: args,
andExprs: andExpr,
})
return wc
}
// AddWhereClause adds all clauses in the whereClause to the wc.
func (wc *WhereClause) AddWhereClause(whereClause *WhereClause) *WhereClause {
if wc == nil {
return nil
}
if whereClause == nil {
return wc
}
wc.clauses = append(wc.clauses, whereClause.clauses...)
return wc
}
+24
View File
@@ -0,0 +1,24 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
+23
View File
@@ -0,0 +1,23 @@
# Contributing #
Thanks for your contribution in advance. No matter what you will contribute to this project, pull request or bug report or feature discussion, it's always highly appreciated.
## New API or feature ##
I want to speak more about how to add new functions to this package.
Package `xstring` is a collection of useful string functions which should be implemented in Go. It's a bit subject to say which function should be included and which should not. I set up following rules in order to make it clear and as objective as possible.
* Rule 1: Only string algorithm, which takes string as input, can be included.
* Rule 2: If a function has been implemented in package `string`, it must not be included.
* Rule 3: If a function is not language neutral, it must not be included.
* Rule 4: If a function is a part of standard library in other languages, it can be included.
* Rule 5: If a function is quite useful in some famous framework or library, it can be included.
New function must be discussed in project issues before submitting any code. If a pull request with new functions is sent without any ref issue, it will be rejected.
## Pull request ##
Pull request is always welcome. Just make sure you have run `go fmt` and all test cases passed before submit.
If the pull request is to add a new API or feature, don't forget to update README.md and add new API in function list.
+22
View File
@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Huan Du
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+117
View File
@@ -0,0 +1,117 @@
# xstrings
[![Build Status](https://github.com/huandu/xstrings/workflows/Go/badge.svg)](https://github.com/huandu/xstrings/actions)
[![Go Doc](https://godoc.org/github.com/huandu/xstrings?status.svg)](https://pkg.go.dev/github.com/huandu/xstrings)
[![Go Report](https://goreportcard.com/badge/github.com/huandu/xstrings)](https://goreportcard.com/report/github.com/huandu/xstrings)
[![Coverage Status](https://coveralls.io/repos/github/huandu/xstrings/badge.svg?branch=master)](https://coveralls.io/github/huandu/xstrings?branch=master)
Go package [xstrings](https://godoc.org/github.com/huandu/xstrings) is a collection of string functions, which are widely used in other languages but absent in Go package [strings](http://golang.org/pkg/strings).
All functions are well tested and carefully tuned for performance.
## Propose a new function
Please review [contributing guideline](CONTRIBUTING.md) and [create new issue](https://github.com/huandu/xstrings/issues) to state why it should be included.
## Install
Use `go get` to install this library.
go get github.com/huandu/xstrings
## API document
See [GoDoc](https://godoc.org/github.com/huandu/xstrings) for full document.
## Function list
Go functions have a unique naming style. One, who has experience in other language but new in Go, may have difficulties to find out right string function to use.
Here is a list of functions in [strings](http://golang.org/pkg/strings) and [xstrings](https://godoc.org/github.com/huandu/xstrings) with enough extra information about how to map these functions to their friends in other languages. Hope this list could be helpful for fresh gophers.
### Package `xstrings` functions
_Keep this table sorted by Function in ascending order._
| Function | Friends | # |
| --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------- |
| [Center](https://godoc.org/github.com/huandu/xstrings#Center) | `str.center` in Python; `String#center` in Ruby | [#30](https://github.com/huandu/xstrings/issues/30) |
| [Count](https://godoc.org/github.com/huandu/xstrings#Count) | `String#count` in Ruby | [#16](https://github.com/huandu/xstrings/issues/16) |
| [Delete](https://godoc.org/github.com/huandu/xstrings#Delete) | `String#delete` in Ruby | [#17](https://github.com/huandu/xstrings/issues/17) |
| [ExpandTabs](https://godoc.org/github.com/huandu/xstrings#ExpandTabs) | `str.expandtabs` in Python | [#27](https://github.com/huandu/xstrings/issues/27) |
| [FirstRuneToLower](https://godoc.org/github.com/huandu/xstrings#FirstRuneToLower) | `lcfirst` in PHP or Perl | [#15](https://github.com/huandu/xstrings/issues/15) |
| [FirstRuneToUpper](https://godoc.org/github.com/huandu/xstrings#FirstRuneToUpper) | `String#capitalize` in Ruby; `ucfirst` in PHP or Perl | [#15](https://github.com/huandu/xstrings/issues/15) |
| [Insert](https://godoc.org/github.com/huandu/xstrings#Insert) | `String#insert` in Ruby | [#18](https://github.com/huandu/xstrings/issues/18) |
| [LastPartition](https://godoc.org/github.com/huandu/xstrings#LastPartition) | `str.rpartition` in Python; `String#rpartition` in Ruby | [#19](https://github.com/huandu/xstrings/issues/19) |
| [LeftJustify](https://godoc.org/github.com/huandu/xstrings#LeftJustify) | `str.ljust` in Python; `String#ljust` in Ruby | [#28](https://github.com/huandu/xstrings/issues/28) |
| [Len](https://godoc.org/github.com/huandu/xstrings#Len) | `mb_strlen` in PHP | [#23](https://github.com/huandu/xstrings/issues/23) |
| [Partition](https://godoc.org/github.com/huandu/xstrings#Partition) | `str.partition` in Python; `String#partition` in Ruby | [#10](https://github.com/huandu/xstrings/issues/10) |
| [Reverse](https://godoc.org/github.com/huandu/xstrings#Reverse) | `String#reverse` in Ruby; `strrev` in PHP; `reverse` in Perl | [#7](https://github.com/huandu/xstrings/issues/7) |
| [RightJustify](https://godoc.org/github.com/huandu/xstrings#RightJustify) | `str.rjust` in Python; `String#rjust` in Ruby | [#29](https://github.com/huandu/xstrings/issues/29) |
| [RuneWidth](https://godoc.org/github.com/huandu/xstrings#RuneWidth) | - | [#27](https://github.com/huandu/xstrings/issues/27) |
| [Scrub](https://godoc.org/github.com/huandu/xstrings#Scrub) | `String#scrub` in Ruby | [#20](https://github.com/huandu/xstrings/issues/20) |
| [Shuffle](https://godoc.org/github.com/huandu/xstrings#Shuffle) | `str_shuffle` in PHP | [#13](https://github.com/huandu/xstrings/issues/13) |
| [ShuffleSource](https://godoc.org/github.com/huandu/xstrings#ShuffleSource) | `str_shuffle` in PHP | [#13](https://github.com/huandu/xstrings/issues/13) |
| [Slice](https://godoc.org/github.com/huandu/xstrings#Slice) | `mb_substr` in PHP | [#9](https://github.com/huandu/xstrings/issues/9) |
| [Squeeze](https://godoc.org/github.com/huandu/xstrings#Squeeze) | `String#squeeze` in Ruby | [#11](https://github.com/huandu/xstrings/issues/11) |
| [Successor](https://godoc.org/github.com/huandu/xstrings#Successor) | `String#succ` or `String#next` in Ruby | [#22](https://github.com/huandu/xstrings/issues/22) |
| [SwapCase](https://godoc.org/github.com/huandu/xstrings#SwapCase) | `str.swapcase` in Python; `String#swapcase` in Ruby | [#12](https://github.com/huandu/xstrings/issues/12) |
| [ToCamelCase](https://godoc.org/github.com/huandu/xstrings#ToCamelCase) | `String#camelize` in RoR | [#1](https://github.com/huandu/xstrings/issues/1) |
| [ToKebab](https://godoc.org/github.com/huandu/xstrings#ToKebabCase) | - | [#41](https://github.com/huandu/xstrings/issues/41) |
| [ToSnakeCase](https://godoc.org/github.com/huandu/xstrings#ToSnakeCase) | `String#underscore` in RoR | [#1](https://github.com/huandu/xstrings/issues/1) |
| [Translate](https://godoc.org/github.com/huandu/xstrings#Translate) | `str.translate` in Python; `String#tr` in Ruby; `strtr` in PHP; `tr///` in Perl | [#21](https://github.com/huandu/xstrings/issues/21) |
| [Width](https://godoc.org/github.com/huandu/xstrings#Width) | `mb_strwidth` in PHP | [#26](https://github.com/huandu/xstrings/issues/26) |
| [WordCount](https://godoc.org/github.com/huandu/xstrings#WordCount) | `str_word_count` in PHP | [#14](https://github.com/huandu/xstrings/issues/14) |
| [WordSplit](https://godoc.org/github.com/huandu/xstrings#WordSplit) | - | [#14](https://github.com/huandu/xstrings/issues/14) |
### Package `strings` functions
_Keep this table sorted by Function in ascending order._
| Function | Friends |
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| [Contains](http://golang.org/pkg/strings/#Contains) | `String#include?` in Ruby |
| [ContainsAny](http://golang.org/pkg/strings/#ContainsAny) | - |
| [ContainsRune](http://golang.org/pkg/strings/#ContainsRune) | - |
| [Count](http://golang.org/pkg/strings/#Count) | `str.count` in Python; `substr_count` in PHP |
| [EqualFold](http://golang.org/pkg/strings/#EqualFold) | `stricmp` in PHP; `String#casecmp` in Ruby |
| [Fields](http://golang.org/pkg/strings/#Fields) | `str.split` in Python; `split` in Perl; `String#split` in Ruby |
| [FieldsFunc](http://golang.org/pkg/strings/#FieldsFunc) | - |
| [HasPrefix](http://golang.org/pkg/strings/#HasPrefix) | `str.startswith` in Python; `String#start_with?` in Ruby |
| [HasSuffix](http://golang.org/pkg/strings/#HasSuffix) | `str.endswith` in Python; `String#end_with?` in Ruby |
| [Index](http://golang.org/pkg/strings/#Index) | `str.index` in Python; `String#index` in Ruby; `strpos` in PHP; `index` in Perl |
| [IndexAny](http://golang.org/pkg/strings/#IndexAny) | - |
| [IndexByte](http://golang.org/pkg/strings/#IndexByte) | - |
| [IndexFunc](http://golang.org/pkg/strings/#IndexFunc) | - |
| [IndexRune](http://golang.org/pkg/strings/#IndexRune) | - |
| [Join](http://golang.org/pkg/strings/#Join) | `str.join` in Python; `Array#join` in Ruby; `implode` in PHP; `join` in Perl |
| [LastIndex](http://golang.org/pkg/strings/#LastIndex) | `str.rindex` in Python; `String#rindex`; `strrpos` in PHP; `rindex` in Perl |
| [LastIndexAny](http://golang.org/pkg/strings/#LastIndexAny) | - |
| [LastIndexFunc](http://golang.org/pkg/strings/#LastIndexFunc) | - |
| [Map](http://golang.org/pkg/strings/#Map) | `String#each_codepoint` in Ruby |
| [Repeat](http://golang.org/pkg/strings/#Repeat) | operator `*` in Python and Ruby; `str_repeat` in PHP |
| [Replace](http://golang.org/pkg/strings/#Replace) | `str.replace` in Python; `String#sub` in Ruby; `str_replace` in PHP |
| [Split](http://golang.org/pkg/strings/#Split) | `str.split` in Python; `String#split` in Ruby; `explode` in PHP; `split` in Perl |
| [SplitAfter](http://golang.org/pkg/strings/#SplitAfter) | - |
| [SplitAfterN](http://golang.org/pkg/strings/#SplitAfterN) | - |
| [SplitN](http://golang.org/pkg/strings/#SplitN) | `str.split` in Python; `String#split` in Ruby; `explode` in PHP; `split` in Perl |
| [Title](http://golang.org/pkg/strings/#Title) | `str.title` in Python |
| [ToLower](http://golang.org/pkg/strings/#ToLower) | `str.lower` in Python; `String#downcase` in Ruby; `strtolower` in PHP; `lc` in Perl |
| [ToLowerSpecial](http://golang.org/pkg/strings/#ToLowerSpecial) | - |
| [ToTitle](http://golang.org/pkg/strings/#ToTitle) | - |
| [ToTitleSpecial](http://golang.org/pkg/strings/#ToTitleSpecial) | - |
| [ToUpper](http://golang.org/pkg/strings/#ToUpper) | `str.upper` in Python; `String#upcase` in Ruby; `strtoupper` in PHP; `uc` in Perl |
| [ToUpperSpecial](http://golang.org/pkg/strings/#ToUpperSpecial) | - |
| [Trim](http://golang.org/pkg/strings/#Trim) | `str.strip` in Python; `String#strip` in Ruby; `trim` in PHP |
| [TrimFunc](http://golang.org/pkg/strings/#TrimFunc) | - |
| [TrimLeft](http://golang.org/pkg/strings/#TrimLeft) | `str.lstrip` in Python; `String#lstrip` in Ruby; `ltrim` in PHP |
| [TrimLeftFunc](http://golang.org/pkg/strings/#TrimLeftFunc) | - |
| [TrimPrefix](http://golang.org/pkg/strings/#TrimPrefix) | - |
| [TrimRight](http://golang.org/pkg/strings/#TrimRight) | `str.rstrip` in Python; `String#rstrip` in Ruby; `rtrim` in PHP |
| [TrimRightFunc](http://golang.org/pkg/strings/#TrimRightFunc) | - |
| [TrimSpace](http://golang.org/pkg/strings/#TrimSpace) | `str.strip` in Python; `String#strip` in Ruby; `trim` in PHP |
| [TrimSuffix](http://golang.org/pkg/strings/#TrimSuffix) | `String#chomp` in Ruby; `chomp` in Perl |
## License
This library is licensed under MIT license. See LICENSE for details.
+21
View File
@@ -0,0 +1,21 @@
// Copyright 2015 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package xstrings
const bufferMaxInitGrowSize = 2048
// Lazy initialize a buffer.
func allocBuffer(orig, cur string) *stringBuilder {
output := &stringBuilder{}
maxSize := len(orig) * 4
// Avoid to reserve too much memory at once.
if maxSize > bufferMaxInitGrowSize {
maxSize = bufferMaxInitGrowSize
}
output.Grow(maxSize)
output.WriteString(orig[:len(orig)-len(cur)])
return output
}
+593
View File
@@ -0,0 +1,593 @@
// Copyright 2015 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package xstrings
import (
"math/rand"
"unicode"
"unicode/utf8"
)
// ToCamelCase is to convert words separated by space, underscore and hyphen to camel case.
//
// Some samples.
//
// "some_words" => "SomeWords"
// "http_server" => "HttpServer"
// "no_https" => "NoHttps"
// "_complex__case_" => "_Complex_Case_"
// "some words" => "SomeWords"
func ToCamelCase(str string) string {
if len(str) == 0 {
return ""
}
buf := &stringBuilder{}
var r0, r1 rune
var size int
// leading connector will appear in output.
for len(str) > 0 {
r0, size = utf8.DecodeRuneInString(str)
str = str[size:]
if !isConnector(r0) {
r0 = unicode.ToUpper(r0)
break
}
buf.WriteRune(r0)
}
if len(str) == 0 {
// A special case for a string contains only 1 rune.
if size != 0 {
buf.WriteRune(r0)
}
return buf.String()
}
for len(str) > 0 {
r1 = r0
r0, size = utf8.DecodeRuneInString(str)
str = str[size:]
if isConnector(r0) && isConnector(r1) {
buf.WriteRune(r1)
continue
}
if isConnector(r1) {
r0 = unicode.ToUpper(r0)
} else {
buf.WriteRune(r1)
}
}
buf.WriteRune(r0)
return buf.String()
}
// ToSnakeCase can convert all upper case characters in a string to
// snake case format.
//
// Some samples.
//
// "FirstName" => "first_name"
// "HTTPServer" => "http_server"
// "NoHTTPS" => "no_https"
// "GO_PATH" => "go_path"
// "GO PATH" => "go_path" // space is converted to underscore.
// "GO-PATH" => "go_path" // hyphen is converted to underscore.
// "http2xx" => "http_2xx" // insert an underscore before a number and after an alphabet.
// "HTTP20xOK" => "http_20x_ok"
// "Duration2m3s" => "duration_2m3s"
// "Bld4Floor3rd" => "bld4_floor_3rd"
func ToSnakeCase(str string) string {
return camelCaseToLowerCase(str, '_')
}
// ToKebabCase can convert all upper case characters in a string to
// kebab case format.
//
// Some samples.
//
// "FirstName" => "first-name"
// "HTTPServer" => "http-server"
// "NoHTTPS" => "no-https"
// "GO_PATH" => "go-path"
// "GO PATH" => "go-path" // space is converted to '-'.
// "GO-PATH" => "go-path" // hyphen is converted to '-'.
// "http2xx" => "http-2xx" // insert an underscore before a number and after an alphabet.
// "HTTP20xOK" => "http-20x-ok"
// "Duration2m3s" => "duration-2m3s"
// "Bld4Floor3rd" => "bld4-floor-3rd"
func ToKebabCase(str string) string {
return camelCaseToLowerCase(str, '-')
}
func camelCaseToLowerCase(str string, connector rune) string {
if len(str) == 0 {
return ""
}
buf := &stringBuilder{}
wt, word, remaining := nextWord(str)
for len(remaining) > 0 {
if wt != connectorWord {
toLower(buf, wt, word, connector)
}
prev := wt
last := word
wt, word, remaining = nextWord(remaining)
switch prev {
case numberWord:
for wt == alphabetWord || wt == numberWord {
toLower(buf, wt, word, connector)
wt, word, remaining = nextWord(remaining)
}
if wt != invalidWord && wt != punctWord && wt != connectorWord {
buf.WriteRune(connector)
}
case connectorWord:
toLower(buf, prev, last, connector)
case punctWord:
// nothing.
default:
if wt != numberWord {
if wt != connectorWord && wt != punctWord {
buf.WriteRune(connector)
}
break
}
if len(remaining) == 0 {
break
}
last := word
wt, word, remaining = nextWord(remaining)
// consider number as a part of previous word.
// e.g. "Bld4Floor" => "bld4_floor"
if wt != alphabetWord {
toLower(buf, numberWord, last, connector)
if wt != connectorWord && wt != punctWord {
buf.WriteRune(connector)
}
break
}
// if there are some lower case letters following a number,
// add connector before the number.
// e.g. "HTTP2xx" => "http_2xx"
buf.WriteRune(connector)
toLower(buf, numberWord, last, connector)
for wt == alphabetWord || wt == numberWord {
toLower(buf, wt, word, connector)
wt, word, remaining = nextWord(remaining)
}
if wt != invalidWord && wt != connectorWord && wt != punctWord {
buf.WriteRune(connector)
}
}
}
toLower(buf, wt, word, connector)
return buf.String()
}
func isConnector(r rune) bool {
return r == '-' || r == '_' || unicode.IsSpace(r)
}
type wordType int
const (
invalidWord wordType = iota
numberWord
upperCaseWord
alphabetWord
connectorWord
punctWord
otherWord
)
func nextWord(str string) (wt wordType, word, remaining string) {
if len(str) == 0 {
return
}
var offset int
remaining = str
r, size := nextValidRune(remaining, utf8.RuneError)
offset += size
if r == utf8.RuneError {
wt = invalidWord
word = str[:offset]
remaining = str[offset:]
return
}
switch {
case isConnector(r):
wt = connectorWord
remaining = remaining[size:]
for len(remaining) > 0 {
r, size = nextValidRune(remaining, r)
if !isConnector(r) {
break
}
offset += size
remaining = remaining[size:]
}
case unicode.IsPunct(r):
wt = punctWord
remaining = remaining[size:]
for len(remaining) > 0 {
r, size = nextValidRune(remaining, r)
if !unicode.IsPunct(r) {
break
}
offset += size
remaining = remaining[size:]
}
case unicode.IsUpper(r):
wt = upperCaseWord
remaining = remaining[size:]
if len(remaining) == 0 {
break
}
r, size = nextValidRune(remaining, r)
switch {
case unicode.IsUpper(r):
prevSize := size
offset += size
remaining = remaining[size:]
for len(remaining) > 0 {
r, size = nextValidRune(remaining, r)
if !unicode.IsUpper(r) {
break
}
prevSize = size
offset += size
remaining = remaining[size:]
}
// it's a bit complex when dealing with a case like "HTTPStatus".
// it's expected to be splitted into "HTTP" and "Status".
// Therefore "S" should be in remaining instead of word.
if len(remaining) > 0 && isAlphabet(r) {
offset -= prevSize
remaining = str[offset:]
}
case isAlphabet(r):
offset += size
remaining = remaining[size:]
for len(remaining) > 0 {
r, size = nextValidRune(remaining, r)
if !isAlphabet(r) || unicode.IsUpper(r) {
break
}
offset += size
remaining = remaining[size:]
}
}
case isAlphabet(r):
wt = alphabetWord
remaining = remaining[size:]
for len(remaining) > 0 {
r, size = nextValidRune(remaining, r)
if !isAlphabet(r) || unicode.IsUpper(r) {
break
}
offset += size
remaining = remaining[size:]
}
case unicode.IsNumber(r):
wt = numberWord
remaining = remaining[size:]
for len(remaining) > 0 {
r, size = nextValidRune(remaining, r)
if !unicode.IsNumber(r) {
break
}
offset += size
remaining = remaining[size:]
}
default:
wt = otherWord
remaining = remaining[size:]
for len(remaining) > 0 {
r, size = nextValidRune(remaining, r)
if size == 0 || isConnector(r) || isAlphabet(r) || unicode.IsNumber(r) || unicode.IsPunct(r) {
break
}
offset += size
remaining = remaining[size:]
}
}
word = str[:offset]
return
}
func nextValidRune(str string, prev rune) (r rune, size int) {
var sz int
for len(str) > 0 {
r, sz = utf8.DecodeRuneInString(str)
size += sz
if r != utf8.RuneError {
return
}
str = str[sz:]
}
r = prev
return
}
func toLower(buf *stringBuilder, wt wordType, str string, connector rune) {
buf.Grow(buf.Len() + len(str))
if wt != upperCaseWord && wt != connectorWord {
buf.WriteString(str)
return
}
for len(str) > 0 {
r, size := utf8.DecodeRuneInString(str)
str = str[size:]
if isConnector(r) {
buf.WriteRune(connector)
} else if unicode.IsUpper(r) {
buf.WriteRune(unicode.ToLower(r))
} else {
buf.WriteRune(r)
}
}
}
// SwapCase will swap characters case from upper to lower or lower to upper.
func SwapCase(str string) string {
var r rune
var size int
buf := &stringBuilder{}
for len(str) > 0 {
r, size = utf8.DecodeRuneInString(str)
switch {
case unicode.IsUpper(r):
buf.WriteRune(unicode.ToLower(r))
case unicode.IsLower(r):
buf.WriteRune(unicode.ToUpper(r))
default:
buf.WriteRune(r)
}
str = str[size:]
}
return buf.String()
}
// FirstRuneToUpper converts first rune to upper case if necessary.
func FirstRuneToUpper(str string) string {
if str == "" {
return str
}
r, size := utf8.DecodeRuneInString(str)
if !unicode.IsLower(r) {
return str
}
buf := &stringBuilder{}
buf.WriteRune(unicode.ToUpper(r))
buf.WriteString(str[size:])
return buf.String()
}
// FirstRuneToLower converts first rune to lower case if necessary.
func FirstRuneToLower(str string) string {
if str == "" {
return str
}
r, size := utf8.DecodeRuneInString(str)
if !unicode.IsUpper(r) {
return str
}
buf := &stringBuilder{}
buf.WriteRune(unicode.ToLower(r))
buf.WriteString(str[size:])
return buf.String()
}
// Shuffle randomizes runes in a string and returns the result.
// It uses default random source in `math/rand`.
func Shuffle(str string) string {
if str == "" {
return str
}
runes := []rune(str)
index := 0
for i := len(runes) - 1; i > 0; i-- {
index = rand.Intn(i + 1)
if i != index {
runes[i], runes[index] = runes[index], runes[i]
}
}
return string(runes)
}
// ShuffleSource randomizes runes in a string with given random source.
func ShuffleSource(str string, src rand.Source) string {
if str == "" {
return str
}
runes := []rune(str)
index := 0
r := rand.New(src)
for i := len(runes) - 1; i > 0; i-- {
index = r.Intn(i + 1)
if i != index {
runes[i], runes[index] = runes[index], runes[i]
}
}
return string(runes)
}
// Successor returns the successor to string.
//
// If there is one alphanumeric rune is found in string, increase the rune by 1.
// If increment generates a "carry", the rune to the left of it is incremented.
// This process repeats until there is no carry, adding an additional rune if necessary.
//
// If there is no alphanumeric rune, the rightmost rune will be increased by 1
// regardless whether the result is a valid rune or not.
//
// Only following characters are alphanumeric.
// - a - z
// - A - Z
// - 0 - 9
//
// Samples (borrowed from ruby's String#succ document):
//
// "abcd" => "abce"
// "THX1138" => "THX1139"
// "<<koala>>" => "<<koalb>>"
// "1999zzz" => "2000aaa"
// "ZZZ9999" => "AAAA0000"
// "***" => "**+"
func Successor(str string) string {
if str == "" {
return str
}
var r rune
var i int
carry := ' '
runes := []rune(str)
l := len(runes)
lastAlphanumeric := l
for i = l - 1; i >= 0; i-- {
r = runes[i]
if ('a' <= r && r <= 'y') ||
('A' <= r && r <= 'Y') ||
('0' <= r && r <= '8') {
runes[i]++
carry = ' '
lastAlphanumeric = i
break
}
switch r {
case 'z':
runes[i] = 'a'
carry = 'a'
lastAlphanumeric = i
case 'Z':
runes[i] = 'A'
carry = 'A'
lastAlphanumeric = i
case '9':
runes[i] = '0'
carry = '0'
lastAlphanumeric = i
}
}
// Needs to add one character for carry.
if i < 0 && carry != ' ' {
buf := &stringBuilder{}
buf.Grow(l + 4) // Reserve enough space for write.
if lastAlphanumeric != 0 {
buf.WriteString(str[:lastAlphanumeric])
}
buf.WriteRune(carry)
for _, r = range runes[lastAlphanumeric:] {
buf.WriteRune(r)
}
return buf.String()
}
// No alphanumeric character. Simply increase last rune's value.
if lastAlphanumeric == l {
runes[l-1]++
}
return string(runes)
}
+120
View File
@@ -0,0 +1,120 @@
// Copyright 2015 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package xstrings
import (
"unicode"
"unicode/utf8"
)
// Len returns str's utf8 rune length.
func Len(str string) int {
return utf8.RuneCountInString(str)
}
// WordCount returns number of words in a string.
//
// Word is defined as a locale dependent string containing alphabetic characters,
// which may also contain but not start with `'` and `-` characters.
func WordCount(str string) int {
var r rune
var size, n int
inWord := false
for len(str) > 0 {
r, size = utf8.DecodeRuneInString(str)
switch {
case isAlphabet(r):
if !inWord {
inWord = true
n++
}
case inWord && (r == '\'' || r == '-'):
// Still in word.
default:
inWord = false
}
str = str[size:]
}
return n
}
const minCJKCharacter = '\u3400'
// Checks r is a letter but not CJK character.
func isAlphabet(r rune) bool {
if !unicode.IsLetter(r) {
return false
}
switch {
// Quick check for non-CJK character.
case r < minCJKCharacter:
return true
// Common CJK characters.
case r >= '\u4E00' && r <= '\u9FCC':
return false
// Rare CJK characters.
case r >= '\u3400' && r <= '\u4D85':
return false
// Rare and historic CJK characters.
case r >= '\U00020000' && r <= '\U0002B81D':
return false
}
return true
}
// Width returns string width in monotype font.
// Multi-byte characters are usually twice the width of single byte characters.
//
// Algorithm comes from `mb_strwidth` in PHP.
// http://php.net/manual/en/function.mb-strwidth.php
func Width(str string) int {
var r rune
var size, n int
for len(str) > 0 {
r, size = utf8.DecodeRuneInString(str)
n += RuneWidth(r)
str = str[size:]
}
return n
}
// RuneWidth returns character width in monotype font.
// Multi-byte characters are usually twice the width of single byte characters.
//
// Algorithm comes from `mb_strwidth` in PHP.
// http://php.net/manual/en/function.mb-strwidth.php
func RuneWidth(r rune) int {
switch {
case r == utf8.RuneError || r < '\x20':
return 0
case '\x20' <= r && r < '\u2000':
return 1
case '\u2000' <= r && r < '\uFF61':
return 2
case '\uFF61' <= r && r < '\uFFA0':
return 1
case '\uFFA0' <= r:
return 2
}
return 0
}
+8
View File
@@ -0,0 +1,8 @@
// Copyright 2015 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
// Package xstrings is to provide string algorithms which are useful but not included in `strings` package.
// See project home page for details. https://github.com/huandu/xstrings
//
// Package xstrings assumes all strings are encoded in utf8.
package xstrings
+173
View File
@@ -0,0 +1,173 @@
// Copyright 2015 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package xstrings
import (
"unicode/utf8"
)
// ExpandTabs can expand tabs ('\t') rune in str to one or more spaces dpending on
// current column and tabSize.
// The column number is reset to zero after each newline ('\n') occurring in the str.
//
// ExpandTabs uses RuneWidth to decide rune's width.
// For example, CJK characters will be treated as two characters.
//
// If tabSize <= 0, ExpandTabs panics with error.
//
// Samples:
//
// ExpandTabs("a\tbc\tdef\tghij\tk", 4) => "a bc def ghij k"
// ExpandTabs("abcdefg\thij\nk\tl", 4) => "abcdefg hij\nk l"
// ExpandTabs("z中\t文\tw", 4) => "z中 文 w"
func ExpandTabs(str string, tabSize int) string {
if tabSize <= 0 {
panic("tab size must be positive")
}
var r rune
var i, size, column, expand int
var output *stringBuilder
orig := str
for len(str) > 0 {
r, size = utf8.DecodeRuneInString(str)
if r == '\t' {
expand = tabSize - column%tabSize
if output == nil {
output = allocBuffer(orig, str)
}
for i = 0; i < expand; i++ {
output.WriteRune(' ')
}
column += expand
} else {
if r == '\n' {
column = 0
} else {
column += RuneWidth(r)
}
if output != nil {
output.WriteRune(r)
}
}
str = str[size:]
}
if output == nil {
return orig
}
return output.String()
}
// LeftJustify returns a string with pad string at right side if str's rune length is smaller than length.
// If str's rune length is larger than length, str itself will be returned.
//
// If pad is an empty string, str will be returned.
//
// Samples:
//
// LeftJustify("hello", 4, " ") => "hello"
// LeftJustify("hello", 10, " ") => "hello "
// LeftJustify("hello", 10, "123") => "hello12312"
func LeftJustify(str string, length int, pad string) string {
l := Len(str)
if l >= length || pad == "" {
return str
}
remains := length - l
padLen := Len(pad)
output := &stringBuilder{}
output.Grow(len(str) + (remains/padLen+1)*len(pad))
output.WriteString(str)
writePadString(output, pad, padLen, remains)
return output.String()
}
// RightJustify returns a string with pad string at left side if str's rune length is smaller than length.
// If str's rune length is larger than length, str itself will be returned.
//
// If pad is an empty string, str will be returned.
//
// Samples:
//
// RightJustify("hello", 4, " ") => "hello"
// RightJustify("hello", 10, " ") => " hello"
// RightJustify("hello", 10, "123") => "12312hello"
func RightJustify(str string, length int, pad string) string {
l := Len(str)
if l >= length || pad == "" {
return str
}
remains := length - l
padLen := Len(pad)
output := &stringBuilder{}
output.Grow(len(str) + (remains/padLen+1)*len(pad))
writePadString(output, pad, padLen, remains)
output.WriteString(str)
return output.String()
}
// Center returns a string with pad string at both side if str's rune length is smaller than length.
// If str's rune length is larger than length, str itself will be returned.
//
// If pad is an empty string, str will be returned.
//
// Samples:
//
// Center("hello", 4, " ") => "hello"
// Center("hello", 10, " ") => " hello "
// Center("hello", 10, "123") => "12hello123"
func Center(str string, length int, pad string) string {
l := Len(str)
if l >= length || pad == "" {
return str
}
remains := length - l
padLen := Len(pad)
output := &stringBuilder{}
output.Grow(len(str) + (remains/padLen+1)*len(pad))
writePadString(output, pad, padLen, remains/2)
output.WriteString(str)
writePadString(output, pad, padLen, (remains+1)/2)
return output.String()
}
func writePadString(output *stringBuilder, pad string, padLen, remains int) {
var r rune
var size int
repeats := remains / padLen
for i := 0; i < repeats; i++ {
output.WriteString(pad)
}
remains = remains % padLen
if remains != 0 {
for i := 0; i < remains; i++ {
r, size = utf8.DecodeRuneInString(pad)
output.WriteRune(r)
pad = pad[size:]
}
}
}
+220
View File
@@ -0,0 +1,220 @@
// Copyright 2015 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package xstrings
import (
"strings"
"unicode/utf8"
)
// Reverse a utf8 encoded string.
func Reverse(str string) string {
var size int
tail := len(str)
buf := make([]byte, tail)
s := buf
for len(str) > 0 {
_, size = utf8.DecodeRuneInString(str)
tail -= size
s = append(s[:tail], []byte(str[:size])...)
str = str[size:]
}
return string(buf)
}
// Slice a string by rune.
//
// Start must satisfy 0 <= start <= rune length.
//
// End can be positive, zero or negative.
// If end >= 0, start and end must satisfy start <= end <= rune length.
// If end < 0, it means slice to the end of string.
//
// Otherwise, Slice will panic as out of range.
func Slice(str string, start, end int) string {
var size, startPos, endPos int
origin := str
if start < 0 || end > len(str) || (end >= 0 && start > end) {
panic("out of range")
}
if end >= 0 {
end -= start
}
for start > 0 && len(str) > 0 {
_, size = utf8.DecodeRuneInString(str)
start--
startPos += size
str = str[size:]
}
if end < 0 {
return origin[startPos:]
}
endPos = startPos
for end > 0 && len(str) > 0 {
_, size = utf8.DecodeRuneInString(str)
end--
endPos += size
str = str[size:]
}
if len(str) == 0 && (start > 0 || end > 0) {
panic("out of range")
}
return origin[startPos:endPos]
}
// Partition splits a string by sep into three parts.
// The return value is a slice of strings with head, match and tail.
//
// If str contains sep, for example "hello" and "l", Partition returns
//
// "he", "l", "lo"
//
// If str doesn't contain sep, for example "hello" and "x", Partition returns
//
// "hello", "", ""
func Partition(str, sep string) (head, match, tail string) {
index := strings.Index(str, sep)
if index == -1 {
head = str
return
}
head = str[:index]
match = str[index : index+len(sep)]
tail = str[index+len(sep):]
return
}
// LastPartition splits a string by last instance of sep into three parts.
// The return value is a slice of strings with head, match and tail.
//
// If str contains sep, for example "hello" and "l", LastPartition returns
//
// "hel", "l", "o"
//
// If str doesn't contain sep, for example "hello" and "x", LastPartition returns
//
// "", "", "hello"
func LastPartition(str, sep string) (head, match, tail string) {
index := strings.LastIndex(str, sep)
if index == -1 {
tail = str
return
}
head = str[:index]
match = str[index : index+len(sep)]
tail = str[index+len(sep):]
return
}
// Insert src into dst at given rune index.
// Index is counted by runes instead of bytes.
//
// If index is out of range of dst, panic with out of range.
func Insert(dst, src string, index int) string {
return Slice(dst, 0, index) + src + Slice(dst, index, -1)
}
// Scrub scrubs invalid utf8 bytes with repl string.
// Adjacent invalid bytes are replaced only once.
func Scrub(str, repl string) string {
var buf *stringBuilder
var r rune
var size, pos int
var hasError bool
origin := str
for len(str) > 0 {
r, size = utf8.DecodeRuneInString(str)
if r == utf8.RuneError {
if !hasError {
if buf == nil {
buf = &stringBuilder{}
}
buf.WriteString(origin[:pos])
hasError = true
}
} else if hasError {
hasError = false
buf.WriteString(repl)
origin = origin[pos:]
pos = 0
}
pos += size
str = str[size:]
}
if buf != nil {
buf.WriteString(origin)
return buf.String()
}
// No invalid byte.
return origin
}
// WordSplit splits a string into words. Returns a slice of words.
// If there is no word in a string, return nil.
//
// Word is defined as a locale dependent string containing alphabetic characters,
// which may also contain but not start with `'` and `-` characters.
func WordSplit(str string) []string {
var word string
var words []string
var r rune
var size, pos int
inWord := false
for len(str) > 0 {
r, size = utf8.DecodeRuneInString(str)
switch {
case isAlphabet(r):
if !inWord {
inWord = true
word = str
pos = 0
}
case inWord && (r == '\'' || r == '-'):
// Still in word.
default:
if inWord {
inWord = false
words = append(words, word[:pos])
}
}
pos += size
str = str[size:]
}
if inWord {
words = append(words, word[:pos])
}
return words
}
+8
View File
@@ -0,0 +1,8 @@
//go:build go1.10
// +build go1.10
package xstrings
import "strings"
type stringBuilder = strings.Builder
+10
View File
@@ -0,0 +1,10 @@
//go:build !go1.10
// +build !go1.10
package xstrings
import "bytes"
type stringBuilder struct {
bytes.Buffer
}
+552
View File
@@ -0,0 +1,552 @@
// Copyright 2015 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package xstrings
import (
"unicode"
"unicode/utf8"
)
type runeRangeMap struct {
FromLo rune // Lower bound of range map.
FromHi rune // An inclusive higher bound of range map.
ToLo rune
ToHi rune
}
type runeDict struct {
Dict [unicode.MaxASCII + 1]rune
}
type runeMap map[rune]rune
// Translator can translate string with pre-compiled from and to patterns.
// If a from/to pattern pair needs to be used more than once, it's recommended
// to create a Translator and reuse it.
type Translator struct {
quickDict *runeDict // A quick dictionary to look up rune by index. Only available for latin runes.
runeMap runeMap // Rune map for translation.
ranges []*runeRangeMap // Ranges of runes.
mappedRune rune // If mappedRune >= 0, all matched runes are translated to the mappedRune.
reverted bool // If to pattern is empty, all matched characters will be deleted.
hasPattern bool
}
// NewTranslator creates new Translator through a from/to pattern pair.
func NewTranslator(from, to string) *Translator {
tr := &Translator{}
if from == "" {
return tr
}
reverted := from[0] == '^'
deletion := len(to) == 0
if reverted {
from = from[1:]
}
var fromStart, fromEnd, fromRangeStep rune
var toStart, toEnd, toRangeStep rune
var fromRangeSize, toRangeSize rune
var singleRunes []rune
// Update the to rune range.
updateRange := func() {
// No more rune to read in the to rune pattern.
if toEnd == utf8.RuneError {
return
}
if toRangeStep == 0 {
to, toStart, toEnd, toRangeStep = nextRuneRange(to, toEnd)
return
}
// Current range is not empty. Consume 1 rune from start.
if toStart != toEnd {
toStart += toRangeStep
return
}
// No more rune. Repeat the last rune.
if to == "" {
toEnd = utf8.RuneError
return
}
// Both start and end are used. Read two more runes from the to pattern.
to, toStart, toEnd, toRangeStep = nextRuneRange(to, utf8.RuneError)
}
if deletion {
toStart = utf8.RuneError
toEnd = utf8.RuneError
} else {
// If from pattern is reverted, only the last rune in the to pattern will be used.
if reverted {
var size int
for len(to) > 0 {
toStart, size = utf8.DecodeRuneInString(to)
to = to[size:]
}
toEnd = utf8.RuneError
} else {
to, toStart, toEnd, toRangeStep = nextRuneRange(to, utf8.RuneError)
}
}
fromEnd = utf8.RuneError
for len(from) > 0 {
from, fromStart, fromEnd, fromRangeStep = nextRuneRange(from, fromEnd)
// fromStart is a single character. Just map it with a rune in the to pattern.
if fromRangeStep == 0 {
singleRunes = tr.addRune(fromStart, toStart, singleRunes)
updateRange()
continue
}
for toEnd != utf8.RuneError && fromStart != fromEnd {
// If mapped rune is a single character instead of a range, simply shift first
// rune in the range.
if toRangeStep == 0 {
singleRunes = tr.addRune(fromStart, toStart, singleRunes)
updateRange()
fromStart += fromRangeStep
continue
}
fromRangeSize = (fromEnd - fromStart) * fromRangeStep
toRangeSize = (toEnd - toStart) * toRangeStep
// Not enough runes in the to pattern. Need to read more.
if fromRangeSize > toRangeSize {
fromStart, toStart = tr.addRuneRange(fromStart, fromStart+toRangeSize*fromRangeStep, toStart, toEnd, singleRunes)
fromStart += fromRangeStep
updateRange()
// Edge case: If fromRangeSize == toRangeSize + 1, the last fromStart value needs be considered
// as a single rune.
if fromStart == fromEnd {
singleRunes = tr.addRune(fromStart, toStart, singleRunes)
updateRange()
}
continue
}
fromStart, toStart = tr.addRuneRange(fromStart, fromEnd, toStart, toStart+fromRangeSize*toRangeStep, singleRunes)
updateRange()
break
}
if fromStart == fromEnd {
fromEnd = utf8.RuneError
continue
}
_, toStart = tr.addRuneRange(fromStart, fromEnd, toStart, toStart, singleRunes)
fromEnd = utf8.RuneError
}
if fromEnd != utf8.RuneError {
tr.addRune(fromEnd, toStart, singleRunes)
}
tr.reverted = reverted
tr.mappedRune = -1
tr.hasPattern = true
// Translate RuneError only if in deletion or reverted mode.
if deletion || reverted {
tr.mappedRune = toStart
}
return tr
}
func (tr *Translator) addRune(from, to rune, singleRunes []rune) []rune {
if from <= unicode.MaxASCII {
if tr.quickDict == nil {
tr.quickDict = &runeDict{}
}
tr.quickDict.Dict[from] = to
} else {
if tr.runeMap == nil {
tr.runeMap = make(runeMap)
}
tr.runeMap[from] = to
}
singleRunes = append(singleRunes, from)
return singleRunes
}
func (tr *Translator) addRuneRange(fromLo, fromHi, toLo, toHi rune, singleRunes []rune) (rune, rune) {
var r rune
var rrm *runeRangeMap
if fromLo < fromHi {
rrm = &runeRangeMap{
FromLo: fromLo,
FromHi: fromHi,
ToLo: toLo,
ToHi: toHi,
}
} else {
rrm = &runeRangeMap{
FromLo: fromHi,
FromHi: fromLo,
ToLo: toHi,
ToHi: toLo,
}
}
// If there is any single rune conflicts with this rune range, clear single rune record.
for _, r = range singleRunes {
if rrm.FromLo <= r && r <= rrm.FromHi {
if r <= unicode.MaxASCII {
tr.quickDict.Dict[r] = 0
} else {
delete(tr.runeMap, r)
}
}
}
tr.ranges = append(tr.ranges, rrm)
return fromHi, toHi
}
func nextRuneRange(str string, last rune) (remaining string, start, end rune, rangeStep rune) {
var r rune
var size int
remaining = str
escaping := false
isRange := false
for len(remaining) > 0 {
r, size = utf8.DecodeRuneInString(remaining)
remaining = remaining[size:]
// Parse special characters.
if !escaping {
if r == '\\' {
escaping = true
continue
}
if r == '-' {
// Ignore slash at beginning of string.
if last == utf8.RuneError {
continue
}
start = last
isRange = true
continue
}
}
escaping = false
if last != utf8.RuneError {
// This is a range which start and end are the same.
// Considier it as a normal character.
if isRange && last == r {
isRange = false
continue
}
start = last
end = r
if isRange {
if start < end {
rangeStep = 1
} else {
rangeStep = -1
}
}
return
}
last = r
}
start = last
end = utf8.RuneError
return
}
// Translate str with a from/to pattern pair.
//
// See comment in Translate function for usage and samples.
func (tr *Translator) Translate(str string) string {
if !tr.hasPattern || str == "" {
return str
}
var r rune
var size int
var needTr bool
orig := str
var output *stringBuilder
for len(str) > 0 {
r, size = utf8.DecodeRuneInString(str)
r, needTr = tr.TranslateRune(r)
if needTr && output == nil {
output = allocBuffer(orig, str)
}
if r != utf8.RuneError && output != nil {
output.WriteRune(r)
}
str = str[size:]
}
// No character is translated.
if output == nil {
return orig
}
return output.String()
}
// TranslateRune return translated rune and true if r matches the from pattern.
// If r doesn't match the pattern, original r is returned and translated is false.
func (tr *Translator) TranslateRune(r rune) (result rune, translated bool) {
switch {
case tr.quickDict != nil:
if r <= unicode.MaxASCII {
result = tr.quickDict.Dict[r]
if result != 0 {
translated = true
if tr.mappedRune >= 0 {
result = tr.mappedRune
}
break
}
}
fallthrough
case tr.runeMap != nil:
var ok bool
if result, ok = tr.runeMap[r]; ok {
translated = true
if tr.mappedRune >= 0 {
result = tr.mappedRune
}
break
}
fallthrough
default:
var rrm *runeRangeMap
ranges := tr.ranges
for i := len(ranges) - 1; i >= 0; i-- {
rrm = ranges[i]
if rrm.FromLo <= r && r <= rrm.FromHi {
translated = true
if tr.mappedRune >= 0 {
result = tr.mappedRune
break
}
if rrm.ToLo < rrm.ToHi {
result = rrm.ToLo + r - rrm.FromLo
} else if rrm.ToLo > rrm.ToHi {
// ToHi can be smaller than ToLo if range is from higher to lower.
result = rrm.ToLo - r + rrm.FromLo
} else {
result = rrm.ToLo
}
break
}
}
}
if tr.reverted {
if !translated {
result = tr.mappedRune
}
translated = !translated
}
if !translated {
result = r
}
return
}
// HasPattern returns true if Translator has one pattern at least.
func (tr *Translator) HasPattern() bool {
return tr.hasPattern
}
// Translate str with the characters defined in from replaced by characters defined in to.
//
// From and to are patterns representing a set of characters. Pattern is defined as following.
//
// Special characters:
//
// 1. '-' means a range of runes, e.g.
// "a-z" means all characters from 'a' to 'z' inclusive;
// "z-a" means all characters from 'z' to 'a' inclusive.
// 2. '^' as first character means a set of all runes excepted listed, e.g.
// "^a-z" means all characters except 'a' to 'z' inclusive.
// 3. '\' escapes special characters.
//
// Normal character represents itself, e.g. "abc" is a set including 'a', 'b' and 'c'.
//
// Translate will try to find a 1:1 mapping from from to to.
// If to is smaller than from, last rune in to will be used to map "out of range" characters in from.
//
// Note that '^' only works in the from pattern. It will be considered as a normal character in the to pattern.
//
// If the to pattern is an empty string, Translate works exactly the same as Delete.
//
// Samples:
//
// Translate("hello", "aeiou", "12345") => "h2ll4"
// Translate("hello", "a-z", "A-Z") => "HELLO"
// Translate("hello", "z-a", "a-z") => "svool"
// Translate("hello", "aeiou", "*") => "h*ll*"
// Translate("hello", "^l", "*") => "**ll*"
// Translate("hello ^ world", `\^lo`, "*") => "he*** * w*r*d"
func Translate(str, from, to string) string {
tr := NewTranslator(from, to)
return tr.Translate(str)
}
// Delete runes in str matching the pattern.
// Pattern is defined in Translate function.
//
// Samples:
//
// Delete("hello", "aeiou") => "hll"
// Delete("hello", "a-k") => "llo"
// Delete("hello", "^a-k") => "he"
func Delete(str, pattern string) string {
tr := NewTranslator(pattern, "")
return tr.Translate(str)
}
// Count how many runes in str match the pattern.
// Pattern is defined in Translate function.
//
// Samples:
//
// Count("hello", "aeiou") => 3
// Count("hello", "a-k") => 3
// Count("hello", "^a-k") => 2
func Count(str, pattern string) int {
if pattern == "" || str == "" {
return 0
}
var r rune
var size int
var matched bool
tr := NewTranslator(pattern, "")
cnt := 0
for len(str) > 0 {
r, size = utf8.DecodeRuneInString(str)
str = str[size:]
if _, matched = tr.TranslateRune(r); matched {
cnt++
}
}
return cnt
}
// Squeeze deletes adjacent repeated runes in str.
// If pattern is not empty, only runes matching the pattern will be squeezed.
//
// Samples:
//
// Squeeze("hello", "") => "helo"
// Squeeze("hello", "m-z") => "hello"
// Squeeze("hello world", " ") => "hello world"
func Squeeze(str, pattern string) string {
var last, r rune
var size int
var skipSqueeze, matched bool
var tr *Translator
var output *stringBuilder
orig := str
last = -1
if len(pattern) > 0 {
tr = NewTranslator(pattern, "")
}
for len(str) > 0 {
r, size = utf8.DecodeRuneInString(str)
// Need to squeeze the str.
if last == r && !skipSqueeze {
if tr != nil {
if _, matched = tr.TranslateRune(r); !matched {
skipSqueeze = true
}
}
if output == nil {
output = allocBuffer(orig, str)
}
if skipSqueeze {
output.WriteRune(r)
}
} else {
if output != nil {
output.WriteRune(r)
}
last = r
skipSqueeze = false
}
str = str[size:]
}
if output == nil {
return orig
}
return output.String()
}
+10 -5
View File
@@ -33,7 +33,7 @@ GOHOSTOS ?= $(shell $(GO) env GOHOSTOS)
GOHOSTARCH ?= $(shell $(GO) env GOHOSTARCH)
GO_VERSION ?= $(shell $(GO) version)
GO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION))Error Parsing File
GO_VERSION_NUMBER ?= $(word 3, $(GO_VERSION))
PRE_GO_111 ?= $(shell echo $(GO_VERSION_NUMBER) | grep -E 'go1\.(10|[0-9])\.')
PROMU := $(FIRST_GOPATH)/bin/promu
@@ -61,7 +61,8 @@ PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_
SKIP_GOLANGCI_LINT :=
GOLANGCI_LINT :=
GOLANGCI_LINT_OPTS ?=
GOLANGCI_LINT_VERSION ?= v2.0.2
GOLANGCI_LINT_VERSION ?= v2.1.5
GOLANGCI_FMT_OPTS ?=
# golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64.
# windows isn't included here because of the path separator being different.
ifeq ($(GOHOSTOS),$(filter $(GOHOSTOS),linux darwin))
@@ -156,9 +157,13 @@ $(GOTEST_DIR):
@mkdir -p $@
.PHONY: common-format
common-format:
common-format: $(GOLANGCI_LINT)
@echo ">> formatting code"
$(GO) fmt $(pkgs)
ifdef GOLANGCI_LINT
@echo ">> formatting code with golangci-lint"
$(GOLANGCI_LINT) fmt $(GOLANGCI_FMT_OPTS)
endif
.PHONY: common-vet
common-vet:
@@ -248,8 +253,8 @@ $(PROMU):
cp $(PROMU_TMP)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM)/promu $(FIRST_GOPATH)/bin/promu
rm -r $(PROMU_TMP)
.PHONY: proto
proto:
.PHONY: common-proto
common-proto:
@echo ">> generating code from proto files"
@./scripts/genproto.sh
+4 -1
View File
@@ -123,13 +123,16 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
finish := float64(0)
pct := float64(0)
recovering := strings.Contains(lines[syncLineIdx], "recovery")
reshaping := strings.Contains(lines[syncLineIdx], "reshape")
resyncing := strings.Contains(lines[syncLineIdx], "resync")
checking := strings.Contains(lines[syncLineIdx], "check")
// Append recovery and resyncing state info.
if recovering || resyncing || checking {
if recovering || resyncing || checking || reshaping {
if recovering {
state = "recovering"
} else if reshaping {
state = "reshaping"
} else if checking {
state = "checking"
} else {
+33
View File
@@ -66,6 +66,10 @@ type Meminfo struct {
// Memory which has been evicted from RAM, and is temporarily
// on the disk
SwapFree *uint64
// Memory consumed by the zswap backend (compressed size)
Zswap *uint64
// Amount of anonymous memory stored in zswap (original size)
Zswapped *uint64
// Memory which is waiting to get written back to the disk
Dirty *uint64
// Memory which is actively being written back to the disk
@@ -85,6 +89,8 @@ type Meminfo struct {
// amount of memory dedicated to the lowest level of page
// tables.
PageTables *uint64
// secondary page tables.
SecPageTables *uint64
// NFS pages sent to the server, but not yet committed to
// stable storage
NFSUnstable *uint64
@@ -129,15 +135,18 @@ type Meminfo struct {
Percpu *uint64
HardwareCorrupted *uint64
AnonHugePages *uint64
FileHugePages *uint64
ShmemHugePages *uint64
ShmemPmdMapped *uint64
CmaTotal *uint64
CmaFree *uint64
Unaccepted *uint64
HugePagesTotal *uint64
HugePagesFree *uint64
HugePagesRsvd *uint64
HugePagesSurp *uint64
Hugepagesize *uint64
Hugetlb *uint64
DirectMap4k *uint64
DirectMap2M *uint64
DirectMap1G *uint64
@@ -161,6 +170,8 @@ type Meminfo struct {
MlockedBytes *uint64
SwapTotalBytes *uint64
SwapFreeBytes *uint64
ZswapBytes *uint64
ZswappedBytes *uint64
DirtyBytes *uint64
WritebackBytes *uint64
AnonPagesBytes *uint64
@@ -171,6 +182,7 @@ type Meminfo struct {
SUnreclaimBytes *uint64
KernelStackBytes *uint64
PageTablesBytes *uint64
SecPageTablesBytes *uint64
NFSUnstableBytes *uint64
BounceBytes *uint64
WritebackTmpBytes *uint64
@@ -182,11 +194,14 @@ type Meminfo struct {
PercpuBytes *uint64
HardwareCorruptedBytes *uint64
AnonHugePagesBytes *uint64
FileHugePagesBytes *uint64
ShmemHugePagesBytes *uint64
ShmemPmdMappedBytes *uint64
CmaTotalBytes *uint64
CmaFreeBytes *uint64
UnacceptedBytes *uint64
HugepagesizeBytes *uint64
HugetlbBytes *uint64
DirectMap4kBytes *uint64
DirectMap2MBytes *uint64
DirectMap1GBytes *uint64
@@ -287,6 +302,12 @@ func parseMemInfo(r io.Reader) (*Meminfo, error) {
case "SwapFree:":
m.SwapFree = &val
m.SwapFreeBytes = &valBytes
case "Zswap:":
m.Zswap = &val
m.ZswapBytes = &valBytes
case "Zswapped:":
m.Zswapped = &val
m.ZswapBytes = &valBytes
case "Dirty:":
m.Dirty = &val
m.DirtyBytes = &valBytes
@@ -317,6 +338,9 @@ func parseMemInfo(r io.Reader) (*Meminfo, error) {
case "PageTables:":
m.PageTables = &val
m.PageTablesBytes = &valBytes
case "SecPageTables:":
m.SecPageTables = &val
m.SecPageTablesBytes = &valBytes
case "NFS_Unstable:":
m.NFSUnstable = &val
m.NFSUnstableBytes = &valBytes
@@ -350,6 +374,9 @@ func parseMemInfo(r io.Reader) (*Meminfo, error) {
case "AnonHugePages:":
m.AnonHugePages = &val
m.AnonHugePagesBytes = &valBytes
case "FileHugePages:":
m.FileHugePages = &val
m.FileHugePagesBytes = &valBytes
case "ShmemHugePages:":
m.ShmemHugePages = &val
m.ShmemHugePagesBytes = &valBytes
@@ -362,6 +389,9 @@ func parseMemInfo(r io.Reader) (*Meminfo, error) {
case "CmaFree:":
m.CmaFree = &val
m.CmaFreeBytes = &valBytes
case "Unaccepted:":
m.Unaccepted = &val
m.UnacceptedBytes = &valBytes
case "HugePages_Total:":
m.HugePagesTotal = &val
case "HugePages_Free:":
@@ -373,6 +403,9 @@ func parseMemInfo(r io.Reader) (*Meminfo, error) {
case "Hugepagesize:":
m.Hugepagesize = &val
m.HugepagesizeBytes = &valBytes
case "Hugetlb:":
m.Hugetlb = &val
m.HugetlbBytes = &valBytes
case "DirectMap4k:":
m.DirectMap4k = &val
m.DirectMap4kBytes = &valBytes
+9 -3
View File
@@ -101,6 +101,12 @@ type ProcStat struct {
RSS int
// Soft limit in bytes on the rss of the process.
RSSLimit uint64
// The address above which program text can run.
StartCode uint64
// The address below which program text can run.
EndCode uint64
// The address of the start (i.e., bottom) of the stack.
StartStack uint64
// CPU number last executed on.
Processor uint
// Real-time scheduling priority, a number in the range 1 to 99 for processes
@@ -177,9 +183,9 @@ func (p Proc) Stat() (ProcStat, error) {
&s.VSize,
&s.RSS,
&s.RSSLimit,
&ignoreUint64,
&ignoreUint64,
&ignoreUint64,
&s.StartCode,
&s.EndCode,
&s.StartStack,
&ignoreUint64,
&ignoreUint64,
&ignoreUint64,
+116
View File
@@ -0,0 +1,116 @@
// Copyright 2025 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package procfs
import (
"os"
"strconv"
"strings"
"github.com/prometheus/procfs/internal/util"
)
// - https://man7.org/linux/man-pages/man5/proc_pid_statm.5.html
// ProcStatm Provides memory usage information for a process, measured in memory pages.
// Read from /proc/[pid]/statm.
type ProcStatm struct {
// The process ID.
PID int
// total program size (same as VmSize in status)
Size uint64
// resident set size (same as VmRSS in status)
Resident uint64
// number of resident shared pages (i.e., backed by a file)
Shared uint64
// text (code)
Text uint64
// library (unused since Linux 2.6; always 0)
Lib uint64
// data + stack
Data uint64
// dirty pages (unused since Linux 2.6; always 0)
Dt uint64
}
// NewStatm returns the current status information of the process.
// Deprecated: Use p.Statm() instead.
func (p Proc) NewStatm() (ProcStatm, error) {
return p.Statm()
}
// Statm returns the current memory usage information of the process.
func (p Proc) Statm() (ProcStatm, error) {
data, err := util.ReadFileNoStat(p.path("statm"))
if err != nil {
return ProcStatm{}, err
}
statmSlice, err := parseStatm(data)
if err != nil {
return ProcStatm{}, err
}
procStatm := ProcStatm{
PID: p.PID,
Size: statmSlice[0],
Resident: statmSlice[1],
Shared: statmSlice[2],
Text: statmSlice[3],
Lib: statmSlice[4],
Data: statmSlice[5],
Dt: statmSlice[6],
}
return procStatm, nil
}
// parseStatm return /proc/[pid]/statm data to uint64 slice.
func parseStatm(data []byte) ([]uint64, error) {
var statmSlice []uint64
statmItems := strings.Fields(string(data))
for i := 0; i < len(statmItems); i++ {
statmItem, err := strconv.ParseUint(statmItems[i], 10, 64)
if err != nil {
return nil, err
}
statmSlice = append(statmSlice, statmItem)
}
return statmSlice, nil
}
// SizeBytes returns the process of total program size in bytes.
func (s ProcStatm) SizeBytes() uint64 {
return s.Size * uint64(os.Getpagesize())
}
// ResidentBytes returns the process of resident set size in bytes.
func (s ProcStatm) ResidentBytes() uint64 {
return s.Resident * uint64(os.Getpagesize())
}
// SHRBytes returns the process of share memory size in bytes.
func (s ProcStatm) SHRBytes() uint64 {
return s.Shared * uint64(os.Getpagesize())
}
// TextBytes returns the process of text (code) size in bytes.
func (s ProcStatm) TextBytes() uint64 {
return s.Text * uint64(os.Getpagesize())
}
// DataBytes returns the process of data + stack size in bytes.
func (s ProcStatm) DataBytes() uint64 {
return s.Data * uint64(os.Getpagesize())
}
+1
View File
@@ -13,6 +13,7 @@ go:
- "1.12"
- "1.13"
- "1.14"
- "1.15"
script:
- ./validate.sh

Some files were not shown because too many files have changed in this diff Show More