From 369eca47ea7470094fc13e4d338816ed084a95a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Garc=C3=ADa?= Date: Mon, 6 Jul 2026 10:53:13 +0000 Subject: [PATCH 1/2] feat: add dynamic select with run-time projection and record accessors Introduce Select(fields ...AnyField), the counterpart of jOOQ's DSL.select(SelectFieldOrAsterisk...) for projections whose arity only becomes known at run time, such as a GROUP BY column set derived from a request. The constructor produces dynamic Record rows and composes with the whole existing clause chain, including set operations and every terminal fetch. Each projected field that carries a Go element type is scanned into a value of that type through a new unexported scanTarget hook on the concrete field implementation, so Record values match the values the equivalent typed SelectN projection would produce, independent of the driver's native representations. Passing no fields mirrors the reference's empty-select convenience and renders SELECT *, taking the column names from the result set. Record mirrors the untyped org.jooq.Record: Size, Columns, Values, Get by index, and GetByName, plus the generic accessors Value and ValueAt mirroring Record.get(..., Class) with deliberate conversions only (byte slices relax to string and numeric kinds convert between one another) rather than silent reflect conversions. Covered by golden SQL and fake-driver unit tests plus PostgreSQL and SQLite integration suites, including the run-time GROUP BY arity use case. --- field.go | 11 + integration/postgres_dynamic_select_test.go | 102 +++++++++ integration/sqlite_dynamic_select_test.go | 38 ++++ record_dynamic.go | 133 ++++++++++++ select_dynamic.go | 78 +++++++ select_dynamic_test.go | 228 ++++++++++++++++++++ 6 files changed, 590 insertions(+) create mode 100644 integration/postgres_dynamic_select_test.go create mode 100644 integration/sqlite_dynamic_select_test.go create mode 100644 record_dynamic.go create mode 100644 select_dynamic.go create mode 100644 select_dynamic_test.go diff --git a/field.go b/field.go index d3d738e..0a2c9df 100644 --- a/field.go +++ b/field.go @@ -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)}) } diff --git a/integration/postgres_dynamic_select_test.go b/integration/postgres_dynamic_select_test.go new file mode 100644 index 0000000..7c18cee --- /dev/null +++ b/integration/postgres_dynamic_select_test.go @@ -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") +} diff --git a/integration/sqlite_dynamic_select_test.go b/integration/sqlite_dynamic_select_test.go new file mode 100644 index 0000000..7c54fd5 --- /dev/null +++ b/integration/sqlite_dynamic_select_test.go @@ -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) +} diff --git a/record_dynamic.go b/record_dynamic.go new file mode 100644 index 0000000..ff5dde8 --- /dev/null +++ b/record_dynamic.go @@ -0,0 +1,133 @@ +package gooq + +import ( + "fmt" + "reflect" + "strings" +) + +// Record is a dynamic row whose column count and names are established at run +// time, mirroring jOOQ's untyped org.jooq.Record. It is produced by the dynamic +// Select constructor, whose projection is assembled from a run-time field list. +// The typed Record1 through Record22 remain the preferred row types when the +// projection is known at compile time, because they preserve each column's Go +// type positionally; Record covers the projections they cannot express. +type Record struct { + columns []string + values []any +} + +// Size returns the number of columns in the record. +func (r Record) Size() int { return len(r.values) } + +// Columns returns the projected column names in projection order. Aliased +// fields report their alias and plain columns report their column name; a +// nameless expression (for example a Raw fragment that was not aliased) +// reports the empty string. For a SELECT * projection the names come from the +// result set. +func (r Record) Columns() []string { + out := make([]string, len(r.columns)) + copy(out, r.columns) + return out +} + +// Values returns the column values in projection order. +func (r Record) Values() []any { + out := make([]any, len(r.values)) + copy(out, r.values) + return out +} + +// Get returns the i-th column value (zero-based), or nil when out of range, +// mirroring jOOQ's Record.get(int) with the lenient out-of-range behavior of +// the typed RecordN.Get methods. +func (r Record) Get(i int) any { + if i < 0 || i >= len(r.values) { + return nil + } + return r.values[i] +} + +// GetByName returns the value of the named column, mirroring jOOQ's +// Record.get(String). The name is matched case-insensitively, consistent with +// the column matching of FetchInto; when several columns carry the name, the +// first one wins. It returns nil when no column carries the name, which is +// indistinguishable from a stored SQL NULL — callers that need to tell the two +// apart resolve the position first through Columns and use Get. +func (r Record) GetByName(name string) any { + for i, column := range r.columns { + if strings.EqualFold(column, name) { + return r.values[i] + } + } + return nil +} + +// Value returns the named column of the record converted to T, mirroring +// jOOQ's Record.get(String, Class). The name is matched case-insensitively. A +// SQL NULL yields the zero value of T. It returns an error when no column +// carries the name or when the stored value cannot be converted to T. +func Value[T any](r Record, name string) (T, error) { + for i, column := range r.columns { + if strings.EqualFold(column, name) { + return convertRecordValue[T](r.values[i], fmt.Sprintf("%q", column)) + } + } + var zero T + return zero, fmt.Errorf("jooq: column %q is not among the record's columns", name) +} + +// ValueAt returns the i-th column of the record (zero-based) converted to T, +// mirroring jOOQ's Record.get(int, Class). A SQL NULL yields the zero value of +// T. It returns an error when the index is out of range or when the stored +// value cannot be converted to T. +func ValueAt[T any](r Record, i int) (T, error) { + if i < 0 || i >= len(r.values) { + var zero T + return zero, fmt.Errorf("jooq: column index %d is out of range for a record of %d columns", i, len(r.values)) + } + return convertRecordValue[T](r.values[i], fmt.Sprintf("%d", i)) +} + +// convertRecordValue turns a stored record value into the requested type T: +// a nil value yields the zero value of T, a directly assignable value is +// returned as is, a []byte relaxes to string (covering drivers that report +// text columns as bytes), and numeric kinds convert between one another. +// Anything else is an error rather than a silent reflect conversion, because +// reflect would, for example, turn an integer into the string of its code +// point instead of its decimal representation. +func convertRecordValue[T any](value any, position string) (T, error) { + var zero T + if value == nil { + return zero, nil + } + if typed, ok := value.(T); ok { + return typed, nil + } + if bytes, ok := value.([]byte); ok { + if converted, ok := any(string(bytes)).(T); ok { + return converted, nil + } + } + targetType := reflect.TypeOf(zero) + if targetType != nil && isNumericKind(targetType.Kind()) { + source := reflect.ValueOf(value) + if isNumericKind(source.Kind()) && source.Type().ConvertibleTo(targetType) { + return source.Convert(targetType).Interface().(T), nil + } + } + return zero, fmt.Errorf("jooq: cannot use column %s value of type %T as %T", position, value, zero) +} + +// isNumericKind reports whether the reflect kind is an integer or floating +// point kind, the only kinds convertRecordValue converts between. +func isNumericKind(kind reflect.Kind) bool { + switch kind { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return true + default: + return false + } +} diff --git a/select_dynamic.go b/select_dynamic.go new file mode 100644 index 0000000..f5a1d0d --- /dev/null +++ b/select_dynamic.go @@ -0,0 +1,78 @@ +package gooq + +import "database/sql" + +// Select begins a SELECT whose projection is assembled at run time, producing +// dynamic Record rows. It is the counterpart of jOOQ's +// DSL.select(SelectFieldOrAsterisk...) for projections whose arity only becomes +// known at run time — for example a GROUP BY column set derived from a request. +// When the projection is fixed at compile time, the typed Select1 through +// Select22 constructors remain preferable because they preserve each column's +// Go type positionally. +// +// Every projected field that carries a Go element type — every Field[T], +// including aliased fields and the Raw and RawValue escape hatches — is scanned +// into a value of that type, so the Record values match the values the +// equivalent typed projection would produce, independent of the driver's native +// representations. A projected field without a typed backing is scanned into +// the driver's native representation. As in the typed constructors, a SQL NULL +// scanned into a non-nullable element type is an error; nullable columns are +// projected with a pointer or sql.Null* element type. +// +// Passing no fields mirrors the reference's empty-select convenience and +// renders SELECT *. The column names are then taken from the result set and +// every value carries the driver's native representation. +func Select(fields ...AnyField) SelectFromStep[Record] { + if len(fields) == 0 { + return newSelect([]node{&literalNode{sql: "*"}}, scanAllColumns) + } + + projection := make([]node, len(fields)) + columns := make([]string, len(fields)) + for i, f := range fields { + projection[i] = f + columns[i] = f.Name() + } + + scan := func(rows *sql.Rows) (Record, error) { + targets := make([]any, len(fields)) + for i, f := range fields { + if provider, ok := f.(scanTargetProvider); ok { + targets[i] = provider.scanTarget() + } else { + targets[i] = new(any) + } + } + if err := rows.Scan(targets...); err != nil { + return Record{}, err + } + values := make([]any, len(targets)) + for i, target := range targets { + values[i] = dereference(target) + } + return Record{columns: columns, values: values}, nil + } + return newSelect(projection, scan) +} + +// scanAllColumns maps a SELECT * row into a Record: the column names come from +// the result set and every value carries the driver's native representation, +// because an asterisk projection declares no typed fields to scan into. +func scanAllColumns(rows *sql.Rows) (Record, error) { + columns, err := rows.Columns() + if err != nil { + return Record{}, err + } + targets := make([]any, len(columns)) + for i := range targets { + targets[i] = new(any) + } + if err := rows.Scan(targets...); err != nil { + return Record{}, err + } + values := make([]any, len(targets)) + for i, target := range targets { + values[i] = *(target.(*any)) + } + return Record{columns: columns, values: values}, nil +} diff --git a/select_dynamic_test.go b/select_dynamic_test.go new file mode 100644 index 0000000..6eb3e0d --- /dev/null +++ b/select_dynamic_test.go @@ -0,0 +1,228 @@ +package gooq + +import ( + "context" + "database/sql/driver" + "errors" + "reflect" + "strings" + "testing" + "time" +) + +// TestSelectDynamicGolden confirms that the dynamic Select renders exactly the +// SQL its typed SelectN counterpart would render for the same projection, and +// that the run-time projection composes with the full clause chain. +func TestSelectDynamicGolden(t *testing.T) { + t.Run("plain columns render like the typed constructor", func(t *testing.T) { + checkSQL(t, + Select(Book.ID, Book.Title).From(Book), + `SELECT "book"."id", "book"."title" FROM "book"`, + nil, + ) + }) + + t.Run("empty projection renders SELECT *", func(t *testing.T) { + checkSQL(t, + Select().From(Book).Where(Book.Price.GT(10)), + `SELECT * FROM "book" WHERE "book"."price" > $1`, + []any{float64(10)}, + ) + }) + + t.Run("run-time arity composes with aliases and GROUP BY", func(t *testing.T) { + groupColumns := []Field[string]{ + Raw[string]("COALESCE(payload->>'kind', '')"), + Book.Title, + } + fields := []AnyField{CountStar().As("total")} + groupBy := make([]AnyField, 0, len(groupColumns)) + for i, column := range groupColumns { + fields = append(fields, column.As("dim_"+string(rune('0'+i)))) + groupBy = append(groupBy, column) + } + checkSQL(t, + Select(fields...).From(Book).GroupBy(groupBy...), + `SELECT COUNT(*) AS "total", COALESCE(payload->>'kind', '') AS "dim_0", `+ + `"book"."title" AS "dim_1" FROM "book" `+ + `GROUP BY COALESCE(payload->>'kind', ''), "book"."title"`, + nil, + ) + }) + + t.Run("distinct and order compose", func(t *testing.T) { + checkSQL(t, + Select(Book.Title).Distinct().From(Book).OrderBy(Book.Title.Asc()), + `SELECT DISTINCT "book"."title" FROM "book" ORDER BY "book"."title" ASC`, + nil, + ) + }) +} + +// TestSelectDynamicFetch confirms that dynamic Record rows carry the projected +// fields' Go element types, that names resolve through the projection aliases, +// and that the typed accessors convert stored values like the reference +// Record.get(..., Class) contract. +func TestSelectDynamicFetch(t *testing.T) { + resetFake() + db := openFakeDB() + defer db.Close() + + instant := time.Date(2024, 6, 1, 10, 0, 0, 0, time.UTC) + queueRows( + []string{"id", "title", "created_at"}, + []driver.Value{int64(7), "Go", instant}, + ) + + createdAt := NewField[time.Time](Book.TableImpl, "created_at") + records, err := Select(Book.ID, Book.Title.As("title"), createdAt). + From(Book). + Fetch(context.Background(), db) + if err != nil { + t.Fatalf("Fetch: %v", err) + } + if len(records) != 1 { + t.Fatalf("records = %d, want 1", len(records)) + } + record := records[0] + + if record.Size() != 3 { + t.Errorf("Size = %d, want 3", record.Size()) + } + if !reflect.DeepEqual(record.Columns(), []string{"id", "title", "created_at"}) { + t.Errorf("Columns = %v", record.Columns()) + } + if got, ok := record.Get(0).(int64); !ok || got != 7 { + t.Errorf("Get(0) = %#v, want int64(7)", record.Get(0)) + } + if got, ok := record.GetByName("TITLE").(string); !ok || got != "Go" { + t.Errorf(`GetByName("TITLE") = %#v, want "Go"`, record.GetByName("TITLE")) + } + if got, ok := record.Get(2).(time.Time); !ok || !got.Equal(instant) { + t.Errorf("Get(2) = %#v, want %v", record.Get(2), instant) + } + if record.Get(3) != nil || record.Get(-1) != nil { + t.Errorf("out-of-range Get should be nil") + } + if record.GetByName("missing") != nil { + t.Errorf(`GetByName("missing") should be nil`) + } + + title, err := Value[string](record, "title") + if err != nil || title != "Go" { + t.Errorf("Value[string] = %q, %v", title, err) + } + idAsInt, err := Value[int32](record, "id") + if err != nil || idAsInt != 7 { + t.Errorf("Value[int32] = %d, %v; want numeric conversion to 7", idAsInt, err) + } + when, err := ValueAt[time.Time](record, 2) + if err != nil || !when.Equal(instant) { + t.Errorf("ValueAt[time.Time] = %v, %v", when, err) + } + if _, err := Value[string](record, "missing"); err == nil { + t.Errorf("Value on an unknown column should error") + } + if _, err := ValueAt[string](record, 9); err == nil { + t.Errorf("ValueAt out of range should error") + } + if _, err := Value[time.Time](record, "title"); err == nil { + t.Errorf("Value with an inconvertible type should error") + } +} + +// TestSelectDynamicFetchAllColumns confirms the SELECT * path: column names +// come from the result set and values carry the driver's native representation, +// with []byte relaxing to string through the typed accessors. +func TestSelectDynamicFetchAllColumns(t *testing.T) { + resetFake() + db := openFakeDB() + defer db.Close() + + queueRows( + []string{"id", "title"}, + []driver.Value{int64(1), []byte("Go")}, + []driver.Value{int64(2), nil}, + ) + + records, err := Select().From(Book).Fetch(context.Background(), db) + if err != nil { + t.Fatalf("Fetch: %v", err) + } + if len(records) != 2 { + t.Fatalf("records = %d, want 2", len(records)) + } + + if !reflect.DeepEqual(records[0].Columns(), []string{"id", "title"}) { + t.Errorf("Columns = %v", records[0].Columns()) + } + title, err := Value[string](records[0], "title") + if err != nil || title != "Go" { + t.Errorf("Value[string] over []byte = %q, %v", title, err) + } + if records[1].GetByName("title") != nil { + t.Errorf("NULL column should be nil, got %#v", records[1].GetByName("title")) + } + nullTitle, err := Value[string](records[1], "title") + if err != nil || nullTitle != "" { + t.Errorf("Value[string] over NULL = %q, %v; want zero value", nullTitle, err) + } +} + +// TestSelectDynamicNullableProjection confirms that a pointer element type +// carries SQL NULL as a nil pointer, while a non-nullable element type +// surfaces the scan error exactly as the typed constructors do. +func TestSelectDynamicNullableProjection(t *testing.T) { + resetFake() + db := openFakeDB() + defer db.Close() + + nullableTitle := NewField[*string](Book.TableImpl, "title") + queueRows([]string{"title"}, []driver.Value{nil}) + records, err := Select(nullableTitle).From(Book).Fetch(context.Background(), db) + if err != nil { + t.Fatalf("Fetch: %v", err) + } + if got, ok := records[0].Get(0).(*string); !ok || got != nil { + t.Errorf("Get(0) = %#v, want nil *string", records[0].Get(0)) + } + + queueRows([]string{"title"}, []driver.Value{nil}) + _, err = Select(Book.Title).From(Book).Fetch(context.Background(), db) + if err == nil || !strings.Contains(err.Error(), "Scan") { + t.Errorf("scanning NULL into a non-nullable element should surface the scan error, got %v", err) + } +} + +// TestSelectDynamicSetOperations confirms that dynamic queries compose through +// set operations over the same Record row shape. +func TestSelectDynamicSetOperations(t *testing.T) { + checkSQL(t, + Select(Book.Title).From(Book).Union(Select(Author.Name.As("title")).From(Author)), + `SELECT "book"."title" FROM "book" UNION SELECT "author"."name" AS "title" FROM "author"`, + nil, + ) +} + +// TestSelectDynamicFetchOne confirms the single-row terminal operations reuse +// the dynamic scan path. +func TestSelectDynamicFetchOne(t *testing.T) { + resetFake() + db := openFakeDB() + defer db.Close() + + queueRows([]string{"total"}, []driver.Value{int64(3)}) + record, err := Select(CountStar().As("total")).From(Book).FetchOne(context.Background(), db) + if err != nil { + t.Fatalf("FetchOne: %v", err) + } + total, err := Value[int64](record, "total") + if err != nil || total != 3 { + t.Errorf("total = %d, %v; want 3", total, err) + } + + queueRows([]string{"total"}, []driver.Value{int64(1)}, []driver.Value{int64(2)}) + if _, err := Select(CountStar().As("total")).From(Book).FetchOne(context.Background(), db); !errors.Is(err, ErrTooManyRows) { + t.Errorf("FetchOne over two rows = %v, want ErrTooManyRows", err) + } +} From 447fe6d4e750fc2069488b159895cbcb6b179628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Garc=C3=ADa?= Date: Mon, 6 Jul 2026 11:01:11 +0000 Subject: [PATCH 2/2] refactor: rename library self-references from jooq to gooq Error message prefixes, the fake test driver name, and every comment that refers to this library itself now say gooq instead of the leftover jooq working name. References to the jOOQ Java library that inspired the design, including its org.jooq package paths and website, are intentionally preserved. --- cmd/gooq-gen/main.go | 4 ++-- codegen/emit.go | 4 ++-- codegen/emit_test.go | 2 +- codegen/introspect.go | 2 +- codegen/typemap.go | 6 +++--- doc.go | 2 +- errors.go | 8 ++++---- example/main.go | 2 +- fakedb_test.go | 6 +++--- fetch_into.go | 6 +++--- fetch_into_test.go | 4 ++-- helpers_test.go | 2 +- integration/generate.go | 4 ++-- integration/go.mod | 2 +- integration/harness_test.go | 2 +- integration/internal/gendb/main.go | 2 +- integration/postgres_test.go | 2 +- internal/gen/main.go | 4 ++-- record_dynamic.go | 6 +++--- 19 files changed, 35 insertions(+), 35 deletions(-) diff --git a/cmd/gooq-gen/main.go b/cmd/gooq-gen/main.go index 77beb3b..46beeab 100644 --- a/cmd/gooq-gen/main.go +++ b/cmd/gooq-gen/main.go @@ -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 ".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 diff --git a/codegen/emit.go b/codegen/emit.go index 8a9e5e9..a26fbc1 100644 --- a/codegen/emit.go +++ b/codegen/emit.go @@ -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" @@ -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. diff --git a/codegen/emit_test.go b/codegen/emit_test.go index 21496c7..baa8610 100644 --- a/codegen/emit_test.go +++ b/codegen/emit_test.go @@ -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{ diff --git a/codegen/introspect.go b/codegen/introspect.go index e664f03..9a3a325 100644 --- a/codegen/introspect.go +++ b/codegen/introspect.go @@ -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 diff --git a/codegen/typemap.go b/codegen/typemap.go index bcb252b..aa3f524 100644 --- a/codegen/typemap.go +++ b/codegen/typemap.go @@ -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 { @@ -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: @@ -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 diff --git a/doc.go b/doc.go index babdea9..de586e3 100644 --- a/doc.go +++ b/doc.go @@ -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. diff --git a/errors.go b/errors.go index 48e4925..6dc528c 100644 --- a/errors.go +++ b/errors.go @@ -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") diff --git a/example/main.go b/example/main.go index 6711977..11d74ce 100644 --- a/example/main.go +++ b/example/main.go @@ -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. diff --git a/fakedb_test.go b/fakedb_test.go index 6a1ac31..b6f6b09 100644 --- a/fakedb_test.go +++ b/fakedb_test.go @@ -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) } @@ -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 { diff --git a/fetch_into.go b/fetch_into.go index 146249f..27d9e1e 100644 --- a/fetch_into.go +++ b/fetch_into.go @@ -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() @@ -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. @@ -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 diff --git a/fetch_into_test.go b/fetch_into_test.go index 2558160..ac6f441 100644 --- a/fetch_into_test.go +++ b/fetch_into_test.go @@ -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) { @@ -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 } diff --git a/helpers_test.go b/helpers_test.go index f212e2b..f3223bf 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -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. diff --git a/integration/generate.go b/integration/generate.go index 19f087c..ca3f71e 100644 --- a/integration/generate.go +++ b/integration/generate.go @@ -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 diff --git a/integration/go.mod b/integration/go.mod index 7af6660..0a0ca86 100644 --- a/integration/go.mod +++ b/integration/go.mod @@ -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 diff --git a/integration/harness_test.go b/integration/harness_test.go index 532af35..1263cc8 100644 --- a/integration/harness_test.go +++ b/integration/harness_test.go @@ -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 diff --git a/integration/internal/gendb/main.go b/integration/internal/gendb/main.go index 1093307..efb7cbc 100644 --- a/integration/internal/gendb/main.go +++ b/integration/internal/gendb/main.go @@ -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: diff --git a/integration/postgres_test.go b/integration/postgres_test.go index edf783b..7a1974a 100644 --- a/integration/postgres_test.go +++ b/integration/postgres_test.go @@ -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. diff --git a/internal/gen/main.go b/internal/gen/main.go index b8746f5..3176925 100644 --- a/internal/gen/main.go +++ b/internal/gen/main.go @@ -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 diff --git a/record_dynamic.go b/record_dynamic.go index ff5dde8..8e7380a 100644 --- a/record_dynamic.go +++ b/record_dynamic.go @@ -74,7 +74,7 @@ func Value[T any](r Record, name string) (T, error) { } } var zero T - return zero, fmt.Errorf("jooq: column %q is not among the record's columns", name) + return zero, fmt.Errorf("gooq: column %q is not among the record's columns", name) } // ValueAt returns the i-th column of the record (zero-based) converted to T, @@ -84,7 +84,7 @@ func Value[T any](r Record, name string) (T, error) { func ValueAt[T any](r Record, i int) (T, error) { if i < 0 || i >= len(r.values) { var zero T - return zero, fmt.Errorf("jooq: column index %d is out of range for a record of %d columns", i, len(r.values)) + return zero, fmt.Errorf("gooq: column index %d is out of range for a record of %d columns", i, len(r.values)) } return convertRecordValue[T](r.values[i], fmt.Sprintf("%d", i)) } @@ -116,7 +116,7 @@ func convertRecordValue[T any](value any, position string) (T, error) { return source.Convert(targetType).Interface().(T), nil } } - return zero, fmt.Errorf("jooq: cannot use column %s value of type %T as %T", position, value, zero) + return zero, fmt.Errorf("gooq: cannot use column %s value of type %T as %T", position, value, zero) } // isNumericKind reports whether the reflect kind is an integer or floating