mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
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:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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!
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/node_modules
|
||||
@@ -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
|
||||
```
|
||||
@@ -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();
|
||||
Generated
+456
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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=
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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]
|
||||
}
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"acmecorp": {
|
||||
"alice": ["admin"],
|
||||
"bob": ["reader"],
|
||||
"caesar": ["reader", "resolver"]
|
||||
},
|
||||
"hooli": {
|
||||
"dylan": ["admin"]
|
||||
}
|
||||
}
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
// Package md2man aims in converting markdown into roff (man pages).
|
||||
package md2man
|
||||
|
||||
import (
|
||||
|
||||
+7
-8
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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/
|
||||
Generated
Vendored
+19
-19
@@ -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
@@ -0,0 +1,472 @@
|
||||
# SQL builder for Go
|
||||
|
||||
[](https://github.com/huandu/go-sqlbuilder/actions)
|
||||
[](https://pkg.go.dev/github.com/huandu/go-sqlbuilder)
|
||||
[](https://goreportcard.com/report/github.com/huandu/go-sqlbuilder)
|
||||
[](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,117 @@
|
||||
# xstrings
|
||||
|
||||
[](https://github.com/huandu/xstrings/actions)
|
||||
[](https://pkg.go.dev/github.com/huandu/xstrings)
|
||||
[](https://goreportcard.com/report/github.com/huandu/xstrings)
|
||||
[](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,8 @@
|
||||
//go:build go1.10
|
||||
// +build go1.10
|
||||
|
||||
package xstrings
|
||||
|
||||
import "strings"
|
||||
|
||||
type stringBuilder = strings.Builder
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
//go:build !go1.10
|
||||
// +build !go1.10
|
||||
|
||||
package xstrings
|
||||
|
||||
import "bytes"
|
||||
|
||||
type stringBuilder struct {
|
||||
bytes.Buffer
|
||||
}
|
||||
+552
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
Reference in New Issue
Block a user