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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/gooq-gen/main.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Command gooq-gen generates typed table accessors for the jooq query builder by
// Command gooq-gen generates typed table accessors for the gooq query builder by
// introspecting a live database schema through the standard information_schema
// catalog. For each table it writes a "<table>.gen.go" file containing an
// embedded gooq.TableImpl, one typed Field per column, an As method for
// aliasing, key metadata accessors, and a package-level accessor variable.
//
// The command is a thin wrapper around the github.com/cgardev/gooq/codegen
// package. The jooq library itself imports no database driver. To run this
// package. The gooq library itself imports no database driver. To run this
// command, the caller builds it with their driver blank-imported, for example:
//
// import _ "github.com/jackc/pgx/v5/stdlib" // for the "postgres" driver
Expand Down
4 changes: 2 additions & 2 deletions codegen/emit.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"text/template"
)

// gooqImport is the import path of the runtime jooq package whose field types
// gooqImport is the import path of the runtime gooq package whose field types
// the generated code references.
const gooqImport = "github.com/cgardev/gooq"

Expand All @@ -31,7 +31,7 @@ type templateColumn struct {
Field string
// Type is the Go field type, such as "gooq.NumericField[int64]".
Type string
// Constructor is the jooq constructor for the field, such as
// Constructor is the gooq constructor for the field, such as
// "gooq.NewNumericField[int64]".
Constructor string
// Column is the unqualified database column name.
Expand Down
2 changes: 1 addition & 1 deletion codegen/emit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ func TestEmitTableTypeOverrideBySQLType(t *testing.T) {
}

// TestEmitTableCompiles asserts that the emitted source parses as valid Go. A
// full type check would require resolving the jooq import, so parsing is used as
// full type check would require resolving the gooq import, so parsing is used as
// the lightweight proof of syntactic validity.
func TestEmitTableCompiles(t *testing.T) {
table := TableSchema{
Expand Down
2 changes: 1 addition & 1 deletion codegen/introspect.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Package codegen introspects a PostgreSQL database and generates typed table
// accessors compatible with the jooq query builder package.
// accessors compatible with the gooq query builder package.
//
// The package depends only on the Go standard library. Database drivers are
// intentionally not imported; callers must blank-import the driver appropriate
Expand Down
6 changes: 3 additions & 3 deletions codegen/typemap.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package codegen

import "strings"

// goMapping describes how a SQL column type maps onto the jooq field types: the
// goMapping describes how a SQL column type maps onto the gooq field types: the
// Go field type used in the generated struct, the constructor that builds it,
// and any additional package imports the field type requires.
type goMapping struct {
Expand All @@ -11,7 +11,7 @@ type goMapping struct {
imports []string
}

// typeMapping captures the refined non-nullable jooq field mapping for a SQL
// typeMapping captures the refined non-nullable gooq field mapping for a SQL
// data type together with the information needed to derive its nullable mapping.
//
// A nullable column is mapped in one of three ways, in order of precedence:
Expand Down Expand Up @@ -54,7 +54,7 @@ func normalizeType(dataType string) string {
}

// mappingFor translates a normalized SQL data type into its mapping, capturing
// both the non-nullable jooq field descriptor and the underlying Go element
// both the non-nullable gooq field descriptor and the underlying Go element
// type. Recognized integer types map to a NumericField[int64], floating and
// fixed-point types to a NumericField[float64], boolean types to a Field[bool],
// temporal types to a Field[time.Time], binary types to a Field[[]byte], JSON
Expand Down
2 changes: 1 addition & 1 deletion doc.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Package jooq provides a type-safe, fluent SQL query builder for Go inspired
// Package gooq provides a type-safe, fluent SQL query builder for Go inspired
// by the Java library jOOQ. It combines parametric Field[T] columns, positional
// RecordN row types, step interfaces that make the clause order a compile-time
// concern, and runtime dialect translation from a single abstract syntax tree.
Expand Down
8 changes: 4 additions & 4 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import "errors"

// ErrTooManyRows is returned by FetchOne and FetchSingle when a query yields
// more rows than the caller expected.
var ErrTooManyRows = errors.New("jooq: query returned more than one row")
var ErrTooManyRows = errors.New("gooq: query returned more than one row")

// ErrReturningUnsupported is recorded when a RETURNING clause is requested for a
// dialect that does not support it. Both supported dialects (PostgreSQL and
// SQLite) render RETURNING natively, so this sentinel exists as a defensive
// guard for any dialect whose supportsReturning reports false.
var ErrReturningUnsupported = errors.New("jooq: RETURNING is not supported by this dialect")
var ErrReturningUnsupported = errors.New("gooq: RETURNING is not supported by this dialect")

// ErrEmptyInsert is recorded when an INSERT statement has neither columns nor a
// DEFAULT VALUES marker.
var ErrEmptyInsert = errors.New("jooq: INSERT has no columns or values")
var ErrEmptyInsert = errors.New("gooq: INSERT has no columns or values")

// ErrColumnValueMismatch is recorded when an inserted row has a different number
// of values than there are columns.
var ErrColumnValueMismatch = errors.New("jooq: column count does not match value count")
var ErrColumnValueMismatch = errors.New("gooq: column count does not match value count")
2 changes: 1 addition & 1 deletion example/main.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Command example demonstrates the jooq-for-go query builder against the
// Command example demonstrates the gooq-for-go query builder against the
// generated table accessors in ./internal/db. It renders representative queries
// rather than connecting to a database, so it runs with no external dependency
// and showcases the single-AST, render-per-dialect design.
Expand Down
6 changes: 3 additions & 3 deletions fakedb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import (
// *sql.Result values without any external dependency or live database.

func init() {
sql.Register("jooqfake", fakeDriver{})
sql.Register("gooqfake", fakeDriver{})
}

// openFakeDB returns a *sql.DB backed by the fake driver.
func openFakeDB() *sql.DB {
db, err := sql.Open("jooqfake", "")
db, err := sql.Open("gooqfake", "")
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -69,7 +69,7 @@ type fakeConn struct{}
func (*fakeConn) Prepare(query string) (driver.Stmt, error) { return &fakeStmt{}, nil }
func (*fakeConn) Close() error { return nil }
func (*fakeConn) Begin() (driver.Tx, error) {
return nil, errors.New("jooqfake: transactions unsupported")
return nil, errors.New("gooqfake: transactions unsupported")
}

func namedToValues(named []driver.NamedValue) []driver.Value {
Expand Down
6 changes: 3 additions & 3 deletions fetch_into.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ func newRowMapper[S any](rows *sql.Rows) (*rowMapper[S], error) {
var zero S
structType := reflect.TypeOf(zero)
if structType == nil || structType.Kind() != reflect.Struct {
return nil, fmt.Errorf("jooq: FetchInto target %T is not a struct type", zero)
return nil, fmt.Errorf("gooq: FetchInto target %T is not a struct type", zero)
}

columns, err := rows.Columns()
Expand Down Expand Up @@ -223,7 +223,7 @@ func (m *rowMapper[S]) columnIndex(column string) (int, error) {
return i, nil
}
}
return 0, fmt.Errorf("jooq: key column %q is not among the selected columns", column)
return 0, fmt.Errorf("gooq: key column %q is not among the selected columns", column)
}

// scan maps the current row into a new value of S.
Expand Down Expand Up @@ -282,7 +282,7 @@ func convertKey[K comparable](scanned any) (K, error) {
if keyType != nil && source.Type().ConvertibleTo(keyType) {
return source.Convert(keyType).Interface().(K), nil
}
return zero, fmt.Errorf("jooq: cannot use key value of type %T as key type %T", value, zero)
return zero, fmt.Errorf("gooq: cannot use key value of type %T as key type %T", value, zero)
}

// dereference unwraps a scan target back to the underlying scanned value. A
Expand Down
4 changes: 2 additions & 2 deletions fetch_into_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
// FetchMap, FetchGroups) and the typed RETURNING helpers (ReturningInto,
// ReturningOneInto). The golden cases assert the rendered RETURNING SQL for both
// dialects; the mapping cases run against the package's existing in-process
// "jooqfake" driver (see fakedb_test.go) so the reflection-based scanner is
// "gooqfake" driver (see fakedb_test.go) so the reflection-based scanner is
// exercised end to end without any external dependency.

func TestReturningIntoRendersStatementSQL(t *testing.T) {
Expand Down Expand Up @@ -67,7 +67,7 @@ func TestReturningIntoRendersStatementSQL(t *testing.T) {

// fakeStatement adapts a column list and row data already queued through
// queueRows to the statement interface, so the mapping helpers can run them
// through the jooqfake driver without rendering real SQL.
// through the gooqfake driver without rendering real SQL.
type fakeStatement struct{}

func (fakeStatement) SQL() (string, []any, error) { return "SELECT mapped", nil, nil }
Expand Down
11 changes: 11 additions & 0 deletions field.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,17 @@ type field[T any] struct {
func (f field[T]) render(b *builder) { f.expr.render(b) }
func (f field[T]) Name() string { return f.name }

// scanTargetProvider is implemented by typed fields that can allocate a scan
// destination of their element type. The dynamic Select uses it to scan each
// projected column into a value of the field's Go type, so dynamic Record rows
// carry the same value types the equivalent typed SelectN projection would
// produce, independent of the driver's native representations.
type scanTargetProvider interface {
scanTarget() any
}

func (f field[T]) scanTarget() any { return new(T) }

func (f field[T]) cmp(op string, v T) Condition {
return newCondition(&binaryPredicate{left: f.expr, op: op, right: bindOf(v)})
}
Expand Down
2 changes: 1 addition & 1 deletion helpers_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package gooq

// This file provides hand-written table definitions shaped exactly like the
// output of cmd/jooq-gen, so the tests exercise the same exported code paths a
// output of cmd/gooq-gen, so the tests exercise the same exported code paths a
// generated schema would: they embed TableImpl and build columns through the
// exported NewField/NewStringField/NewNumericField constructors.

Expand Down
4 changes: 2 additions & 2 deletions integration/generate.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Package integration contains the PostgreSQL integration tests for the jooq
// Package integration contains the PostgreSQL integration tests for the gooq
// query builder. The typed table accessors under internal/db are produced by
// running the jooq code generator against a live PostgreSQL database; they are
// running the gooq code generator against a live PostgreSQL database; they are
// not written by hand.
//
// Regenerate the accessors after changing testdata/schema.sql by running the
Expand Down
2 changes: 1 addition & 1 deletion integration/go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Module integration holds the database integration tests for the jooq library.
// Module integration holds the database integration tests for the gooq library.
// It is a SEPARATE Go module on purpose: the core module (github.com/cgardev/gooq)
// has zero external dependencies, and keeping testcontainers and the PostgreSQL
// driver here ensures that consuming the library never pulls them in. The core
Expand Down
2 changes: 1 addition & 1 deletion integration/harness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func library(t *testing.T) (context.Context, *sql.Tx) {
return ctx, tx
}

// seed inserts the canonical fixtures through the jooq insert builder. The data
// seed inserts the canonical fixtures through the gooq insert builder. The data
// is deliberately varied so the edge-case tests have something meaningful to
// query: two authors (one with a JSONB metadata document, one without), three
// books with different prices, page counts, print status, JSONB attribute
Expand Down
2 changes: 1 addition & 1 deletion integration/internal/gendb/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Command gendb regenerates the typed table accessors used by the integration
// tests. It starts a disposable PostgreSQL container, applies the authoritative
// schema from testdata/schema.sql, and runs the jooq code generator against the
// schema from testdata/schema.sql, and runs the gooq code generator against the
// live database. The generated files are written to internal/db.
//
// Run it from the integration module root with:
Expand Down
102 changes: 102 additions & 0 deletions integration/postgres_dynamic_select_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package integration

import (
"testing"
"time"

"github.com/cgardev/gooq"
"github.com/cgardev/gooq/integration/internal/db"
)

// postgres_dynamic_select_test.go exercises the dynamic Select end to end
// against the real PostgreSQL container: a projection whose arity is only
// established at run time, the typed Record accessors, and the empty-projection
// SELECT * convenience.

// TestPostgresDynamicSelectGroupByArity aggregates the seeded books under a
// GROUP BY column set assembled at run time, the motivating use case for the
// dynamic Select: a caller-supplied dimension list of unknown arity.
func TestPostgresDynamicSelectGroupByArity(t *testing.T) {
ctx, tx := library(t)

groupColumns := []gooq.Field[string]{db.Book.AuthorId}
fields := []gooq.AnyField{gooq.CountStar().As("total")}
groupBy := make([]gooq.AnyField, 0, len(groupColumns))
for _, column := range groupColumns {
fields = append(fields, column.As("dim_0"))
groupBy = append(groupBy, column)
}

records, err := gooq.Select(fields...).
From(db.Book).
GroupBy(groupBy...).
OrderBy(db.Book.AuthorId.Asc()).
Fetch(ctx, tx)
noError(t, "dynamic grouped select", err)

// The three seeded books are written by two distinct authors: Donovan wrote
// one and Kernighan wrote two.
equal(t, "group count", len(records), 2)

firstAuthor, err := gooq.Value[string](records[0], "dim_0")
noError(t, "read first group dimension", err)
equal(t, "first group author", firstAuthor, authorDonovan)
firstTotal, err := gooq.Value[int64](records[0], "total")
noError(t, "read first group total", err)
equal(t, "first group total", firstTotal, int64(1))

secondTotal, err := gooq.Value[int64](records[1], "total")
noError(t, "read second group total", err)
equal(t, "second group total", secondTotal, int64(2))
}

// TestPostgresDynamicSelectTypedValues verifies that dynamic Record values
// carry the projected fields' Go element types against a real driver: text as
// string, numeric expressions as float64, and timestamps as time.Time.
func TestPostgresDynamicSelectTypedValues(t *testing.T) {
ctx, tx := library(t)

publishedAt := gooq.Raw[time.Time]("\"book\".\"published_at\"")
records, err := gooq.Select(db.Book.Title, db.Book.Price, publishedAt.As("published_at")).
From(db.Book).
Where(db.Book.Id.EQ(bookGo)).
Fetch(ctx, tx)
noError(t, "dynamic typed select", err)
equal(t, "row count", len(records), 1)

title, err := gooq.Value[string](records[0], "title")
noError(t, "read title", err)
equal(t, "title", title, "The Go Programming Language")

price, err := gooq.ValueAt[float64](records[0], 1)
noError(t, "read price", err)
equal(t, "price", price, 39.99)

when, ok := records[0].GetByName("published_at").(time.Time)
if !ok {
t.Fatalf("published_at = %#v, want time.Time", records[0].GetByName("published_at"))
}
if !when.Equal(publishedGo) {
t.Errorf("published_at = %v, want %v", when, publishedGo)
}
}

// TestPostgresDynamicSelectAllColumns verifies the empty-projection SELECT *
// convenience: the column names come from the result set.
func TestPostgresDynamicSelectAllColumns(t *testing.T) {
ctx, tx := library(t)

records, err := gooq.Select().
From(db.Book).
Where(db.Book.Id.EQ(bookGo)).
Fetch(ctx, tx)
noError(t, "dynamic select star", err)
equal(t, "row count", len(records), 1)

if records[0].Size() == 0 {
t.Fatalf("expected the record to carry every table column")
}
title, err := gooq.Value[string](records[0], "title")
noError(t, "read title", err)
equal(t, "title", title, "The Go Programming Language")
}
2 changes: 1 addition & 1 deletion integration/postgres_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

// These tests read top to bottom as small stories. Each opens a seeded library,
// runs one fluent jooq query, and asserts the typed result. The container, the
// runs one fluent gooq query, and asserts the typed result. The container, the
// schema, the seeding, and the per-test rollback all live in harness_test.go, so
// nothing here repeats the plumbing.

Expand Down
38 changes: 38 additions & 0 deletions integration/sqlite_dynamic_select_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package integration

import (
"testing"

"github.com/cgardev/gooq"
"github.com/cgardev/gooq/integration/internal/db"
)

// sqlite_dynamic_select_test.go exercises the dynamic Select end to end against
// the pure-Go SQLite database: the run-time projection with GROUP BY and the
// typed Record accessors under a driver with different native representations.

// TestSQLiteDynamicSelectGroupByArity aggregates the seeded books under a
// run-time GROUP BY column set against SQLite.
func TestSQLiteDynamicSelectGroupByArity(t *testing.T) {
ctx, conn := sqliteLibrary(t)

records, err := gooq.Select(gooq.CountStar().As("total"), db.Book.AuthorId.As("dim_0")).
From(db.Book).
GroupBy(db.Book.AuthorId).
OrderBy(db.Book.AuthorId.Asc()).
Using(gooq.SQLite()).
Fetch(ctx, conn)
noError(t, "dynamic grouped select", err)

// The three seeded books are written by two distinct authors: Donovan wrote
// one and Kernighan wrote two.
equal(t, "group count", len(records), 2)

firstTotal, err := gooq.Value[int64](records[0], "total")
noError(t, "read first group total", err)
equal(t, "first group total", firstTotal, int64(1))

secondAuthor, err := gooq.Value[string](records[1], "dim_0")
noError(t, "read second group dimension", err)
equal(t, "second group author", secondAuthor, authorKernighan)
}
4 changes: 2 additions & 2 deletions internal/gen/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Command gen produces the repetitive higher-arity Record and Select code for
// the jooq package. It is invoked by the //go:generate directive in doc.go and
// writes record_gen.go and select_gen.go into the parent jooq package directory.
// the gooq package. It is invoked by the //go:generate directive in doc.go and
// writes record_gen.go and select_gen.go into the parent gooq package directory.
//
// Arities 1 through 5 are written by hand; this program emits 6 through 22.
package main
Expand Down
Loading
Loading