From 2f42d7fee1442220344700dc786b7c66cc07b1e9 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 16 May 2025 10:55:03 +0200 Subject: [PATCH 1/8] Begin pgcql --- Makefile | 5 +- go.mod | 60 ++++++++++++ pgcql/pg_definition.go | 28 ++++++ pgcql/pg_field_string.go | 192 +++++++++++++++++++++++++++++++++++++++ pgcql/pg_query.go | 86 ++++++++++++++++++ pgcql/pgcql.go | 32 +++++++ pgcql/pgcql_test.go | 100 ++++++++++++++++++++ pgcql/pgx_test.go | 40 ++++++++ 8 files changed, 542 insertions(+), 1 deletion(-) create mode 100644 pgcql/pg_definition.go create mode 100644 pgcql/pg_field_string.go create mode 100644 pgcql/pg_query.go create mode 100644 pgcql/pgcql.go create mode 100644 pgcql/pgcql_test.go create mode 100644 pgcql/pgx_test.go diff --git a/Makefile b/Makefile index b75642e..216d4b6 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all test coverage clean +.PHONY: all test coverage lint clean all: go build -o . ./... @@ -10,6 +10,9 @@ coverage: go test ./... -coverprofile=cover.out go tool cover -html=cover.out +lint: + go run github.com/golangci/golangci-lint/cmd/golangci-lint@latest run + clean: rm -f cover.out rm -f cql-cli diff --git a/go.mod b/go.mod index cc2a93a..93bc71e 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,63 @@ module github.com/indexdata/cql-go go 1.23.4 + +require ( + github.com/jackc/pgx/v5 v5.7.4 + github.com/stretchr/testify v1.10.0 + github.com/testcontainers/testcontainers-go v0.37.0 + github.com/testcontainers/testcontainers-go/modules/postgres v0.37.0 +) + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.0.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.8.2 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.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/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect + github.com/shirou/gopsutil/v4 v4.25.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // 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.49.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/pgcql/pg_definition.go b/pgcql/pg_definition.go new file mode 100644 index 0000000..3c276fb --- /dev/null +++ b/pgcql/pg_definition.go @@ -0,0 +1,28 @@ +package pgcql + +import "github.com/indexdata/cql-go/cql" + +type PgDefinition struct { + fields map[string]Field +} + +func (pg *PgDefinition) AddField(name string, field Field) Definition { + if pg.fields == nil { + pg.fields = make(map[string]Field) + } + pg.fields[name] = field + return pg +} + +func (pg *PgDefinition) GetFieldType(name string) Field { + if field, ok := pg.fields[name]; ok { + return field + } + return nil +} + +func (pg *PgDefinition) Parse(q cql.Query, queryArgumentIndex int) (Query, error) { + query := &PgQuery{} + err := query.parse(q, queryArgumentIndex, pg) + return query, err +} diff --git a/pgcql/pg_field_string.go b/pgcql/pg_field_string.go new file mode 100644 index 0000000..b55584d --- /dev/null +++ b/pgcql/pg_field_string.go @@ -0,0 +1,192 @@ +package pgcql + +import ( + "fmt" + + "github.com/indexdata/cql-go/cql" +) + +type FieldString struct { + column string + language string + enableLike bool + enableExact bool +} + +func (f *FieldString) GetColumn() string { + return f.column +} + +func (f *FieldString) WithColumn(column string) Field { + f.column = column + return f +} + +func (f *FieldString) WithFullText(language string) Field { + if language == "" { + f.language = "simple" + } else { + f.language = language + } + return f +} + +func (f *FieldString) WithLikeOps() Field { + f.enableExact = true + f.enableLike = true + return f +} + +func (f *FieldString) WithExact() Field { + f.enableExact = true + return f +} + +func maskedExact(cqlTerm string) (string, error) { + var pgTerm []rune + backslash := false + + for _, c := range cqlTerm { + if backslash { + switch c { + case '*', '"', '?', '^', '\\': + pgTerm = append(pgTerm, c) + default: + return "", fmt.Errorf("a masking backslash in a CQL string must be followed by *, ?, ^, \" or \\") + } + backslash = false + } else { + switch c { + case '*': + return "", fmt.Errorf("masking op * unsupported") + case '?': + return "", fmt.Errorf("masking op ? unsupported") + case '^': + return "", fmt.Errorf("anchor op ^ unsupported") + case '\\': + // Do nothing, just set backslash to true + case '\'': + pgTerm = append(pgTerm, '\'', '\'') + default: + pgTerm = append(pgTerm, c) + } + backslash = c == '\\' + } + } + if backslash { + return "", fmt.Errorf("a CQL string must not end with a masking backslash") + } + return string(pgTerm), nil +} + +func maskedLike(cqlTerm string) (string, bool, error) { + var pgTerm []rune + ops := false + backslash := false + + for _, c := range cqlTerm { + if backslash { + switch c { + case '*', '?', '^', '"': + pgTerm = append(pgTerm, c) + case '\\': + pgTerm = append(pgTerm, '\\', '\\') + default: + return "", false, fmt.Errorf("a masking backslash in a CQL string must be followed by *, ?, ^, \" or \\") + } + backslash = false + } else { + switch c { + case '*': + pgTerm = append(pgTerm, '%') + ops = true + case '?': + pgTerm = append(pgTerm, '_') + ops = true + case '^': + return "", false, fmt.Errorf("anchor op ^ unsupported") + case '\\': + // Do nothing, just set backslash to true + case '%', '_': + pgTerm = append(pgTerm, '\\', c) + case '\'': + pgTerm = append(pgTerm, '\'', '\'') + default: + pgTerm = append(pgTerm, c) + } + backslash = c == '\\' + } + } + if backslash { + return "", false, fmt.Errorf("a CQL string must not end with a masking backslash") + } + return string(pgTerm), ops, nil +} + +func (f *FieldString) handleEmptyTerm(sc cql.SearchClause) string { + if sc.Term == "" && sc.Relation == cql.EQ { + return f.column + " IS NOT NULL" + } + return "" +} + +func unorderedRelation(sc cql.SearchClause) (string, error) { + switch sc.Relation { + case cql.EXACT, cql.EQ: + return "=", nil + case cql.NE: + return "<>", nil + default: + return "", fmt.Errorf("unsupported relation %s", sc.Relation) + } +} + +func (f *FieldString) Generate(sc cql.SearchClause, queryArgumentIndex int) (string, []any, error) { + sql := f.handleEmptyTerm(sc) + if sql != "" { + return sql, nil, nil + } + fulltext := f.language != "" + var pgfunc string + if fulltext { + if sc.Relation == cql.ADJ || sc.Relation == cql.EQ { + pgfunc = "phraseto_tsquery" + } else if sc.Relation == cql.ALL { + pgfunc = "plainto_tsquery" + } + } + if pgfunc != "" { + pgTerm, err := maskedExact(sc.Term) + if err != nil { + return "", nil, err + } + // TODO.. add to arguments + sql := "to_tsvector('" + f.language + "', " + f.column + ") @@ " + pgfunc + "('" + f.language + "', '" + pgTerm + "')" + return sql, nil, nil + } + if !f.enableExact { + return "", nil, &PgError{message: "exact search not supported"} + } + if f.enableLike && (sc.Relation == cql.EQ || sc.Relation == cql.EXACT || sc.Relation == cql.NE) { + pgTerm, ops, err := maskedLike(sc.Term) + if err != nil { + return "", nil, err + } + if ops { + pgOp := "LIKE" + if sc.Relation == cql.NE { + pgOp = "NOT LIKE" + } + return f.column + " " + pgOp + fmt.Sprintf(" $%d", queryArgumentIndex), []interface{}{pgTerm}, nil + } + } + pgTerm, err := maskedExact(sc.Term) + if err != nil { + return "", nil, err + } + pgOp, err := unorderedRelation(sc) + if err != nil { + return "", nil, err + } + return f.column + " " + pgOp + fmt.Sprintf(" $%d", queryArgumentIndex), []interface{}{pgTerm}, nil +} diff --git a/pgcql/pg_query.go b/pgcql/pg_query.go new file mode 100644 index 0000000..5604813 --- /dev/null +++ b/pgcql/pg_query.go @@ -0,0 +1,86 @@ +package pgcql + +import ( + "fmt" + + "github.com/indexdata/cql-go/cql" +) + +type PgQuery struct { + def *PgDefinition + queryArgumentIndex int + arguments []any + sql string +} + +func (p *PgQuery) parse(q cql.Query, queryArgumentIndex int, def *PgDefinition) error { + p.def = def + p.arguments = make([]interface{}, 0) + p.queryArgumentIndex = queryArgumentIndex + if q.SortSpec != nil { + return &PgError{message: "sorting not supported"} + } + return p.parseClause(q.Clause, 0) +} + +func (p *PgQuery) parseClause(sc cql.Clause, level int) error { + if sc.SearchClause != nil { + index := sc.SearchClause.Index + fieldType := p.def.GetFieldType(index) + if fieldType == nil { + return &PgError{message: fmt.Sprintf("unknown field %s", index)} + } + sql, args, err := fieldType.Generate(*sc.SearchClause, p.queryArgumentIndex) + if err != nil { + return err + } + p.sql += sql + if args != nil { + p.queryArgumentIndex += len(args) + p.arguments = append(p.arguments, args...) + } + return nil + } else if sc.BoolClause != nil { + if level > 0 { + p.sql += "(" + } + err := p.parseClause(sc.BoolClause.Left, level+1) + if err != nil { + return err + } + if sc.BoolClause.Operator == cql.AND { + p.sql += " AND " + } else if sc.BoolClause.Operator == cql.OR { + p.sql += " OR " + } else if sc.BoolClause.Operator == cql.NOT { + p.sql += " AND NOT " + } else { + return &PgError{message: fmt.Sprintf("unsupported operator %s", sc.BoolClause.Operator)} + } + err = p.parseClause(sc.BoolClause.Right, level+1) + if err != nil { + return err + } + if level > 0 { + p.sql += ")" + } + return nil + } + return &PgError{message: "unsupported clause type"} +} + +func (p *PgQuery) GetWhereClause() string { + return p.sql +} + +func (p *PgQuery) GetQueryArguments() []any { + return p.arguments +} + +func (p *PgQuery) GetOrderByClause() string { + return "" +} + +func (p *PgQuery) GetOrderByFields() string { + return "" +} diff --git a/pgcql/pgcql.go b/pgcql/pgcql.go new file mode 100644 index 0000000..97255ea --- /dev/null +++ b/pgcql/pgcql.go @@ -0,0 +1,32 @@ +package pgcql + +import ( + "github.com/indexdata/cql-go/cql" +) + +type PgError struct { + message string +} + +func (e *PgError) Error() string { + return e.message +} + +type Field interface { + GetColumn() string + WithColumn(column string) Field + Generate(sc cql.SearchClause, queryArgumentIndex int) (string, []any, error) +} + +type Definition interface { + AddField(name string, field Field) Definition + GetFieldType(name string) Field + Parse(q cql.Query, queryArgumentIndex int) (Query, error) +} + +type Query interface { + GetWhereClause() string + GetQueryArguments() []any + GetOrderByClause() string + GetOrderByFields() string +} diff --git a/pgcql/pgcql_test.go b/pgcql/pgcql_test.go new file mode 100644 index 0000000..c42dd7b --- /dev/null +++ b/pgcql/pgcql_test.go @@ -0,0 +1,100 @@ +package pgcql + +import ( + "reflect" + "strings" + "testing" + + "github.com/indexdata/cql-go/cql" + "github.com/stretchr/testify/assert" +) + +func TestBadSearchClause(t *testing.T) { + def := &PgDefinition{} + + assert.Nil(t, def.GetFieldType("foo")) + + q := cql.Query{} + _, err := def.Parse(q, 1) + assert.Error(t, err, "Expected error for empty query") + assert.Equal(t, "unsupported clause type", err.Error()) +} + +func TestParsing(t *testing.T) { + def := &PgDefinition{} + title := &FieldString{} + title.WithExact() + title.WithColumn("Title") + + assert.Equal(t, title.GetColumn(), "Title", "GetColumn() should return the column name") + + author := &FieldString{} + author.WithColumn("Author") + author.WithLikeOps() + + serverChoice := &FieldString{} + serverChoice.WithExact() + serverChoice.WithColumn("T") + + def.AddField("title", title).AddField("author", author).AddField("cql.serverChoice", serverChoice) + + for _, testcase := range []struct { + query string + expected string + expectedArgs []interface{} + }{ + {"abc", "T = $1", []interface{}{"abc"}}, + {"\"\"", "T IS NOT NULL", []interface{}{}}, + {"au=2", "error: unknown field au", nil}, + {"title>2", "error: unsupported relation >", nil}, + {"title=2", "Title = $1", []interface{}{"2"}}, + {"title<>2", "Title <> $1", []interface{}{"2"}}, + {"a or b and c", "(T = $1 OR T = $2) AND T = $3", []interface{}{"a", "b", "c"}}, + {"title = abc", "Title = $1", []interface{}{"abc"}}, + {"author = \"test\"", "Author = $1", []interface{}{"test"}}, + {"title = a AND author = b c", "Title = $1 AND Author = $2", []interface{}{"a", "b c"}}, + {"title = 'a' OR author = 'b'", "Title = $1 OR Author = $2", []interface{}{"''a''", "''b''"}}, + {"title = a NOT author = b", "Title = $1 AND NOT Author = $2", []interface{}{"a", "b"}}, + {"a prox b", "error: unsupported operator prox", []interface{}{}}, + {"a sortby title", "error: sorting not supported", []interface{}{}}, + {"au=2 or a", "error: unknown field au", nil}, + {"a or au=2", "error: unknown field au", nil}, + } { + var parser cql.Parser + q, err := parser.Parse(testcase.query) + if err != nil { + t.Errorf("%s: CQL parse error: %v", testcase.query, err) + continue + } + pgQuery, err := def.Parse(q, 1) + + expectedError := strings.HasPrefix(testcase.expected, "error: ") + + if err != nil { + if expectedError { + if strings.TrimPrefix(testcase.expected, "error: ") != err.Error() { + t.Errorf("%s: Expected error %s, got %s", testcase.query, strings.TrimPrefix(testcase.expected, "error: "), err) + } + } else { + t.Errorf("%s: Failed to parse: %v", testcase.query, err) + } + continue + } + if expectedError { + t.Errorf("%s: Expected error, but got OK", testcase.query) + continue + } + if pgQuery.GetWhereClause() != testcase.expected { + t.Errorf("%s: Expected %s, got %s", testcase.query, testcase.expected, pgQuery.GetWhereClause()) + } + if !reflect.DeepEqual(pgQuery.GetQueryArguments(), testcase.expectedArgs) { + t.Errorf("%s: Expected arguments %v, got %v", testcase.query, testcase.expectedArgs, pgQuery.GetQueryArguments()) + } + if pgQuery.GetOrderByClause() != "" { + t.Errorf("%s: Expected empty order by clause, got %s", testcase.query, pgQuery.GetOrderByClause()) + } + if pgQuery.GetOrderByFields() != "" { + t.Errorf("%s: Expected empty order by fields, got %s", testcase.query, pgQuery.GetOrderByFields()) + } + } +} diff --git a/pgcql/pgx_test.go b/pgcql/pgx_test.go new file mode 100644 index 0000000..90694e9 --- /dev/null +++ b/pgcql/pgx_test.go @@ -0,0 +1,40 @@ +package pgcql + +import ( + "context" + "testing" + "time" + + "github.com/jackc/pgx/v5" + "github.com/stretchr/testify/assert" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" + "github.com/testcontainers/testcontainers-go/wait" +) + +func TestPgx(t *testing.T) { + ctx := context.Background() + pgContainer, err := postgres.Run(ctx, "postgres", + postgres.WithDatabase("crosslink"), + postgres.WithUsername("crosslink"), + postgres.WithPassword("crosslink"), + testcontainers.WithWaitStrategy( + wait.ForLog("database system is ready to accept connections"). + WithOccurrence(2).WithStartupTimeout(5*time.Second)), + ) + assert.NoError(t, err, "failed to start db container") + var connStr string + connStr, err = pgContainer.ConnectionString(ctx, "sslmode=disable") + assert.NoError(t, err, "failed to get db connection string") + assert.NotEmpty(t, connStr, "connection string should not be empty") + + conn, err := pgx.Connect(ctx, connStr) + assert.NoError(t, err, "failed to connect to db") + defer func() { + err := conn.Close(ctx) + assert.NoError(t, err, "failed to close db connection") + }() + + err = pgContainer.Terminate(ctx) + assert.NoError(t, err, "failed to stop db container") +} From 8a8f783da2a4ed9413f26a5beef501c2700ad5bf Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 16 May 2025 10:57:50 +0200 Subject: [PATCH 2/8] Add go.sum --- go.sum | 155 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 go.sum diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..51c198d --- /dev/null +++ b/go.sum @@ -0,0 +1,155 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +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/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +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 v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +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/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/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.0.1+incompatible h1:FCHjSRdXhNRFjlHMTv4jUNlIBbTeRjrWfeFuJp7jpo0= +github.com/docker/docker v28.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +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/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= +github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +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.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +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/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/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= +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/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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/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/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= +github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +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.7.0/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/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg= +github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM= +github.com/testcontainers/testcontainers-go/modules/postgres v0.37.0 h1:hsVwFkS6s+79MbKEO+W7A1wNIw1fmkMtF4fg83m6kbc= +github.com/testcontainers/testcontainers-go/modules/postgres v0.37.0/go.mod h1:Qj/eGbRbO/rEYdcRLmN+bEojzatP/+NS1y8ojl2PQsc= +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/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.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +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.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +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/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/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/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-20220715151400-c0bba94af5f8/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.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +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/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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +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= From 51441a3fb3a947ebe5cb5bb55322c69f7bbf37e9 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 16 May 2025 13:06:54 +0200 Subject: [PATCH 3/8] SetColumn default; more testing --- pgcql/pg_definition.go | 3 ++ pgcql/pg_field_string.go | 3 +- pgcql/pgcql.go | 2 +- pgcql/pgcql_test.go | 9 ++---- pgcql/pgx_test.go | 63 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 9 deletions(-) diff --git a/pgcql/pg_definition.go b/pgcql/pg_definition.go index 3c276fb..971a140 100644 --- a/pgcql/pg_definition.go +++ b/pgcql/pg_definition.go @@ -7,6 +7,9 @@ type PgDefinition struct { } func (pg *PgDefinition) AddField(name string, field Field) Definition { + if field.GetColumn() == "" { + field.SetColumn(name) + } if pg.fields == nil { pg.fields = make(map[string]Field) } diff --git a/pgcql/pg_field_string.go b/pgcql/pg_field_string.go index b55584d..5bdcffc 100644 --- a/pgcql/pg_field_string.go +++ b/pgcql/pg_field_string.go @@ -17,9 +17,8 @@ func (f *FieldString) GetColumn() string { return f.column } -func (f *FieldString) WithColumn(column string) Field { +func (f *FieldString) SetColumn(column string) { f.column = column - return f } func (f *FieldString) WithFullText(language string) Field { diff --git a/pgcql/pgcql.go b/pgcql/pgcql.go index 97255ea..d98d1e3 100644 --- a/pgcql/pgcql.go +++ b/pgcql/pgcql.go @@ -14,7 +14,7 @@ func (e *PgError) Error() string { type Field interface { GetColumn() string - WithColumn(column string) Field + SetColumn(column string) Generate(sc cql.SearchClause, queryArgumentIndex int) (string, []any, error) } diff --git a/pgcql/pgcql_test.go b/pgcql/pgcql_test.go index c42dd7b..cd46ab9 100644 --- a/pgcql/pgcql_test.go +++ b/pgcql/pgcql_test.go @@ -23,18 +23,15 @@ func TestBadSearchClause(t *testing.T) { func TestParsing(t *testing.T) { def := &PgDefinition{} title := &FieldString{} - title.WithExact() - title.WithColumn("Title") + title.WithExact().SetColumn("Title") assert.Equal(t, title.GetColumn(), "Title", "GetColumn() should return the column name") author := &FieldString{} - author.WithColumn("Author") - author.WithLikeOps() + author.WithLikeOps().SetColumn("Author") serverChoice := &FieldString{} - serverChoice.WithExact() - serverChoice.WithColumn("T") + serverChoice.WithExact().SetColumn("T") def.AddField("title", title).AddField("author", author).AddField("cql.serverChoice", serverChoice) diff --git a/pgcql/pgx_test.go b/pgcql/pgx_test.go index 90694e9..c076ed1 100644 --- a/pgcql/pgx_test.go +++ b/pgcql/pgx_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/indexdata/cql-go/cql" "github.com/jackc/pgx/v5" "github.com/stretchr/testify/assert" "github.com/testcontainers/testcontainers-go" @@ -34,7 +35,69 @@ func TestPgx(t *testing.T) { err := conn.Close(ctx) assert.NoError(t, err, "failed to close db connection") }() + _, err = conn.Exec(ctx, "CREATE TABLE mytable (id SERIAL PRIMARY KEY, title TEXT, author TEXT, year INT)") + assert.NoError(t, err, "failed to create mytable") + var rows pgx.Rows + + rows, err = conn.Query(ctx, "INSERT INTO mytable (title, author, year) VALUES ($1, $2, $3)", "the art of computer programming, volume 1", "donald e. knuth", 1968) + assert.NoError(t, err, "failed to insert data") + rows.Close() + + rows, err = conn.Query(ctx, "INSERT INTO mytable (title, author, year) VALUES ($1, $2, $3)", "the TeXbook", "d. e. knuth", 1984) + assert.NoError(t, err, "failed to insert data") + rows.Close() + + rows, err = conn.Query(ctx, "INSERT INTO mytable (title, year) VALUES ($1, $2)", "anonymous", 2025) + assert.NoError(t, err, "failed to insert data") + rows.Close() + + t.Run("exact", func(t *testing.T) { + def := &PgDefinition{} + + def.AddField("title", (&FieldString{}).WithExact()) + def.AddField("author", (&FieldString{}).WithExact()) + def.AddField("year", (&FieldString{}).WithExact()) + + var parser cql.Parser + for _, testcase := range []struct { + query string + expectedIds []int + }{ + {"title = \"the TeXbook\"", []int{2}}, + {"title = \"the texbook\"", []int{}}, + {"title = \"the \"", []int{}}, + {"title = \"\"", []int{1, 2, 3}}, + {"author = \"\"", []int{1, 2}}, + {"title = \"the art of computer programming, volume 1\"", []int{1}}, + {"author = \"d. e. knuth\"", []int{2}}, + {"author = \"donald e. knuth\"", []int{1}}, + {"title = \"the TeXbook\" AND author = \"d. e. knuth\"", []int{2}}, + {"title = \"the TeXbook\" AND author = \"donald e. knuth\"", []int{}}, + {"title = \"the TeXbook\" OR author = \"d. e. knuth\"", []int{2}}, + {"title = \"the TeXbook\" OR author = \"donald e. knuth\"", []int{1, 2}}, + {"title = \"the TeXbook\" AND author = \"d. e. knuth\" AND year = 1984", []int{2}}, + {"title = \"the TeXbook\" AND author = \"d. e. knuth\" AND year = 1968", []int{}}, + {"title = \"the TeXbook\" AND author = \"d. e. knuth\" AND year = 1984 AND title = \"the art of computer programming, volume 1\"", []int{}}, + {"title = \"the TeXbook\" AND author = \"d. e. knuth\" AND year = 1984 AND title = \"the TeXbook\"", []int{2}}, + } { + q, err := parser.Parse(testcase.query) + assert.NoErrorf(t, err, "failed to parse cql query '%s'", testcase.query) + res, err := def.Parse(q, 1) + assert.NoErrorf(t, err, "failed to parse pgcql query for cql query '%s'", testcase.query) + rows, err = conn.Query(ctx, "SELECT id FROM mytable WHERE "+res.GetWhereClause(), res.GetQueryArguments()...) + assert.NoErrorf(t, err, "failed to execute query for cql query '%s'", testcase.query) + ids := make([]int, 0) + for rows.Next() { + var id int + err := rows.Scan(&id) + assert.NoErrorf(t, err, "failed to scan for cql query '%s'", testcase.query) + ids = append(ids, id) + } + assert.Equal(t, testcase.expectedIds, ids, "expected ids %v, got %v for query '%s'", testcase.expectedIds, ids, testcase.query) + rows.Close() + } + }) err = pgContainer.Terminate(ctx) assert.NoError(t, err, "failed to stop db container") } From 03cf22dff53ef6793b64143b744eed267dd18d36 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 16 May 2025 16:42:35 +0200 Subject: [PATCH 4/8] String field done --- pgcql/pg_field_string.go | 13 ++---- pgcql/pgcql_test.go | 55 +++++++++++++++++------- pgcql/pgx_test.go | 90 ++++++++++++++++++++++++++++++++-------- 3 files changed, 118 insertions(+), 40 deletions(-) diff --git a/pgcql/pg_field_string.go b/pgcql/pg_field_string.go index 5bdcffc..dea9dd5 100644 --- a/pgcql/pg_field_string.go +++ b/pgcql/pg_field_string.go @@ -64,8 +64,6 @@ func maskedExact(cqlTerm string) (string, error) { return "", fmt.Errorf("anchor op ^ unsupported") case '\\': // Do nothing, just set backslash to true - case '\'': - pgTerm = append(pgTerm, '\'', '\'') default: pgTerm = append(pgTerm, c) } @@ -108,8 +106,6 @@ func maskedLike(cqlTerm string) (string, bool, error) { // Do nothing, just set backslash to true case '%', '_': pgTerm = append(pgTerm, '\\', c) - case '\'': - pgTerm = append(pgTerm, '\'', '\'') default: pgTerm = append(pgTerm, c) } @@ -159,9 +155,8 @@ func (f *FieldString) Generate(sc cql.SearchClause, queryArgumentIndex int) (str if err != nil { return "", nil, err } - // TODO.. add to arguments - sql := "to_tsvector('" + f.language + "', " + f.column + ") @@ " + pgfunc + "('" + f.language + "', '" + pgTerm + "')" - return sql, nil, nil + sql := "to_tsvector('" + f.language + "', " + f.column + ") @@ " + pgfunc + "('" + f.language + "', " + fmt.Sprintf("$%d", queryArgumentIndex) + ")" + return sql, []any{pgTerm}, nil } if !f.enableExact { return "", nil, &PgError{message: "exact search not supported"} @@ -176,7 +171,7 @@ func (f *FieldString) Generate(sc cql.SearchClause, queryArgumentIndex int) (str if sc.Relation == cql.NE { pgOp = "NOT LIKE" } - return f.column + " " + pgOp + fmt.Sprintf(" $%d", queryArgumentIndex), []interface{}{pgTerm}, nil + return f.column + " " + pgOp + fmt.Sprintf(" $%d", queryArgumentIndex), []any{pgTerm}, nil } } pgTerm, err := maskedExact(sc.Term) @@ -187,5 +182,5 @@ func (f *FieldString) Generate(sc cql.SearchClause, queryArgumentIndex int) (str if err != nil { return "", nil, err } - return f.column + " " + pgOp + fmt.Sprintf(" $%d", queryArgumentIndex), []interface{}{pgTerm}, nil + return f.column + " " + pgOp + fmt.Sprintf(" $%d", queryArgumentIndex), []any{pgTerm}, nil } diff --git a/pgcql/pgcql_test.go b/pgcql/pgcql_test.go index cd46ab9..630d7a3 100644 --- a/pgcql/pgcql_test.go +++ b/pgcql/pgcql_test.go @@ -33,29 +33,56 @@ func TestParsing(t *testing.T) { serverChoice := &FieldString{} serverChoice.WithExact().SetColumn("T") - def.AddField("title", title).AddField("author", author).AddField("cql.serverChoice", serverChoice) + full := &FieldString{} + full.WithFullText("english") + + def.AddField("title", title).AddField("author", author).AddField("cql.serverChoice", serverChoice).AddField("full", full) for _, testcase := range []struct { query string expected string - expectedArgs []interface{} + expectedArgs []any }{ - {"abc", "T = $1", []interface{}{"abc"}}, - {"\"\"", "T IS NOT NULL", []interface{}{}}, + {"abc", "T = $1", []any{"abc"}}, + {"\"\"", "T IS NOT NULL", []any{}}, {"au=2", "error: unknown field au", nil}, {"title>2", "error: unsupported relation >", nil}, - {"title=2", "Title = $1", []interface{}{"2"}}, - {"title<>2", "Title <> $1", []interface{}{"2"}}, - {"a or b and c", "(T = $1 OR T = $2) AND T = $3", []interface{}{"a", "b", "c"}}, - {"title = abc", "Title = $1", []interface{}{"abc"}}, - {"author = \"test\"", "Author = $1", []interface{}{"test"}}, - {"title = a AND author = b c", "Title = $1 AND Author = $2", []interface{}{"a", "b c"}}, - {"title = 'a' OR author = 'b'", "Title = $1 OR Author = $2", []interface{}{"''a''", "''b''"}}, - {"title = a NOT author = b", "Title = $1 AND NOT Author = $2", []interface{}{"a", "b"}}, - {"a prox b", "error: unsupported operator prox", []interface{}{}}, - {"a sortby title", "error: sorting not supported", []interface{}{}}, + {"title=2", "Title = $1", []any{"2"}}, + {"title<>2", "Title <> $1", []any{"2"}}, + {"a or b and c", "(T = $1 OR T = $2) AND T = $3", []any{"a", "b", "c"}}, + {"title = abc", "Title = $1", []any{"abc"}}, + {"author = \"test\"", "Author = $1", []any{"test"}}, + {"author <> \"test\"", "Author <> $1", []any{"test"}}, + {"author = \"test*\"", "Author LIKE $1", []any{"test%"}}, + {"author <> \"test*\"", "Author NOT LIKE $1", []any{"test%"}}, + {"title = a AND author = b c", "Title = $1 AND Author = $2", []any{"a", "b c"}}, + {"title = 'a' OR author = 'b'", "Title = $1 OR Author = $2", []any{"'a'", "'b'"}}, + {"title = a NOT author = b", "Title = $1 AND NOT Author = $2", []any{"a", "b"}}, + {"a prox b", "error: unsupported operator prox", []any{}}, + {"a sortby title", "error: sorting not supported", []any{}}, {"au=2 or a", "error: unknown field au", nil}, {"a or au=2", "error: unknown field au", nil}, + {"author=\"ab?%\"", "Author LIKE $1", []any{"ab_\\%"}}, + {"author=\"ab*_\"", "Author LIKE $1", []any{"ab%\\_"}}, + {"author=\"a^\"", "error: anchor op ^ unsupported", nil}, + {"author=\"a*\\", "error: a CQL string must not end with a masking backslash", nil}, + {"author=\"a*\\x\"", "error: a masking backslash in a CQL string must be followed by *, ?, ^, \" or \\", nil}, + {"author=\"a*\\*\\\"\\?\\^\\\\", "Author LIKE $1", []any{"a%*\"?^\\\\"}}, + {"author=\"a\\*\\\"\\?\\^\\\\", "Author = $1", []any{"a*\"?^\\"}}, + + {"title=\"a*\"", "error: masking op * unsupported", nil}, + {"title=\"a?\"", "error: masking op ? unsupported", nil}, + {"title=\"a^\"", "error: anchor op ^ unsupported", nil}, + {"title=\"a\\*\"", "Title = $1", []any{"a*"}}, + {"title=\"a\\?\"", "Title = $1", []any{"a?"}}, + {"title=\"a\\^\"", "Title = $1", []any{"a^"}}, + {"title=\"a\\", "error: a CQL string must not end with a masking backslash", nil}, + {"title=\"a\\x\"", "error: a masking backslash in a CQL string must be followed by *, ?, ^, \" or \\", nil}, + {"full = \"abc\"", "to_tsvector('english', full) @@ phraseto_tsquery('english', $1)", []any{"abc"}}, + {"full adj \"abc\"", "to_tsvector('english', full) @@ phraseto_tsquery('english', $1)", []any{"abc"}}, + {"full all \"abc\"", "to_tsvector('english', full) @@ plainto_tsquery('english', $1)", []any{"abc"}}, + {"full=\"a*\"", "error: masking op * unsupported", nil}, + {"full any x", "error: exact search not supported", nil}, } { var parser cql.Parser q, err := parser.Parse(testcase.query) diff --git a/pgcql/pgx_test.go b/pgcql/pgx_test.go index c076ed1..f65d1de 100644 --- a/pgcql/pgx_test.go +++ b/pgcql/pgx_test.go @@ -13,6 +13,25 @@ import ( "github.com/testcontainers/testcontainers-go/wait" ) +func runQuery(t *testing.T, parser cql.Parser, conn *pgx.Conn, ctx context.Context, def *PgDefinition, query string, expectedIds []int) { + q, err := parser.Parse(query) + assert.NoErrorf(t, err, "failed to parse cql query '%s'", query) + res, err := def.Parse(q, 1) + assert.NoErrorf(t, err, "failed to parse pgcql query for cql query '%s'", query) + var rows pgx.Rows + rows, err = conn.Query(ctx, "SELECT id FROM mytable WHERE "+res.GetWhereClause(), res.GetQueryArguments()...) + assert.NoErrorf(t, err, "failed to execute pgx query for cql query '%s' whereClause='%s'", query, res.GetWhereClause()) + ids := make([]int, 0) + for rows.Next() { + var id int + err := rows.Scan(&id) + assert.NoErrorf(t, err, "failed to scan for cql query '%s'", query) + ids = append(ids, id) + } + assert.Equal(t, expectedIds, ids, "expected ids %v, got %v for query '%s'", expectedIds, ids, query) + rows.Close() +} + func TestPgx(t *testing.T) { ctx := context.Background() pgContainer, err := postgres.Run(ctx, "postgres", @@ -48,11 +67,11 @@ func TestPgx(t *testing.T) { assert.NoError(t, err, "failed to insert data") rows.Close() - rows, err = conn.Query(ctx, "INSERT INTO mytable (title, year) VALUES ($1, $2)", "anonymous", 2025) + rows, err = conn.Query(ctx, "INSERT INTO mytable (title, year) VALUES ($1, $2)", "anonymous' list", 2025) assert.NoError(t, err, "failed to insert data") rows.Close() - t.Run("exact", func(t *testing.T) { + t.Run("exact ops", func(t *testing.T) { def := &PgDefinition{} def.AddField("title", (&FieldString{}).WithExact()) @@ -80,24 +99,61 @@ func TestPgx(t *testing.T) { {"title = \"the TeXbook\" AND author = \"d. e. knuth\" AND year = 1968", []int{}}, {"title = \"the TeXbook\" AND author = \"d. e. knuth\" AND year = 1984 AND title = \"the art of computer programming, volume 1\"", []int{}}, {"title = \"the TeXbook\" AND author = \"d. e. knuth\" AND year = 1984 AND title = \"the TeXbook\"", []int{2}}, + {"title = \"anonymous' list\"", []int{3}}, + {"title = \"anonymous'' list\"", []int{}}, + {"title = \"anonymous list\"", []int{}}, + } { + runQuery(t, parser, conn, ctx, def, testcase.query, testcase.expectedIds) + } + }) + + t.Run("like ops", func(t *testing.T) { + def := &PgDefinition{} + + def.AddField("title", (&FieldString{}).WithLikeOps()) + def.AddField("author", (&FieldString{}).WithLikeOps()) + def.AddField("year", (&FieldString{}).WithLikeOps()) + + var parser cql.Parser + for _, testcase := range []struct { + query string + expectedIds []int + }{ + {"title = \"the TeX*\"", []int{2}}, + {"title = \"the Te?book\"", []int{2}}, + {"title = \"anonymous' l*\"", []int{3}}, } { - q, err := parser.Parse(testcase.query) - assert.NoErrorf(t, err, "failed to parse cql query '%s'", testcase.query) - res, err := def.Parse(q, 1) - assert.NoErrorf(t, err, "failed to parse pgcql query for cql query '%s'", testcase.query) - rows, err = conn.Query(ctx, "SELECT id FROM mytable WHERE "+res.GetWhereClause(), res.GetQueryArguments()...) - assert.NoErrorf(t, err, "failed to execute query for cql query '%s'", testcase.query) - ids := make([]int, 0) - for rows.Next() { - var id int - err := rows.Scan(&id) - assert.NoErrorf(t, err, "failed to scan for cql query '%s'", testcase.query) - ids = append(ids, id) - } - assert.Equal(t, testcase.expectedIds, ids, "expected ids %v, got %v for query '%s'", testcase.expectedIds, ids, testcase.query) - rows.Close() + runQuery(t, parser, conn, ctx, def, testcase.query, testcase.expectedIds) } }) + + t.Run("fulltext ops", func(t *testing.T) { + def := &PgDefinition{} + + def.AddField("title", (&FieldString{}).WithFullText("simple")) + def.AddField("author", (&FieldString{}).WithFullText("")) + def.AddField("year", (&FieldString{}).WithLikeOps()) + + var parser cql.Parser + for _, testcase := range []struct { + query string + expectedIds []int + }{ + {"title = \"the TeXbook\"", []int{2}}, + {"title = \"the Texbook\"", []int{2}}, + {"title = \"Texbook\"", []int{2}}, + {"title = \"Texboo\"", []int{}}, + {"author all \"knuth d e\"", []int{2}}, + {"author = \"d e knuth\"", []int{2}}, + {"author adj \"d e knuth\"", []int{2}}, + {"author adj \"e knuth\"", []int{1, 2}}, + {"author adj \"e d knuth\"", []int{}}, + } { + runQuery(t, parser, conn, ctx, def, testcase.query, testcase.expectedIds) + } + }) + err = pgContainer.Terminate(ctx) assert.NoError(t, err, "failed to stop db container") + } From f1425970aaec75ba6d01de9fc3792a222ffce185 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 16 May 2025 17:00:39 +0200 Subject: [PATCH 5/8] any --- pgcql/pg_query.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pgcql/pg_query.go b/pgcql/pg_query.go index 5604813..44d729f 100644 --- a/pgcql/pg_query.go +++ b/pgcql/pg_query.go @@ -15,7 +15,7 @@ type PgQuery struct { func (p *PgQuery) parse(q cql.Query, queryArgumentIndex int, def *PgDefinition) error { p.def = def - p.arguments = make([]interface{}, 0) + p.arguments = make([]any, 0) p.queryArgumentIndex = queryArgumentIndex if q.SortSpec != nil { return &PgError{message: "sorting not supported"} From 925bd6e53ccfe20359ac9cec85826696b6af3a46 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Fri, 16 May 2025 21:55:00 +0200 Subject: [PATCH 6/8] FieldNumber and base/common FieldCommon --- pgcql/pg_field_common.go | 47 ++++++++++++++++++++++++++++++++++++++++ pgcql/pg_field_number.go | 28 ++++++++++++++++++++++++ pgcql/pg_field_string.go | 23 ++------------------ pgcql/pgcql_test.go | 16 ++++++++++++++ pgcql/pgx_test.go | 11 +++++++--- 5 files changed, 101 insertions(+), 24 deletions(-) create mode 100644 pgcql/pg_field_common.go create mode 100644 pgcql/pg_field_number.go diff --git a/pgcql/pg_field_common.go b/pgcql/pg_field_common.go new file mode 100644 index 0000000..866d75b --- /dev/null +++ b/pgcql/pg_field_common.go @@ -0,0 +1,47 @@ +package pgcql + +import ( + "fmt" + + "github.com/indexdata/cql-go/cql" +) + +type FieldCommon struct { + column string +} + +func (f *FieldCommon) GetColumn() string { + return f.column +} +func (f *FieldCommon) SetColumn(column string) { + f.column = column +} + +func (f *FieldCommon) handleUnorderedRelation(sc cql.SearchClause) (string, error) { + switch sc.Relation { + case "==", cql.EXACT, cql.EQ: + return "=", nil + case cql.NE: + return "<>", nil + default: + return "", fmt.Errorf("unsupported relation %s", sc.Relation) + } +} + +func (f *FieldCommon) handleOrderedRelation(sc cql.SearchClause) (string, error) { + switch sc.Relation { + case "==", cql.EXACT: + return "=", nil + case "=", "<>", ">", "<", "<=", ">=": + return string(sc.Relation), nil + default: + return "", &PgError{message: "unsupported operator " + string(sc.Relation)} + } +} + +func (f *FieldCommon) handleEmptyTerm(sc cql.SearchClause) string { + if sc.Term == "" && sc.Relation == cql.EQ { + return f.column + " IS NOT NULL" + } + return "" +} diff --git a/pgcql/pg_field_number.go b/pgcql/pg_field_number.go new file mode 100644 index 0000000..2a7731c --- /dev/null +++ b/pgcql/pg_field_number.go @@ -0,0 +1,28 @@ +package pgcql + +import ( + "fmt" + "strconv" + + "github.com/indexdata/cql-go/cql" +) + +type FieldNumber struct { + FieldCommon +} + +func (f *FieldNumber) Generate(sc cql.SearchClause, queryArgumentIndex int) (string, []any, error) { + s := f.handleEmptyTerm(sc) + if s != "" { + return s, []any{}, nil + } + relOrdered, err := f.handleOrderedRelation(sc) + if err != nil { + return "", nil, err + } + number, err := strconv.ParseFloat(sc.Term, 64) + if err != nil { + return "", nil, &PgError{message: fmt.Sprintf("invalid number %s", sc.Term)} + } + return f.column + " " + relOrdered + fmt.Sprintf(" $%d", queryArgumentIndex), []any{number}, nil +} diff --git a/pgcql/pg_field_string.go b/pgcql/pg_field_string.go index dea9dd5..6c936dd 100644 --- a/pgcql/pg_field_string.go +++ b/pgcql/pg_field_string.go @@ -7,20 +7,12 @@ import ( ) type FieldString struct { - column string + FieldCommon language string enableLike bool enableExact bool } -func (f *FieldString) GetColumn() string { - return f.column -} - -func (f *FieldString) SetColumn(column string) { - f.column = column -} - func (f *FieldString) WithFullText(language string) Field { if language == "" { f.language = "simple" @@ -125,17 +117,6 @@ func (f *FieldString) handleEmptyTerm(sc cql.SearchClause) string { return "" } -func unorderedRelation(sc cql.SearchClause) (string, error) { - switch sc.Relation { - case cql.EXACT, cql.EQ: - return "=", nil - case cql.NE: - return "<>", nil - default: - return "", fmt.Errorf("unsupported relation %s", sc.Relation) - } -} - func (f *FieldString) Generate(sc cql.SearchClause, queryArgumentIndex int) (string, []any, error) { sql := f.handleEmptyTerm(sc) if sql != "" { @@ -178,7 +159,7 @@ func (f *FieldString) Generate(sc cql.SearchClause, queryArgumentIndex int) (str if err != nil { return "", nil, err } - pgOp, err := unorderedRelation(sc) + pgOp, err := f.handleUnorderedRelation(sc) if err != nil { return "", nil, err } diff --git a/pgcql/pgcql_test.go b/pgcql/pgcql_test.go index 630d7a3..e511681 100644 --- a/pgcql/pgcql_test.go +++ b/pgcql/pgcql_test.go @@ -38,6 +38,9 @@ func TestParsing(t *testing.T) { def.AddField("title", title).AddField("author", author).AddField("cql.serverChoice", serverChoice).AddField("full", full) + price := &FieldNumber{} + def.AddField("price", price) + for _, testcase := range []struct { query string expected string @@ -48,6 +51,8 @@ func TestParsing(t *testing.T) { {"au=2", "error: unknown field au", nil}, {"title>2", "error: unsupported relation >", nil}, {"title=2", "Title = $1", []any{"2"}}, + {"title==2", "Title = $1", []any{"2"}}, + {"title exact 2", "Title = $1", []any{"2"}}, {"title<>2", "Title <> $1", []any{"2"}}, {"a or b and c", "(T = $1 OR T = $2) AND T = $3", []any{"a", "b", "c"}}, {"title = abc", "Title = $1", []any{"abc"}}, @@ -83,6 +88,17 @@ func TestParsing(t *testing.T) { {"full all \"abc\"", "to_tsvector('english', full) @@ plainto_tsquery('english', $1)", []any{"abc"}}, {"full=\"a*\"", "error: masking op * unsupported", nil}, {"full any x", "error: exact search not supported", nil}, + {"price = 10", "price = $1", []any{10.0}}, + {"price == 10", "price = $1", []any{10.0}}, + {"price exact 10", "price = $1", []any{10.0}}, + {"price > 10.95", "price > $1", []any{10.95}}, + {"price < 10.95", "price < $1", []any{10.95}}, + {"price >= 10.95", "price >= $1", []any{10.95}}, + {"price < 10.95", "price < $1", []any{10.95}}, + {"price <= 10.95", "price <= $1", []any{10.95}}, + {"price <= beta", "error: invalid number beta", nil}, + {"price all 10.95", "error: unsupported operator all", nil}, + {"price = \"\"", "price IS NOT NULL", []any{}}, } { var parser cql.Parser q, err := parser.Parse(testcase.query) diff --git a/pgcql/pgx_test.go b/pgcql/pgx_test.go index f65d1de..19d9c55 100644 --- a/pgcql/pgx_test.go +++ b/pgcql/pgx_test.go @@ -76,7 +76,7 @@ func TestPgx(t *testing.T) { def.AddField("title", (&FieldString{}).WithExact()) def.AddField("author", (&FieldString{}).WithExact()) - def.AddField("year", (&FieldString{}).WithExact()) + def.AddField("year", (&FieldNumber{})) var parser cql.Parser for _, testcase := range []struct { @@ -102,6 +102,11 @@ func TestPgx(t *testing.T) { {"title = \"anonymous' list\"", []int{3}}, {"title = \"anonymous'' list\"", []int{}}, {"title = \"anonymous list\"", []int{}}, + {"year <> 1984", []int{1, 3}}, + {"year < 1984", []int{1}}, + {"year <= 1984", []int{1, 2}}, + {"year >= 1984", []int{2, 3}}, + {"year > 1984", []int{3}}, } { runQuery(t, parser, conn, ctx, def, testcase.query, testcase.expectedIds) } @@ -112,7 +117,7 @@ func TestPgx(t *testing.T) { def.AddField("title", (&FieldString{}).WithLikeOps()) def.AddField("author", (&FieldString{}).WithLikeOps()) - def.AddField("year", (&FieldString{}).WithLikeOps()) + def.AddField("year", (&FieldNumber{})) var parser cql.Parser for _, testcase := range []struct { @@ -132,7 +137,7 @@ func TestPgx(t *testing.T) { def.AddField("title", (&FieldString{}).WithFullText("simple")) def.AddField("author", (&FieldString{}).WithFullText("")) - def.AddField("year", (&FieldString{}).WithLikeOps()) + def.AddField("year", (&FieldNumber{})) var parser cql.Parser for _, testcase := range []struct { From 3fe21678367c234198410be89e3cbfe0dd2e8f2d Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Sat, 17 May 2025 19:05:25 +0200 Subject: [PATCH 7/8] Small cli for pgcql --- .gitignore | 1 + Makefile | 2 +- cmd/pgcql-cli/main.go | 50 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 cmd/pgcql-cli/main.go diff --git a/.gitignore b/.gitignore index 67c027b..0d09638 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /cql-cli +/pgcql-cli .vscode cover.out diff --git a/Makefile b/Makefile index 216d4b6..045ab2d 100644 --- a/Makefile +++ b/Makefile @@ -15,4 +15,4 @@ lint: clean: rm -f cover.out - rm -f cql-cli + rm -f cql-cli pgcql-cli diff --git a/cmd/pgcql-cli/main.go b/cmd/pgcql-cli/main.go new file mode 100644 index 0000000..aaad49c --- /dev/null +++ b/cmd/pgcql-cli/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/indexdata/cql-go/cql" + "github.com/indexdata/cql-go/pgcql" +) + +func main() { + var serverChoiceColumn string + flag.StringVar(&serverChoiceColumn, "s", "text", "column for cql.serverChoice") + def := &pgcql.PgDefinition{} + if serverChoiceColumn != "" { + serverChoice := &pgcql.FieldString{} + serverChoice.WithFullText("english").SetColumn(serverChoiceColumn) + def.AddField("cql.serverChoice", serverChoice) + } + flag.Parse() + if len(flag.Args()) == 0 { + fmt.Println("Usage: pgcql-cli [-s serverchoicefield] field .. query") + fmt.Println("Example: pgcql-cli -s notes ti \"free and ti=powerful\"") + os.Exit(1) + } + for i := 0; i < len(flag.Args()); i++ { + if i < len(flag.Args())-1 { + field := &pgcql.FieldString{} + field.WithLikeOps() + def.AddField(flag.Args()[i], field) + continue + } + var parser cql.Parser + query, err := parser.Parse(flag.Args()[i]) + if err != nil { + fmt.Printf("cql error: %s", err) + return + } + res, err := def.Parse(query, 1) + if err != nil { + fmt.Printf("pgcql error: %s\n", err) + return + } + fmt.Printf("whereByClause: %s\n", res.GetWhereClause()) + for j, qa := range res.GetQueryArguments() { + fmt.Printf("$%d: %v\n", j+1, qa) + } + } +} From 5649b9f7d9e390cb46fc703c1e6025b62bc5ae20 Mon Sep 17 00:00:00 2001 From: Adam Dickmeiss Date: Sat, 17 May 2025 19:16:11 +0200 Subject: [PATCH 8/8] Constructor for PgDefinition that returns Definition --- cmd/pgcql-cli/main.go | 2 +- pgcql/pg_definition.go | 4 ++++ pgcql/pgcql_test.go | 4 ++-- pgcql/pgx_test.go | 9 ++++----- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/cmd/pgcql-cli/main.go b/cmd/pgcql-cli/main.go index aaad49c..11049f2 100644 --- a/cmd/pgcql-cli/main.go +++ b/cmd/pgcql-cli/main.go @@ -12,7 +12,7 @@ import ( func main() { var serverChoiceColumn string flag.StringVar(&serverChoiceColumn, "s", "text", "column for cql.serverChoice") - def := &pgcql.PgDefinition{} + def := pgcql.NewPgDefinition() if serverChoiceColumn != "" { serverChoice := &pgcql.FieldString{} serverChoice.WithFullText("english").SetColumn(serverChoiceColumn) diff --git a/pgcql/pg_definition.go b/pgcql/pg_definition.go index 971a140..22598ac 100644 --- a/pgcql/pg_definition.go +++ b/pgcql/pg_definition.go @@ -6,6 +6,10 @@ type PgDefinition struct { fields map[string]Field } +func NewPgDefinition() Definition { + return &PgDefinition{} +} + func (pg *PgDefinition) AddField(name string, field Field) Definition { if field.GetColumn() == "" { field.SetColumn(name) diff --git a/pgcql/pgcql_test.go b/pgcql/pgcql_test.go index e511681..561af00 100644 --- a/pgcql/pgcql_test.go +++ b/pgcql/pgcql_test.go @@ -10,7 +10,7 @@ import ( ) func TestBadSearchClause(t *testing.T) { - def := &PgDefinition{} + def := NewPgDefinition() assert.Nil(t, def.GetFieldType("foo")) @@ -21,7 +21,7 @@ func TestBadSearchClause(t *testing.T) { } func TestParsing(t *testing.T) { - def := &PgDefinition{} + def := NewPgDefinition() title := &FieldString{} title.WithExact().SetColumn("Title") diff --git a/pgcql/pgx_test.go b/pgcql/pgx_test.go index 19d9c55..9975d4c 100644 --- a/pgcql/pgx_test.go +++ b/pgcql/pgx_test.go @@ -13,7 +13,7 @@ import ( "github.com/testcontainers/testcontainers-go/wait" ) -func runQuery(t *testing.T, parser cql.Parser, conn *pgx.Conn, ctx context.Context, def *PgDefinition, query string, expectedIds []int) { +func runQuery(t *testing.T, parser cql.Parser, conn *pgx.Conn, ctx context.Context, def Definition, query string, expectedIds []int) { q, err := parser.Parse(query) assert.NoErrorf(t, err, "failed to parse cql query '%s'", query) res, err := def.Parse(q, 1) @@ -72,7 +72,7 @@ func TestPgx(t *testing.T) { rows.Close() t.Run("exact ops", func(t *testing.T) { - def := &PgDefinition{} + def := NewPgDefinition() def.AddField("title", (&FieldString{}).WithExact()) def.AddField("author", (&FieldString{}).WithExact()) @@ -113,7 +113,7 @@ func TestPgx(t *testing.T) { }) t.Run("like ops", func(t *testing.T) { - def := &PgDefinition{} + def := NewPgDefinition() def.AddField("title", (&FieldString{}).WithLikeOps()) def.AddField("author", (&FieldString{}).WithLikeOps()) @@ -133,7 +133,7 @@ func TestPgx(t *testing.T) { }) t.Run("fulltext ops", func(t *testing.T) { - def := &PgDefinition{} + def := NewPgDefinition() def.AddField("title", (&FieldString{}).WithFullText("simple")) def.AddField("author", (&FieldString{}).WithFullText("")) @@ -160,5 +160,4 @@ func TestPgx(t *testing.T) { err = pgContainer.Terminate(ctx) assert.NoError(t, err, "failed to stop db container") - }