diff --git a/.agents/architecture.md b/.agents/architecture.md index feb1e1aa..3b3fb1f0 100644 --- a/.agents/architecture.md +++ b/.agents/architecture.md @@ -28,6 +28,8 @@ The language packages in `internal/codegen` produce files from that resolved mod ## Compilation boundary +Static absolute TablePathPrefix resolution belongs to the shared analyzer. A named query or schema source has one validated prefix; different named queries and schema files do not inherit it. The catalog stores resolved physical paths while relation aliases retain their authored SQL identity. Resolved table-reference token bindings travel with executable syntax for generators such as jOOQ, so table mapping does not reimplement path semantics. Source pragmas and authored table references remain in executable YQL; wildcard reanalysis refreshes the token bindings. The compiler rejects conflicting or late prefixes instead of modeling statement-local prefix changes. + `analyzer.Analyze(schema, queries)` is the default compilation entry point; `AnalyzeWithOptions(schema, queries, Options)` accepts an explicit function contract with a fixed arity. The CLI invokes `AnalyzeWithOptions` for offline analysis or `AnalyzeWithDatabase` for connected analysis once per `sql` configuration entry, then gives the shared resolved result to every selected generator. `compile` stops after analysis. Planned macro processing belongs inside this boundary; see [the roadmap](roadmap.md). A separate compiler package is unnecessary while the analyzer owns these stages. `model.Type` owns structural equality and YQL type formatting. The analyzer, built-in function resolver and Python model reuse checks share those operations. Go's native and database/sql generators bind root Struct parameters and list-of-Struct batches through generated named types; both bind scalar List parameters, while nested Struct/List fields remain unsupported. SDK-specific type mapping stays in each generator. diff --git a/.agents/development.md b/.agents/development.md index 7f12500d..520d6acc 100644 --- a/.agents/development.md +++ b/.agents/development.md @@ -39,7 +39,7 @@ go tool cover -html=coverage.out -o coverage.html `make coverage` runs the root Go module's tests without caching and writes `coverage.out` in atomic mode. `-coverpkg=./...` includes calls across package boundaries, so CLI and golden tests contribute to analyzer and generator coverage. The final `total` from `go tool cover` is the combined statement coverage; the per-test-package percentages are not independent package coverage figures. The denominator contains only sqlc-ydb runtime code, including the CLI entry point even when untested. Unit and end-to-end tests run and contribute coverage of that code, but their own source is not measured. Examples, the `internal/endtoend` harness, `*_test.go` files, golden outputs, `.github` tooling and the external ANTLR parser dependency are outside this scope. Go already excludes test source and the separate examples module from the profile; `codecov.yml` also explicitly excludes the repository's non-runtime paths. This local profile excludes live YDB tests unless their environment variables are set, and excludes optional SDK checks unless explicitly enabled. -CI saves the offline profile together with `coverage-sdk.out` and `coverage-typescript.out` from compiler/SDK-backed generator tests as the `generator-coverage` artifact and uploads them to Codecov with the `unit` flag on pushes to `main` and pull requests. The `ydb-acceptance` job also instruments the semantic, integer pagination, callback, Go generator and Python generator live tests and uploads their five profiles with the `integration` flag, saving them as the `generator-integration-coverage` artifact. Codecov merges these profiles; generated application runtime coverage is not measured. SDK checks contribute coverage of the generator code they execute. The PR comment updates as reports arrive, so the first report can show only offline coverage. [codecov.yml](../codecov.yml) compares project coverage with the base commit (allowing a one percentage point drop) and requires 80% patch coverage. It enables one updated PR comment with the coverage difference and impacted files, including on the first PR without a base report. A successful `main` upload establishes the comparison baseline and populates the README badge. +CI saves the offline profile together with `coverage-sdk.out` and `coverage-typescript.out` from compiler/SDK-backed generator tests as the `generator-coverage` artifact and uploads them to Codecov with the `unit` flag on pushes to `main` and pull requests. The `ydb-acceptance` job also instruments the semantic, integer pagination, table-path-prefix, callback, Go generator and Python generator live tests and uploads their six profiles with the `integration` flag, saving them as the `generator-integration-coverage` artifact. Codecov merges these profiles; generated application runtime coverage is not measured. SDK checks contribute coverage of the generator code they execute. The PR comment updates as reports arrive, so the first report can show only offline coverage. [codecov.yml](../codecov.yml) compares project coverage with the base commit (allowing a one percentage point drop) and requires 80% patch coverage. It enables one updated PR comment with the coverage difference and impacted files, including on the first PR without a base report. A successful `main` upload establishes the comparison baseline and populates the README badge. Repository administrators must enable `ydb-platform/sqlc-ydb` in Codecov and grant the [Codecov GitHub App](https://github.com/apps/codecov) access so it can post PR comments. Set the repository Actions secret `CODECOV_TOKEN` to the Codecov upload token, as in ydb-go-sdk. Alternatively, the organization can allow tokenless public uploads with **Global Upload Token → Not required** in Codecov; the action accepts an empty secret in that mode. Public fork PR uploads do not need access to the secret. See [Codecov token authentication](https://docs.codecov.com/docs/codecov-tokens) and [PR comments](https://docs.codecov.com/docs/pull-request-comments). An upload failure fails the CI job rather than silently leaving stale coverage. @@ -81,7 +81,7 @@ Review updated files alongside the SQL, configuration and implementation changes ## Generated runtime checks -The [examples](../examples/README.md) share one Go module in `examples/`; generated code lives under each example's `go/` directory. Cross-example Go tests and database helpers live in `tests/examples/go`, with a scoped `go.work` connecting the example module without local replace directives. C# framework, TypeScript, Rust and PHP examples share dependencies and test harnesses in `tests/examples/csharp`, `tests/examples/typescript`, `tests/examples/rust` and `tests/examples/php`. TypeScript dependencies resolve from `tests/examples/typescript/package.json`; the shared jOOQ build lives in `tests/examples/java/jooq`. The authors example hosts the Python, Java native/JDBC, Kotlin and ADO.NET builds; its C++ CMake project compiles all five example families. Schema, queries and generator configuration are shared in each example root. The additional `records` and `counters` recipes generate both Go adapters and run in the same sequential Go acceptance step. The counters live tests also invoke `bin/sqlc-ydb` against the opt-in schema-check/discovery configurations, compile and execute the discovered client in a temporary directory, and verify schema drift and the offline override; run `make generate` first to build the CLI. `make generate` and `make check-examples` cover every offline `examples/*/sqlc.yaml` configuration; the release smoke test also compiles, generates and diffs all of them. `make check-examples` fails if generation changes tracked files or creates untracked files under `examples`, so a new generated source cannot be omitted from a commit unnoticed. +The [examples](../examples/README.md) share one Go module in `examples/`; generated code lives under each example's `go/` directory. Cross-example Go tests and database helpers live in `tests/examples/go`, with a scoped `go.work` connecting the example module without local replace directives. C# framework, TypeScript, Rust and PHP examples share dependencies and test harnesses in `tests/examples/csharp`, `tests/examples/typescript`, `tests/examples/rust` and `tests/examples/php`. TypeScript dependencies resolve from `tests/examples/typescript/package.json`; the shared jOOQ build lives in `tests/examples/java/jooq`. The authors example hosts the Python, Java native/JDBC, Kotlin and ADO.NET builds; its C++ CMake project compiles the five upstream-derived families and namespaces. The shared Java/Kotlin and .NET projects also compile the namespaces profiles; TypeScript, Rust and PHP include their namespaces modules in the existing build/load checks. Schema, queries and generator configuration are shared in each example root. The additional `records` and `counters` recipes generate both Go adapters and run in the same sequential Go acceptance step. The counters live tests also invoke `bin/sqlc-ydb` against the opt-in schema-check/discovery configurations, compile and execute the discovered client in a temporary directory, and verify schema drift and the offline override; run `make generate` first to build the CLI. `make generate` and `make check-examples` cover every offline `examples/*/sqlc.yaml` configuration; the release smoke test also compiles, generates and diffs all of them. `make check-examples` fails if generation changes tracked files or creates untracked files under `examples`, so a new generated source cannot be omitted from a commit unnoticed. Check all generated example families against their pinned runtime dependencies: @@ -113,6 +113,8 @@ YDB_CONNECTION_STRING=grpc://localhost:2136/local \ `TestLiveYDBIntegerLimits` generates both Go adapters and verifies integer aliases, every supported required/optional pagination width, signed and NULL argument behavior, comma-form LIMIT ordering, and boundary values. It separately checks that YDB rejects unsupported count types and overflowing literals. Run `go test -p 1 -count=1 -timeout=240s ./internal/endtoend -run '^TestLiveYDBIntegerLimits$' -v` with the same disposable DSN. Stable and nightly acceptance run this suite in a separate sequential step with its own 240-second Go test budget and five-minute step timeout; stable CI includes `coverage-integer-limits-live.out` in the integration coverage artifact and Codecov upload. `TestAuthorsPagination` additionally executes the checked-in authors pagination example through both Go adapters. +`TestLiveYDBTablePathPrefix` checks uniquely owned namespaced tables, offline/discovered/local-schema agreement, drift, indexed reads, writes and absolute-path bypass through generated Go native and database/sql clients. Set `SQLC_YDB_TEST_MAVEN=mvn` to include jOOQ runtime checks with the pinned SDK dependencies. Run `go test -p 1 -count=1 -timeout=240s ./internal/endtoend -run '^TestLiveYDBTablePathPrefix$' -v` with the disposable DSN. Stable and nightly CI give this suite its own sequential 240-second Go test budget and five-minute step timeout; stable CI enables the Maven checks and uploads `coverage-table-path-prefix-live.out` as its sixth integration coverage profile. The checked-in namespaces example additionally supplies all generated runtime profiles. + For a manually prepared development schema, see the [local-ydb initialization recipe](../docs/database-analysis.md#prepare-a-disposable-local-database). Automated acceptance uses uniquely named objects and cleans them up instead of sharing a fixed application schema. The semantic metadata suite compares analyzer result types, nullability and column order directly with YDB, independently of generated code. Run it sequentially with the runtime suites after installing the pinned Python dependencies: diff --git a/.agents/roadmap.md b/.agents/roadmap.md index d2a944e2..3ecf2df1 100644 --- a/.agents/roadmap.md +++ b/.agents/roadmap.md @@ -4,11 +4,10 @@ Current behavior is in [compatibility](../docs/compatibility.md), stage ownershi ## Query compatibility priorities -The following issues track planned work; their examples and acceptance criteria define the scope. Name-mapped INSERT/UPSERT SELECT, trailing Struct commas, verified integer aliases and compatible LIMIT/OFFSET types, and secondary-index metadata with VIEW selection are implemented; their original scope is in [#25](https://github.com/ydb-platform/sqlc-ydb/issues/25), [#26](https://github.com/ydb-platform/sqlc-ydb/issues/26), [#27](https://github.com/ydb-platform/sqlc-ydb/issues/27), and [#28](https://github.com/ydb-platform/sqlc-ydb/issues/28). The [compatibility contract](../docs/compatibility.md#current-analyzer-coverage) records their boundaries. Subsequent changes should remain separate, reviewable PRs. +The following issues track planned work; their examples and acceptance criteria define the scope. Name-mapped INSERT/UPSERT SELECT, trailing Struct commas, verified integer aliases and compatible LIMIT/OFFSET types, secondary-index metadata with VIEW selection, and static absolute TablePathPrefix resolution are implemented; their original scope is in [#25](https://github.com/ydb-platform/sqlc-ydb/issues/25), [#26](https://github.com/ydb-platform/sqlc-ydb/issues/26), [#27](https://github.com/ydb-platform/sqlc-ydb/issues/27), [#28](https://github.com/ydb-platform/sqlc-ydb/issues/28), and [#29](https://github.com/ydb-platform/sqlc-ydb/issues/29). The [compatibility contract](../docs/compatibility.md#current-analyzer-coverage) records their boundaries. Subsequent changes should remain separate, reviewable PRs. | Order | Planned capability | Tracking | | --- | --- | --- | -| 2 | TablePathPrefix and consistent table resolution | [#29](https://github.com/ydb-platform/sqlc-ydb/issues/29) | | 2 | ALTER COLUMN DROP NOT NULL | [#30](https://github.com/ydb-platform/sqlc-ydb/issues/30) | | 3 | Shared Boolean expressions, conditional aggregates and scalar conversions | [#31](https://github.com/ydb-platform/sqlc-ydb/issues/31) | | 4 | Scoped tabular expressions, IN subqueries and collection aggregation | [#32](https://github.com/ydb-platform/sqlc-ydb/issues/32) | diff --git a/.agents/sdk-evidence.md b/.agents/sdk-evidence.md index bbe6d4bd..e35c8323 100644 --- a/.agents/sdk-evidence.md +++ b/.agents/sdk-evidence.md @@ -4,6 +4,12 @@ This maintainer reference records API snapshots and non-obvious SDK behavior use Language-level generated API, value mappings and ownership contracts remain in the public guides under `docs/`. C++ build details are in [C++ development](cpp-development.md), and additional C# source evidence is in [C# SDK evidence](csharp-sdk-evidence.md). +## TablePathPrefix execution + +On 2026-09-23, the Java generator's pinned SDK tests and `TestLiveYDBTablePathPrefix` validated jOOQ 3.21.0, YDB dialect 2.0.0 and JDBC 2.4.1 with static absolute prefixes. Non-declared queries retain the typed DSL statement as a QueryPart in `dsl.query`/`dsl.resultQuery`, preceded by the exact pragma source; result queries use typed `Field` coercions. This preserves typed binds and the same prefix context as the existing declared JDBC path, including relative RenderMapping outputs. Absolute mapped paths bypass the pragma. Dropping the source pragma would change the root used by relative table mappings. The static wrapper keeps both execution paths consistent without generated runtime path heuristics. + +The live suite used `ydbplatform/local-ydb:26.3.1.16` (`sha256:32687d3bc4b7a3e4200e2142800e5fc2e91d48ba46160ea094e9f1ce56794c12`) and also exercised generated native Go/database/sql clients using SDK v3.151.1. It verified actual returned values and writes, including namespace isolation, VIEW, cross-namespace SELECT-backed writes and typed Uint64 boundaries. Compiler/mock checks remain distinct from these server execution checks. + ## Go error stack traces Checked against the pinned Go SDK 3.151.1 on 2026-09-13: [`pkg/xerrors.WithStackTrace`](https://github.com/ydb-platform/ydb-go-sdk/blob/v3.151.1/pkg/xerrors/stacktrace.go) delegates to the [internal wrapper](https://github.com/ydb-platform/ydb-go-sdk/blob/v3.151.1/internal/xerrors/stacktrace.go), which returns `nil` for `nil`, records the caller location and exposes the original error through `Unwrap`. Generated native methods and Decimal validation helpers wrap returned errors; `database/sql` retains its error behavior. `TestGeneratedYDBErrorStackTraces` compiles separate `:one`, `:many` and `:exec` files against this SDK and executes failure paths to verify generated locations, `errors.Is`/`errors.As`, Decimal validation and successful `Exec` returning `nil`. diff --git a/.agents/yql-evidence.md b/.agents/yql-evidence.md index 82d078a1..96339315 100644 --- a/.agents/yql-evidence.md +++ b/.agents/yql-evidence.md @@ -48,6 +48,18 @@ On the same image, `TestLiveYDBSemanticTypes` compared arithmetic results with s Review follow-up on 2026-09-22 confirmed required/optional Uint32-to-Int64 assignments in INSERT/UPSERT SELECT, Uint32-to-Uint64 key widening in UPDATE/DELETE ON SELECT, and Boolean/nullable Boolean assignments from comparisons. YDB rejected duplicate SET targets with `Duplicated member: value`; offline analysis now diagnoses the duplicate before generation. The generated computed-DML fixture also executes comparison assignments and SELECT-backed widening through both Go adapters and Python. +## TablePathPrefix + +The [TablePathPrefix reference](https://ydb.tech/docs/en/yql/reference/syntax/pragma#tablepathprefix) describes path concatenation and absolute-table-path bypass. Sequential probes on 2026-09-23 against `ydbplatform/local-ydb:26.3.1.16` (`sha256:32687d3bc4b7a3e4200e2142800e5fc2e91d48ba46160ea094e9f1ce56794c12`) observed two version-specific behaviors: an explicit prefix is rooted at the server root, not the connection's database, and the last prefix affects the whole request, including earlier SELECT/CREATE statements. On that image, ordinary table references without a pragma retain their database-relative behavior, while an empty or relative prefix does not mean the connection's database root. The compiler contract accepts one nonempty absolute prefix per named query/schema source and diagnoses conflicting or late declarations. + +Source review on 2026-09-23 covered open, unmerged YDB PRs [#53874](https://github.com/ydb-platform/ydb/pull/53874) at `51336f136ced691df04bbf5389d998d4e37742d8` and [#53875](https://github.com/ydb-platform/ydb/pull/53875) at `4c54358a95a636ffeb26ce6da1ade538814b6d41`. The former resolves relative prefix values against the configured database path for the local Kikimr service when that database path is absolute; it preserves absolute prefix values. The latter captures table-reference prefix context during parsing so later pragmas do not retroactively change earlier references. These are source-analysis findings, not execution results from a server containing those changes. The supported leading absolute prefix and jOOQ relative table mappings retain the same meaning under both models. Relative prefix values and statement-local prefix changes remain outside sqlc-ydb's current coverage. + +The 26.3.1.16 probes also accepted dot/parent/slash normalization, absolute table-path bypass, prefixed CREATE/ALTER/RENAME/DROP, ordinary quoted string literals and hexadecimal, Unicode and octal slash escapes. Doubled SQL quotes and a Utf8 suffix were rejected for pragma literals. A declared SQL alias hides the original table qualifier; source `users AS u` accepts `u.id` but rejects `users.id`, `users.*` and the resolved physical path as qualifiers. The analyzer retains authored relation aliases independently of resolved catalog identity. + +The review-followup probe on the same image confirmed that aliases are case-sensitive. With fixture rows `(id, link) = (1, 0), (2, 1)`, a self-join with aliases `r` and `R` and condition `r.id = R.link` returned `(r.id, R.id) = (1, 2)`. A projection of `r.ID` was rejected with `Member not found: ID`. `TestColumnBindingsPreserveAliasCase` verifies distinct resolved table, alias and type bindings in SELECT projections, JOIN conditions and WHERE predicates, with and without a prefix. + +`TestLiveYDBTablePathPrefix` passed on that image using generated native Go/database/sql clients with SDK v3.151.1 and jOOQ 3.21.0, dialect 2.0.0 and JDBC 2.4.1. It checked two namespaces, matching offline/discovered/local-schema metadata, per-namespace drift, writes, VIEW reads, absolute-path bypass and cross-namespace UPSERT SELECT. jOOQ execution covered inferred and declared parameters, relative/absolute RenderMapping outputs, maximum Uint64, DELETE RETURNING and a declared self-select UPSERT. The tests created and removed only their own tables; they did not restart the shared local-ydb service. CI separately runs the public suite on its pinned stable image and nightly. + ## Integer aliases and pagination On 2026-09-23, `TestLiveYDBIntegerLimits` passed sequentially through generated native Go and database/sql clients using Go SDK v3.151.1 against `ydbplatform/local-ydb:26.3.1.8` (`sha256:d0c402700a78cbb8a45b35ffd4be69f5fcb241361ffe3fa5c8846d56b5872294`). DECLARE bindings and actual table-column metadata confirmed `TinyInt` → `Int8`, `SmallInt` → `Int16`, `Int`/`Integer` → `Int32`, and `BigInt` → `Int64`. Alias bindings preserved signed boundary values. The analyzer canonicalizes these names; generated SQL retains their original spelling. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 05cfdcbb..c74f7d24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -202,6 +202,11 @@ jobs: - name: Verify integer aliases and pagination with YDB timeout-minutes: 5 run: go test -p 1 -count=1 -timeout=240s -covermode=atomic -coverpkg=./... -coverprofile=coverage-integer-limits-live.out ./internal/endtoend -run '^TestLiveYDBIntegerLimits$' -v + - name: Verify TablePathPrefix namespaces and runtime bindings with YDB + timeout-minutes: 5 + env: + SQLC_YDB_TEST_MAVEN: mvn + run: go test -p 1 -count=1 -timeout=240s -covermode=atomic -coverpkg=./... -coverprofile=coverage-table-path-prefix-live.out ./internal/endtoend -run '^TestLiveYDBTablePathPrefix$' -v - name: Verify streaming callbacks with YDB timeout-minutes: 5 run: go test -p 1 -count=1 -timeout=240s -covermode=atomic -coverpkg=./... -coverprofile=coverage-each-live.out ./internal/endtoend -run '^TestLiveYDBEach$' -v @@ -254,6 +259,7 @@ jobs: path: | coverage-semantic.out coverage-integer-limits-live.out + coverage-table-path-prefix-live.out coverage-go-live.out coverage-python-live.out coverage-each-live.out @@ -262,7 +268,7 @@ jobs: uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage-semantic.out,./coverage-integer-limits-live.out,./coverage-go-live.out,./coverage-python-live.out,./coverage-each-live.out + files: ./coverage-semantic.out,./coverage-integer-limits-live.out,./coverage-table-path-prefix-live.out,./coverage-go-live.out,./coverage-python-live.out,./coverage-each-live.out flags: integration disable_search: true fail_ci_if_error: true @@ -307,6 +313,9 @@ jobs: - name: Verify integer aliases and pagination with nightly YDB timeout-minutes: 5 run: go test -p 1 -count=1 -timeout=240s ./internal/endtoend -run '^TestLiveYDBIntegerLimits$' -v + - name: Verify TablePathPrefix namespaces with nightly YDB + timeout-minutes: 5 + run: go test -p 1 -count=1 -timeout=240s ./internal/endtoend -run '^TestLiveYDBTablePathPrefix$' -v - name: Verify streaming callbacks with nightly YDB timeout-minutes: 5 run: go test -p 1 -count=1 -timeout=240s ./internal/endtoend -run '^TestLiveYDBEach$' -v diff --git a/CHANGELOG.md b/CHANGELOG.md index 8846f2c9..ef2b6fa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ ## Unreleased +### Added + +- Support static absolute TablePathPrefix pragmas in named queries and schema sources, with consistent offline and connected table resolution, preserved SQL execution context and namespaced models. Add a namespaces example for every language/runtime profile. + +### Changed + +- An explicit table alias is now the only qualifier accepted for that source's columns and wildcards, including queries without TablePathPrefix. Queries such as `SELECT records.id FROM records AS r`, accepted by v0.2.4, must use `r.id` to match YDB's name resolution. + +### Fixed + +- Preserve case-sensitive table aliases in resolved column bindings, so a JOIN using distinct aliases such as `r` and `R` cannot bind one source's column to the other source. + ## v0.2.4 ### Added diff --git a/Makefile b/Makefile index 0c3d9b78..0db6fc33 100644 --- a/Makefile +++ b/Makefile @@ -50,7 +50,7 @@ check-examples: generate fi cd examples && go test -p 1 ./... cd tests/examples/go && go test -p 1 ./... - python3 -m compileall -q examples/authors/python + python3 -m compileall -q $(wildcard examples/*/python) clean: rm -f bin/sqlc-ydb diff --git a/docs/compatibility.md b/docs/compatibility.md index 2750f764..14d9c055 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -106,6 +106,27 @@ The [computed DML fixture](../internal/endtoend/testdata/computed_dml/queries.sq INSERT/UPSERT SELECT supports two mapping contracts. With an explicit target column list, explicit source expressions match the targets positionally; source aliases are optional and do not retarget values by name, and wildcard source projections remain unsupported. Without a target column list, source result names select the destination columns; supported expressions need aliases, while column projections keep their names. This named form accepts `*`, `alias.*` and computed columns combined with wildcards. Duplicate names, unknown destination columns and incompatible types fail analysis. Both forms reject missing required NOT NULL columns and missing non-generated primary-key columns before generation. Omitted generated serial keys use the sequence; other primary-key columns must be supplied. UPSERT preserves omitted nullable columns on an existing row, but all NOT NULL columns without generated defaults must still be supplied, even for an empty source or when the row already exists. UPDATE/DELETE ON SELECT instead match source result names to target columns and require every primary-key column with a compatible declared type. Shared query-file declarations must currently be moved into each named query. These are temporary coverage limits, separate from the permanent decision to exclude plugins. Comments and whitespace may precede the first query annotation; comment-only query files are ignored. +## Table path resolution + +Named queries and schema sources accept `PRAGMA TablePathPrefix("/database/folder");` with a nonempty absolute string literal. Relative table references then resolve under that prefix; an absolute table reference bypasses it. Include the database name in the prefix. Relative and empty prefix values are outside the compiler's current coverage and are rejected with an explicit-path hint. Parameterized or computed prefix values and other pragmas remain unsupported. + +```sql +-- name: GetStagingUser :one +PRAGMA TablePathPrefix("/database/staging"); +DECLARE $id AS Uint64; +SELECT u.id, u.name +FROM users AS u +WHERE u.id = $id; +``` + +The offline catalog must describe the same resolved table path: use this prefix in its schema source or declare the table as `/database/staging/users`. Each named query and each schema source has its own prefix scope. Put the pragma before local assignments such as `$key = $id` and before data or schema statements, and repeat it in every source that needs it. External parameter declarations such as `DECLARE $id AS Uint64` may precede or follow the pragma. Identical repeated prefixes are accepted; prefix changes within a named query or schema source are not supported by the compiler. Query-file preambles before `-- name:` remain unsupported. + +Path resolution is shared by SELECT sources, joins, secondary-index VIEW selection, supported writes and schema migrations. Tables with the same basename in different directories remain distinct catalog entries. Aliases, column names, strings and comments retain their original meaning. Executable SQL retains the pragma and authored table references; wildcard expansion continues to replace only the projection spans. The jOOQ target renders its resolved table identities through the dialect, preserving the pragma's execution context and table mappings. + +An explicit source alias hides the table's original qualifier: `FROM records AS r` permits `r.id` and `r.*`, while `records.id` and `records.*` are rejected. This also applies without TablePathPrefix and corrects queries accepted by v0.2.4 that YDB rejects; replace the original qualifier with the alias when upgrading. Without an explicit alias, use the authored table name, including its directory components when present. Qualifiers are case-sensitive: `r` and `R` refer to distinct aliases. + +The prefix is static SQL, not a generated method argument or a runtime environment-variable substitution. To select a different environment, prepare matching schema/query inputs with the intended absolute prefix and regenerate. The [namespaces example](../examples/namespaces) demonstrates two catalogs, indexed reads, writes and a join that bypasses the prefix with an absolute table path. Connected analysis uses the same resolved identities; see [database-assisted analysis](database-analysis.md). + ## Schema migration coverage Ordinary secondary indexes support `INDEX name GLOBAL [SYNC|ASYNC] ON (key_columns) [COVER (data_columns)]` in CREATE TABLE and `ALTER TABLE ... ADD INDEX`; omitting SYNC/ASYNC selects SYNC. `DROP INDEX` removes the named index from the catalog. `FROM table VIEW index` resolves the index against the underlying table, including quoted names, aliases, joins, wildcard projections and supported SELECT-backed DML. Column types and wildcard order come from the base table. Generated SQL preserves explicit index selection; an unknown index is an error. Vector, unique, local and other specialized index types and index settings are currently unsupported. diff --git a/docs/database-analysis.md b/docs/database-analysis.md index 885cfd47..f7bdcf7d 100644 --- a/docs/database-analysis.md +++ b/docs/database-analysis.md @@ -23,6 +23,8 @@ Set `YDB_CONNECTION_STRING` to a URI such as `grpc://localhost:2136/local`, then DescribeTable also supplies ordinary global synchronous/asynchronous indexes, their ordered key columns and covering columns. `FROM table VIEW index` discovers the base table and validates the selected index against that metadata. Unsupported index kinds fail discovery with an actionable error, even when the query does not select that index; they are never treated as ordinary indexes or omitted silently. +With a supported absolute `PRAGMA TablePathPrefix`, discovery resolves each relative table reference under that prefix before calling DescribeTable. Explicit absolute table paths bypass the prefix. Offline catalog lookup, live discovery and schema-drift checks use the same resolved identities, including tables with identical basenames in different directories. Local schema sources must resolve to those same paths. EXPLAIN and generated executable SQL retain the source pragma; the compiler does not infer a prefix from `database.uri` or substitute a runtime value. See [table path resolution](compatibility.md#table-path-resolution) for accepted forms and source scopes. + The existing semantic analyzer resolves parameters and query projections against this catalog. Database discovery does not add support for expressions, functions, CTEs or other syntax outside the [current analyzer coverage](compatibility.md#current-analyzer-coverage). In particular, connecting to YDB does not make a computed projection supported automatically. ## Check a local schema against YDB diff --git a/docs/java.md b/docs/java.md index a3726986..b3332514 100644 --- a/docs/java.md +++ b/docs/java.md @@ -64,6 +64,8 @@ Each output contains `Tables.java` (typed fields derived from the local schema), Secondary-index sources retain `VIEW index` in both DSL and explicitly declared SQL. The base table remains a mapped table reference, and aliases follow the index selection. The [authors example](../examples/authors/README.md) includes ordinary and covering index reads through both paths. +For a supported static TablePathPrefix, table constants contain the analyzer's resolved absolute paths. Full path components distinguish generated Java table/model identifiers; paths that normalize to the same Java symbol produce a collision diagnostic. RenderMapping matches these full physical table names. Both the typed DSL and declared SQL paths retain the pragma's execution context: absolute mapped outputs bypass the prefix, while relative mapped outputs resolve under it. The [namespaces example](../examples/namespaces) demonstrates duplicate table basenames, indexed reads and a cross-catalog join. Parameter and column naming rules are unchanged. + For jOOQ callback transactions, wrap the callback's transaction-local configuration with `YDB.using(configuration)` and pass that `YdbDSLContext` to `Queries`. A cast from `configuration.dsl()` is invalid because jOOQ returns a plain `DefaultDSLContext` there. The shared [Maven project](../tests/examples/java/jooq/pom.xml) records the Java, jOOQ, YDB dialect and JDBC dependencies used for verification. DSL value carriers follow that dialect: Uint64 uses ULong, Json uses JSON, Timestamp uses Instant, Utf8 uses String. DTO members use reference types, including nullable values. Structured batch records use the JDBC carriers described above: `long` for `Uint64`, `String` for `Json`, and `Instant` for `Timestamp`. diff --git a/examples/README.md b/examples/README.md index 6156d48e..d7eca94e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -12,6 +12,7 @@ YDB adaptations of all five families in [sqlc's examples](https://github.com/sql | [records](records) | Typed INSERT/UPSERT/UPDATE/DELETE SELECT, Struct/list parameters, Digest, JSON tag predicates and configured function signatures; Go native and `database/sql` | | [counters](counters) | Computed DML, constants, fixed wildcard projections and opt-in live schema checking/discovery; Go native and `database/sql` | | [streaming](streaming) | Typed callback exports; Go native SDK and database/sql | +| [namespaces](namespaces) | Static TablePathPrefix, two catalogs with identically named tables, indexed reads and cross-catalog joins; all built-in languages | The five upstream-derived families generate Go native SDK and `database/sql`, C++ native SDK and userver, C# Dapper, Java jOOQ, Kotlin Query SDK, TypeScript, Rust and PHP APIs. The `authors` family also includes Python (native, DB-API, SQLAlchemy), C# ADO.NET, Java native/JDBC, and Kotlin JDBC/Exposed profiles. Shared cross-example builds and test harnesses live in `tests/examples/`, grouped by language. The streaming recipe covers both Go profiles; other profiles reject `:each`. The example directories contain their SQL, generator configuration and code grouped by language and runtime. Runtime commands are in [development](../.agents/development.md). @@ -33,4 +34,4 @@ Example READMEs document SQL changes and unsupported upstream contracts. These a The first five SQL/schema scenarios are adapted from upstream commit `3c2546a4b47fabbcec3e07df420effb1a464728f`; generated code comes from sqlc-ydb. -The `records`, `counters` and `streaming` recipes are native sqlc-ydb examples. Their default configurations are offline and included in the same generation/Go checks; the counters README documents the optional connected configurations. +The `records`, `counters`, `streaming` and `namespaces` recipes are native sqlc-ydb examples. Their default configurations are offline and included in the same generation/Go checks; the counters README documents the optional connected configurations. diff --git a/examples/authors/cpp/CMakeLists.txt b/examples/authors/cpp/CMakeLists.txt index 309bd8c7..f70050f3 100644 --- a/examples/authors/cpp/CMakeLists.txt +++ b/examples/authors/cpp/CMakeLists.txt @@ -13,7 +13,7 @@ add_subdirectory(native) add_subdirectory(userver) # Compile every generated C++ example in the same pinned SDK environment. -foreach(example batch booktest jets ondeck) +foreach(example batch booktest jets ondeck namespaces) foreach(runtime native userver) set(source_dir "${CMAKE_CURRENT_SOURCE_DIR}/../../${example}/cpp/${runtime}") add_library(${example}_${runtime} STATIC "${source_dir}/queries.cpp") diff --git a/examples/namespaces/README.md b/examples/namespaces/README.md new file mode 100644 index 00000000..bc7f2c29 --- /dev/null +++ b/examples/namespaces/README.md @@ -0,0 +1,11 @@ +# Namespaces + +Two catalogs contain a table named `users`. Each named query has an explicit static `TablePathPrefix`, so its unqualified table reference selects the intended catalog. `CompareUserNames` combines a relative reference with an absolute path into the other catalog; `FindPrimaryUsersByName` selects a secondary index. The schema uses one prefix per source file. The example generates all built-in language/runtime profiles. + +The checked-in paths target the `/local` database of local-ydb. For another database or environment, replace `/local/sqlc_namespaces/a` and `/local/sqlc_namespaces/b` consistently in the schema and queries, then regenerate. The prefix is part of the compiled SQL, not a generated method argument or an environment-variable substitution. sqlc-ydb currently requires an absolute prefix including the database name. + +`GetPrimaryUser` and `FindPrimaryUsersByName` demonstrate offline parameter inference. `GetSecondaryUser` and both writes declare their parameters explicitly. Connected analysis requires declarations for every external parameter; add them to the inferred queries before using a database configuration. + +The schema defines two independent tables, and generated catalog models retain their full path when deriving unique names. SQL aliases and result field names retain their query meanings. See [table path resolution](../../docs/compatibility.md#table-path-resolution) for the supported pragma forms and [database-assisted analysis](../../docs/database-analysis.md) for schema checking and discovery. + +Run `make generate` from the repository root. The Go examples compile under the shared examples module. Runtime checks use disposable tables and run sequentially; see [development](../../.agents/development.md). diff --git a/examples/namespaces/cpp/native/models.hpp b/examples/namespaces/cpp/native/models.hpp new file mode 100644 index 00000000..c99ed9e8 --- /dev/null +++ b/examples/namespaces/cpp/native/models.hpp @@ -0,0 +1,31 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +#pragma once + +#include +#include +#include + +namespace namespaces::native { + +struct GetPrimaryUserRow final { + std::uint64_t id; + std::string name; +}; + +struct GetSecondaryUserRow final { + std::uint64_t id; + std::string name; +}; + +struct FindPrimaryUsersByNameRow final { + std::uint64_t id; + std::string name; +}; + +struct CompareUserNamesRow final { + std::uint64_t id; + std::string primary_name; + std::optional secondary_name; +}; + +} // namespace namespaces::native diff --git a/examples/namespaces/cpp/native/queries.cpp b/examples/namespaces/cpp/native/queries.cpp new file mode 100644 index 00000000..4de965e8 --- /dev/null +++ b/examples/namespaces/cpp/native/queries.cpp @@ -0,0 +1,254 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +#include "queries.hpp" + +#include +#include +#include + +#include +#include + +namespace namespaces::native { + +// -- name: UpsertPrimaryUser :exec +void Queries::UpsertPrimaryUser(std::uint64_t id, const std::string& name) const { + const auto sqlc_execute = [&](NYdb::NQuery::TSession sqlc_session, const NYdb::NQuery::TTxControl& sqlc_tx) -> NYdb::TStatus { + auto sqlc_params = NYdb::TParamsBuilder() + .AddParam("$id").Uint64(id).Build() + .AddParam("$name").Utf8(name).Build() + .Build(); + auto sqlc_result = sqlc_session.ExecuteQuery( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "DECLARE $id AS Uint64;\n" + "DECLARE $name AS Utf8;\n" + "UPSERT INTO users (id, name) VALUES ($id, $name);", + sqlc_tx, + sqlc_params, + this->execute_settings_ + ).GetValueSync(); + return sqlc_result; + }; + const auto sqlc_status = this->transaction_ != nullptr + ? sqlc_execute(this->transaction_->GetSession(), NYdb::NQuery::TTxControl::Tx(*this->transaction_)) + : this->client_->RetryQuerySync([&](NYdb::NQuery::TSession sqlc_session) -> NYdb::TStatus { + return sqlc_execute( + std::move(sqlc_session), + NYdb::NQuery::TTxControl::BeginTx(this->tx_settings_).CommitTx() + ); + }, this->retry_settings_); + NYdb::NStatusHelpers::ThrowOnError(sqlc_status); +} + +// -- name: UpsertSecondaryUser :exec +void Queries::UpsertSecondaryUser(std::uint64_t id, const std::string& name) const { + const auto sqlc_execute = [&](NYdb::NQuery::TSession sqlc_session, const NYdb::NQuery::TTxControl& sqlc_tx) -> NYdb::TStatus { + auto sqlc_params = NYdb::TParamsBuilder() + .AddParam("$id").Uint64(id).Build() + .AddParam("$name").Utf8(name).Build() + .Build(); + auto sqlc_result = sqlc_session.ExecuteQuery( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + "DECLARE $id AS Uint64;\n" + "DECLARE $name AS Utf8;\n" + "UPSERT INTO users (id, name) VALUES ($id, $name);", + sqlc_tx, + sqlc_params, + this->execute_settings_ + ).GetValueSync(); + return sqlc_result; + }; + const auto sqlc_status = this->transaction_ != nullptr + ? sqlc_execute(this->transaction_->GetSession(), NYdb::NQuery::TTxControl::Tx(*this->transaction_)) + : this->client_->RetryQuerySync([&](NYdb::NQuery::TSession sqlc_session) -> NYdb::TStatus { + return sqlc_execute( + std::move(sqlc_session), + NYdb::NQuery::TTxControl::BeginTx(this->tx_settings_).CommitTx() + ); + }, this->retry_settings_); + NYdb::NStatusHelpers::ThrowOnError(sqlc_status); +} + +// -- name: GetPrimaryUser :one +std::optional Queries::GetPrimaryUser(std::uint64_t id) const { + std::optional sqlc_result_set; + const auto sqlc_execute = [&](NYdb::NQuery::TSession sqlc_session, const NYdb::NQuery::TTxControl& sqlc_tx) -> NYdb::TStatus { + auto sqlc_params = NYdb::TParamsBuilder() + .AddParam("$id").Uint64(id).Build() + .Build(); + auto sqlc_result = sqlc_session.ExecuteQuery( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;", + sqlc_tx, + sqlc_params, + this->execute_settings_ + ).GetValueSync(); + if (sqlc_result.IsSuccess()) { + if (sqlc_result.GetResultSets().size() != 1) { + throw std::runtime_error("expected exactly one result set"); + } + sqlc_result_set = sqlc_result.GetResultSet(0); + } + return sqlc_result; + }; + const auto sqlc_status = this->transaction_ != nullptr + ? sqlc_execute(this->transaction_->GetSession(), NYdb::NQuery::TTxControl::Tx(*this->transaction_)) + : this->client_->RetryQuerySync([&](NYdb::NQuery::TSession sqlc_session) -> NYdb::TStatus { + return sqlc_execute( + std::move(sqlc_session), + NYdb::NQuery::TTxControl::BeginTx(this->tx_settings_).CommitTx() + ); + }, this->retry_settings_); + NYdb::NStatusHelpers::ThrowOnError(sqlc_status); + if (!sqlc_result_set) { + throw std::runtime_error("GetPrimaryUser: successful query returned no result set"); + } + NYdb::TResultSetParser sqlc_parser(*sqlc_result_set); + if (!sqlc_parser.TryNextRow()) { + return std::nullopt; + } + GetPrimaryUserRow sqlc_row{ + sqlc_parser.ColumnParser("id").GetUint64(), + sqlc_parser.ColumnParser("name").GetUtf8(), + }; + return sqlc_row; +} + +// -- name: GetSecondaryUser :one +std::optional Queries::GetSecondaryUser(std::uint64_t id) const { + std::optional sqlc_result_set; + const auto sqlc_execute = [&](NYdb::NQuery::TSession sqlc_session, const NYdb::NQuery::TTxControl& sqlc_tx) -> NYdb::TStatus { + auto sqlc_params = NYdb::TParamsBuilder() + .AddParam("$id").Uint64(id).Build() + .Build(); + auto sqlc_result = sqlc_session.ExecuteQuery( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + "DECLARE $id AS Uint64;\n" + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;", + sqlc_tx, + sqlc_params, + this->execute_settings_ + ).GetValueSync(); + if (sqlc_result.IsSuccess()) { + if (sqlc_result.GetResultSets().size() != 1) { + throw std::runtime_error("expected exactly one result set"); + } + sqlc_result_set = sqlc_result.GetResultSet(0); + } + return sqlc_result; + }; + const auto sqlc_status = this->transaction_ != nullptr + ? sqlc_execute(this->transaction_->GetSession(), NYdb::NQuery::TTxControl::Tx(*this->transaction_)) + : this->client_->RetryQuerySync([&](NYdb::NQuery::TSession sqlc_session) -> NYdb::TStatus { + return sqlc_execute( + std::move(sqlc_session), + NYdb::NQuery::TTxControl::BeginTx(this->tx_settings_).CommitTx() + ); + }, this->retry_settings_); + NYdb::NStatusHelpers::ThrowOnError(sqlc_status); + if (!sqlc_result_set) { + throw std::runtime_error("GetSecondaryUser: successful query returned no result set"); + } + NYdb::TResultSetParser sqlc_parser(*sqlc_result_set); + if (!sqlc_parser.TryNextRow()) { + return std::nullopt; + } + GetSecondaryUserRow sqlc_row{ + sqlc_parser.ColumnParser("id").GetUint64(), + sqlc_parser.ColumnParser("name").GetUtf8(), + }; + return sqlc_row; +} + +// -- name: FindPrimaryUsersByName :many +std::vector Queries::FindPrimaryUsersByName(const std::string& name) const { + std::optional sqlc_result_set; + const auto sqlc_execute = [&](NYdb::NQuery::TSession sqlc_session, const NYdb::NQuery::TTxControl& sqlc_tx) -> NYdb::TStatus { + auto sqlc_params = NYdb::TParamsBuilder() + .AddParam("$name").Utf8(name).Build() + .Build(); + auto sqlc_result = sqlc_session.ExecuteQuery( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "SELECT u.`id` AS `id`, `u`.`name` AS `name` FROM users VIEW by_name AS u WHERE u.name = $name ORDER BY u.id;", + sqlc_tx, + sqlc_params, + this->execute_settings_ + ).GetValueSync(); + if (sqlc_result.IsSuccess()) { + if (sqlc_result.GetResultSets().size() != 1) { + throw std::runtime_error("expected exactly one result set"); + } + sqlc_result_set = sqlc_result.GetResultSet(0); + } + return sqlc_result; + }; + const auto sqlc_status = this->transaction_ != nullptr + ? sqlc_execute(this->transaction_->GetSession(), NYdb::NQuery::TTxControl::Tx(*this->transaction_)) + : this->client_->RetryQuerySync([&](NYdb::NQuery::TSession sqlc_session) -> NYdb::TStatus { + return sqlc_execute( + std::move(sqlc_session), + NYdb::NQuery::TTxControl::BeginTx(this->tx_settings_).CommitTx() + ); + }, this->retry_settings_); + NYdb::NStatusHelpers::ThrowOnError(sqlc_status); + if (!sqlc_result_set) { + throw std::runtime_error("FindPrimaryUsersByName: successful query returned no result set"); + } + NYdb::TResultSetParser sqlc_parser(*sqlc_result_set); + std::vector sqlc_rows; + sqlc_rows.reserve(sqlc_result_set->RowsCount()); + while (sqlc_parser.TryNextRow()) { + sqlc_rows.push_back(FindPrimaryUsersByNameRow{ + sqlc_parser.ColumnParser("id").GetUint64(), + sqlc_parser.ColumnParser("name").GetUtf8(), + }); + } + return sqlc_rows; +} + +// -- name: CompareUserNames :many +std::vector Queries::CompareUserNames() const { + std::optional sqlc_result_set; + const auto sqlc_execute = [&](NYdb::NQuery::TSession sqlc_session, const NYdb::NQuery::TTxControl& sqlc_tx) -> NYdb::TStatus { + auto sqlc_result = sqlc_session.ExecuteQuery( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name\n" + "FROM users AS a\n" + "LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id\n" + "ORDER BY a.id;", + sqlc_tx, + this->execute_settings_ + ).GetValueSync(); + if (sqlc_result.IsSuccess()) { + if (sqlc_result.GetResultSets().size() != 1) { + throw std::runtime_error("expected exactly one result set"); + } + sqlc_result_set = sqlc_result.GetResultSet(0); + } + return sqlc_result; + }; + const auto sqlc_status = this->transaction_ != nullptr + ? sqlc_execute(this->transaction_->GetSession(), NYdb::NQuery::TTxControl::Tx(*this->transaction_)) + : this->client_->RetryQuerySync([&](NYdb::NQuery::TSession sqlc_session) -> NYdb::TStatus { + return sqlc_execute( + std::move(sqlc_session), + NYdb::NQuery::TTxControl::BeginTx(this->tx_settings_).CommitTx() + ); + }, this->retry_settings_); + NYdb::NStatusHelpers::ThrowOnError(sqlc_status); + if (!sqlc_result_set) { + throw std::runtime_error("CompareUserNames: successful query returned no result set"); + } + NYdb::TResultSetParser sqlc_parser(*sqlc_result_set); + std::vector sqlc_rows; + sqlc_rows.reserve(sqlc_result_set->RowsCount()); + while (sqlc_parser.TryNextRow()) { + sqlc_rows.push_back(CompareUserNamesRow{ + sqlc_parser.ColumnParser("id").GetUint64(), + sqlc_parser.ColumnParser("primary_name").GetUtf8(), + sqlc_parser.ColumnParser("secondary_name").GetOptionalUtf8(), + }); + } + return sqlc_rows; +} + +} // namespace namespaces::native diff --git a/examples/namespaces/cpp/native/queries.hpp b/examples/namespaces/cpp/native/queries.hpp new file mode 100644 index 00000000..ea7f2481 --- /dev/null +++ b/examples/namespaces/cpp/native/queries.hpp @@ -0,0 +1,42 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +#pragma once + +#include "models.hpp" + +#include +#include +#include +#include + +#include + +namespace namespaces::native { + +class Queries final { +public: + explicit Queries(NYdb::NQuery::TQueryClient& client, + const NYdb::NRetry::TRetryOperationSettings& retry_settings = {}, + const NYdb::NQuery::TTxSettings& tx_settings = NYdb::NQuery::TTxSettings::SerializableRW(), + const NYdb::NQuery::TExecuteQuerySettings& execute_settings = {}) + : client_(&client), transaction_(nullptr), retry_settings_(retry_settings), + tx_settings_(tx_settings), execute_settings_(execute_settings) {} + explicit Queries(NYdb::NQuery::TTransaction& transaction, + const NYdb::NQuery::TExecuteQuerySettings& execute_settings = {}) + : client_(nullptr), transaction_(&transaction), execute_settings_(execute_settings) {} + + void UpsertPrimaryUser(std::uint64_t id, const std::string& name) const; + void UpsertSecondaryUser(std::uint64_t id, const std::string& name) const; + std::optional GetPrimaryUser(std::uint64_t id) const; + std::optional GetSecondaryUser(std::uint64_t id) const; + std::vector FindPrimaryUsersByName(const std::string& name) const; + std::vector CompareUserNames() const; + +private: + NYdb::NQuery::TQueryClient* client_; + NYdb::NQuery::TTransaction* transaction_; + NYdb::NRetry::TRetryOperationSettings retry_settings_; + NYdb::NQuery::TTxSettings tx_settings_{NYdb::NQuery::TTxSettings::SerializableRW()}; + NYdb::NQuery::TExecuteQuerySettings execute_settings_; +}; + +} // namespace namespaces::native diff --git a/examples/namespaces/cpp/userver/models.hpp b/examples/namespaces/cpp/userver/models.hpp new file mode 100644 index 00000000..e82f2425 --- /dev/null +++ b/examples/namespaces/cpp/userver/models.hpp @@ -0,0 +1,32 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +#pragma once + +#include +#include + +#include + +namespace namespaces::userver { + +struct GetPrimaryUserRow final { + std::uint64_t id; + ::userver::ydb::Utf8 name; +}; + +struct GetSecondaryUserRow final { + std::uint64_t id; + ::userver::ydb::Utf8 name; +}; + +struct FindPrimaryUsersByNameRow final { + std::uint64_t id; + ::userver::ydb::Utf8 name; +}; + +struct CompareUserNamesRow final { + std::uint64_t id; + ::userver::ydb::Utf8 primary_name; + std::optional<::userver::ydb::Utf8> secondary_name; +}; + +} // namespace namespaces::userver diff --git a/examples/namespaces/cpp/userver/queries.cpp b/examples/namespaces/cpp/userver/queries.cpp new file mode 100644 index 00000000..aa512f10 --- /dev/null +++ b/examples/namespaces/cpp/userver/queries.cpp @@ -0,0 +1,139 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +#include "queries.hpp" + +namespace namespaces::userver { + +// -- name: UpsertPrimaryUser :exec +void Queries::UpsertPrimaryUser(std::uint64_t id, const ::userver::ydb::Utf8& name) const { + const auto sqlc_query = ::userver::ydb::Query{ + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "DECLARE $id AS Uint64;\n" + "DECLARE $name AS Utf8;\n" + "UPSERT INTO users (id, name) VALUES ($id, $name);", + ::userver::ydb::Query::Name{"UpsertPrimaryUser"}, + ::userver::ydb::Query::LogMode::kNameOnly, + }; + static_cast( + this->transaction_ != nullptr + ? this->transaction_->Execute(this->execute_settings_, sqlc_query, "$id", id, "$name", name) + : this->client_->ExecuteQuery(this->operation_settings_, sqlc_query, "$id", id, "$name", name) + ); +} + +// -- name: UpsertSecondaryUser :exec +void Queries::UpsertSecondaryUser(std::uint64_t id, const ::userver::ydb::Utf8& name) const { + const auto sqlc_query = ::userver::ydb::Query{ + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + "DECLARE $id AS Uint64;\n" + "DECLARE $name AS Utf8;\n" + "UPSERT INTO users (id, name) VALUES ($id, $name);", + ::userver::ydb::Query::Name{"UpsertSecondaryUser"}, + ::userver::ydb::Query::LogMode::kNameOnly, + }; + static_cast( + this->transaction_ != nullptr + ? this->transaction_->Execute(this->execute_settings_, sqlc_query, "$id", id, "$name", name) + : this->client_->ExecuteQuery(this->operation_settings_, sqlc_query, "$id", id, "$name", name) + ); +} + +// -- name: GetPrimaryUser :one +std::optional Queries::GetPrimaryUser(std::uint64_t id) const { + const auto sqlc_query = ::userver::ydb::Query{ + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;", + ::userver::ydb::Query::Name{"GetPrimaryUser"}, + ::userver::ydb::Query::LogMode::kNameOnly, + }; + auto sqlc_response = + this->transaction_ != nullptr + ? this->transaction_->Execute(this->execute_settings_, sqlc_query, "$id", id) + : this->client_->ExecuteQuery(this->operation_settings_, sqlc_query, "$id", id); + auto sqlc_cursor = sqlc_response.GetSingleCursor(); + if (sqlc_cursor.empty()) { + return std::nullopt; + } + auto sqlc_row = sqlc_cursor.GetFirstRow(); + return GetPrimaryUserRow{ + sqlc_row.Get("id"), + sqlc_row.Get<::userver::ydb::Utf8>("name"), + }; +} + +// -- name: GetSecondaryUser :one +std::optional Queries::GetSecondaryUser(std::uint64_t id) const { + const auto sqlc_query = ::userver::ydb::Query{ + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + "DECLARE $id AS Uint64;\n" + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;", + ::userver::ydb::Query::Name{"GetSecondaryUser"}, + ::userver::ydb::Query::LogMode::kNameOnly, + }; + auto sqlc_response = + this->transaction_ != nullptr + ? this->transaction_->Execute(this->execute_settings_, sqlc_query, "$id", id) + : this->client_->ExecuteQuery(this->operation_settings_, sqlc_query, "$id", id); + auto sqlc_cursor = sqlc_response.GetSingleCursor(); + if (sqlc_cursor.empty()) { + return std::nullopt; + } + auto sqlc_row = sqlc_cursor.GetFirstRow(); + return GetSecondaryUserRow{ + sqlc_row.Get("id"), + sqlc_row.Get<::userver::ydb::Utf8>("name"), + }; +} + +// -- name: FindPrimaryUsersByName :many +std::vector Queries::FindPrimaryUsersByName(const ::userver::ydb::Utf8& name) const { + const auto sqlc_query = ::userver::ydb::Query{ + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "SELECT u.`id` AS `id`, `u`.`name` AS `name` FROM users VIEW by_name AS u WHERE u.name = $name ORDER BY u.id;", + ::userver::ydb::Query::Name{"FindPrimaryUsersByName"}, + ::userver::ydb::Query::LogMode::kNameOnly, + }; + auto sqlc_response = + this->transaction_ != nullptr + ? this->transaction_->Execute(this->execute_settings_, sqlc_query, "$name", name) + : this->client_->ExecuteQuery(this->operation_settings_, sqlc_query, "$name", name); + auto sqlc_cursor = sqlc_response.GetSingleCursor(); + std::vector sqlc_rows; + sqlc_rows.reserve(sqlc_cursor.size()); + for (auto sqlc_row : sqlc_cursor) { + sqlc_rows.push_back(FindPrimaryUsersByNameRow{ + sqlc_row.Get("id"), + sqlc_row.Get<::userver::ydb::Utf8>("name"), + }); + } + return sqlc_rows; +} + +// -- name: CompareUserNames :many +std::vector Queries::CompareUserNames() const { + const auto sqlc_query = ::userver::ydb::Query{ + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name\n" + "FROM users AS a\n" + "LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id\n" + "ORDER BY a.id;", + ::userver::ydb::Query::Name{"CompareUserNames"}, + ::userver::ydb::Query::LogMode::kNameOnly, + }; + auto sqlc_response = + this->transaction_ != nullptr + ? this->transaction_->Execute(this->execute_settings_, sqlc_query) + : this->client_->ExecuteQuery(this->operation_settings_, sqlc_query); + auto sqlc_cursor = sqlc_response.GetSingleCursor(); + std::vector sqlc_rows; + sqlc_rows.reserve(sqlc_cursor.size()); + for (auto sqlc_row : sqlc_cursor) { + sqlc_rows.push_back(CompareUserNamesRow{ + sqlc_row.Get("id"), + sqlc_row.Get<::userver::ydb::Utf8>("primary_name"), + sqlc_row.Get>("secondary_name"), + }); + } + return sqlc_rows; +} + +} // namespace namespaces::userver diff --git a/examples/namespaces/cpp/userver/queries.hpp b/examples/namespaces/cpp/userver/queries.hpp new file mode 100644 index 00000000..8471bce3 --- /dev/null +++ b/examples/namespaces/cpp/userver/queries.hpp @@ -0,0 +1,38 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +#pragma once + +#include "models.hpp" + +#include +#include +#include + +#include +#include + +namespace namespaces::userver { + +class Queries final { +public: + explicit Queries(::userver::ydb::TableClient& client, + const ::userver::ydb::OperationSettings& operation_settings = {}) + : client_(&client), transaction_(nullptr), operation_settings_(operation_settings) {} + explicit Queries(::userver::ydb::TxActor& transaction, + const ::userver::ydb::ExecuteSettings& execute_settings = {}) + : client_(nullptr), transaction_(&transaction), execute_settings_(execute_settings) {} + + void UpsertPrimaryUser(std::uint64_t id, const ::userver::ydb::Utf8& name) const; + void UpsertSecondaryUser(std::uint64_t id, const ::userver::ydb::Utf8& name) const; + std::optional GetPrimaryUser(std::uint64_t id) const; + std::optional GetSecondaryUser(std::uint64_t id) const; + std::vector FindPrimaryUsersByName(const ::userver::ydb::Utf8& name) const; + std::vector CompareUserNames() const; + +private: + ::userver::ydb::TableClient* client_; + ::userver::ydb::TxActor* transaction_; + ::userver::ydb::OperationSettings operation_settings_; + ::userver::ydb::ExecuteSettings execute_settings_; +}; + +} // namespace namespaces::userver diff --git a/examples/namespaces/csharp/adonet/Models.cs b/examples/namespaces/csharp/adonet/Models.cs new file mode 100644 index 00000000..7a784d08 --- /dev/null +++ b/examples/namespaces/csharp/adonet/Models.cs @@ -0,0 +1,45 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +#nullable enable +using System; + +namespace Namespaces.AdoNet; +public sealed record LocalSqlcNamespacesAUsers( + ulong ID, + string Name +); + +public sealed record LocalSqlcNamespacesBUsers( + ulong ID, + string Name +); + +public sealed record UpsertPrimaryUserParams( + ulong ID, + string Name +); + +public sealed record UpsertSecondaryUserParams( + ulong ID, + string Name +); + +public sealed record GetPrimaryUserRow( + ulong ID, + string Name +); + +public sealed record GetSecondaryUserRow( + ulong ID, + string Name +); + +public sealed record FindPrimaryUsersByNameRow( + ulong ID, + string Name +); + +public sealed record CompareUserNamesRow( + ulong ID, + string PrimaryName, + string? SecondaryName +); diff --git a/examples/namespaces/csharp/adonet/Queries.cs b/examples/namespaces/csharp/adonet/Queries.cs new file mode 100644 index 00000000..73f5717d --- /dev/null +++ b/examples/namespaces/csharp/adonet/Queries.cs @@ -0,0 +1,143 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +#nullable enable +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Ydb.Sdk.Ado; +using Ydb.Sdk.Value; + +namespace Namespaces.AdoNet; + + +public sealed class Queries +{ + private readonly YdbConnection _connection; + private readonly YdbTransaction? _transaction; + + public Queries(YdbConnection connection, YdbTransaction? transaction = null) + { + _connection = connection ?? throw new ArgumentNullException(nameof(connection)); + if (transaction is not null && !ReferenceEquals(transaction.Connection, connection)) + { + throw new ArgumentException("Transaction must belong to the supplied connection.", nameof(transaction)); + } + _transaction = transaction; + } + + public Queries WithTransaction(YdbTransaction transaction) => new(_connection, transaction ?? throw new ArgumentNullException(nameof(transaction))); + + // -- name: UpsertPrimaryUser :exec + public async Task UpsertPrimaryUserAsync(UpsertPrimaryUserParams args, CancellationToken cancellationToken = default) + { + await using var command = new YdbCommand( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "DECLARE $id AS Uint64;\n" + + "DECLARE $name AS Utf8;\n" + + "UPSERT INTO users (id, name) VALUES ($id, $name);", _connection) { Transaction = _transaction }; + command.Parameters.Add(new YdbParameter("$id", DbType.UInt64, args.ID)); + command.Parameters.Add(new YdbParameter("$name", DbType.String, args.Name)); + await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + } + + // -- name: UpsertSecondaryUser :exec + public async Task UpsertSecondaryUserAsync(UpsertSecondaryUserParams args, CancellationToken cancellationToken = default) + { + await using var command = new YdbCommand( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + + "DECLARE $id AS Uint64;\n" + + "DECLARE $name AS Utf8;\n" + + "UPSERT INTO users (id, name) VALUES ($id, $name);", _connection) { Transaction = _transaction }; + command.Parameters.Add(new YdbParameter("$id", DbType.UInt64, args.ID)); + command.Parameters.Add(new YdbParameter("$name", DbType.String, args.Name)); + await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); + } + + // -- name: GetPrimaryUser :one + public async Task GetPrimaryUserAsync(ulong id, CancellationToken cancellationToken = default) + { + await using var command = new YdbCommand( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;", _connection) { Transaction = _transaction }; + command.Parameters.Add(new YdbParameter("$id", DbType.UInt64, id)); + await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + throw new InvalidOperationException("query returned no rows"); + } + return GetPrimaryUserRowFrom(reader); + } + + private static GetPrimaryUserRow GetPrimaryUserRowFrom(DbDataReader reader) => new( + reader.GetFieldValue(0), + reader.GetFieldValue(1) + ); + + // -- name: GetSecondaryUser :one + public async Task GetSecondaryUserAsync(ulong id, CancellationToken cancellationToken = default) + { + await using var command = new YdbCommand( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + + "DECLARE $id AS Uint64;\n" + + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;", _connection) { Transaction = _transaction }; + command.Parameters.Add(new YdbParameter("$id", DbType.UInt64, id)); + await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + throw new InvalidOperationException("query returned no rows"); + } + return GetSecondaryUserRowFrom(reader); + } + + private static GetSecondaryUserRow GetSecondaryUserRowFrom(DbDataReader reader) => new( + reader.GetFieldValue(0), + reader.GetFieldValue(1) + ); + + // -- name: FindPrimaryUsersByName :many + public async Task> FindPrimaryUsersByNameAsync(string name, CancellationToken cancellationToken = default) + { + await using var command = new YdbCommand( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "SELECT u.`id` AS `id`, `u`.`name` AS `name` FROM users VIEW by_name AS u WHERE u.name = $name ORDER BY u.id;", _connection) { Transaction = _transaction }; + command.Parameters.Add(new YdbParameter("$name", DbType.String, name)); + await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + var rows = new List(); + while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + rows.Add(FindPrimaryUsersByNameRowFrom(reader)); + } + return rows; + } + + private static FindPrimaryUsersByNameRow FindPrimaryUsersByNameRowFrom(DbDataReader reader) => new( + reader.GetFieldValue(0), + reader.GetFieldValue(1) + ); + + // -- name: CompareUserNames :many + public async Task> CompareUserNamesAsync(CancellationToken cancellationToken = default) + { + await using var command = new YdbCommand( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name\n" + + "FROM users AS a\n" + + "LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id\n" + + "ORDER BY a.id;", _connection) { Transaction = _transaction }; + await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); + var rows = new List(); + while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) + { + rows.Add(CompareUserNamesRowFrom(reader)); + } + return rows; + } + + private static CompareUserNamesRow CompareUserNamesRowFrom(DbDataReader reader) => new( + reader.GetFieldValue(0), + reader.GetFieldValue(1), + reader.IsDBNull(2) ? null : reader.GetFieldValue(2) + ); +} diff --git a/examples/namespaces/csharp/dapper/Models.cs b/examples/namespaces/csharp/dapper/Models.cs new file mode 100644 index 00000000..d26a4935 --- /dev/null +++ b/examples/namespaces/csharp/dapper/Models.cs @@ -0,0 +1,45 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +#nullable enable +using System; + +namespace Namespaces.Dapper; +public sealed record LocalSqlcNamespacesAUsers( + ulong ID, + string Name +); + +public sealed record LocalSqlcNamespacesBUsers( + ulong ID, + string Name +); + +public sealed record UpsertPrimaryUserParams( + ulong ID, + string Name +); + +public sealed record UpsertSecondaryUserParams( + ulong ID, + string Name +); + +public sealed record GetPrimaryUserRow( + ulong ID, + string Name +); + +public sealed record GetSecondaryUserRow( + ulong ID, + string Name +); + +public sealed record FindPrimaryUsersByNameRow( + ulong ID, + string Name +); + +public sealed record CompareUserNamesRow( + ulong ID, + string PrimaryName, + string? SecondaryName +); diff --git a/examples/namespaces/csharp/dapper/Queries.cs b/examples/namespaces/csharp/dapper/Queries.cs new file mode 100644 index 00000000..9f8665e7 --- /dev/null +++ b/examples/namespaces/csharp/dapper/Queries.cs @@ -0,0 +1,200 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +#nullable enable +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Dapper; +using Ydb.Sdk.Ado; +using Ydb.Sdk.Value; + +namespace Namespaces.Dapper; + +public sealed class Queries +{ + private readonly YdbConnection _connection; + private readonly YdbTransaction? _transaction; + + public Queries(YdbConnection connection, YdbTransaction? transaction = null) + { + _connection = connection ?? throw new ArgumentNullException(nameof(connection)); + if (transaction is not null && !ReferenceEquals(transaction.Connection, connection)) + { + throw new ArgumentException("Transaction must belong to the supplied connection.", nameof(transaction)); + } + _transaction = transaction; + } + + public Queries WithTransaction(YdbTransaction transaction) => new(_connection, transaction ?? throw new ArgumentNullException(nameof(transaction))); + + static Queries() + { + SqlMapper.SetTypeMap(typeof(CompareUserNamesRow), new ColumnTypeMap(typeof(CompareUserNamesRow), new Dictionary + { + ["primary_name"] = nameof(CompareUserNamesRow.PrimaryName), + ["secondary_name"] = nameof(CompareUserNamesRow.SecondaryName), + })); + } + + private sealed class ColumnTypeMap : SqlMapper.ITypeMap + { + private readonly DefaultTypeMap _default; + private readonly IReadOnlyDictionary _columns; + + public ColumnTypeMap(Type type, IReadOnlyDictionary columns) + { + _default = new DefaultTypeMap(type); + _columns = columns; + } + + private string MemberName(string column) => _columns.TryGetValue(column, out var member) ? member : column; + public ConstructorInfo? FindConstructor(string[] names, Type[] types) => _default.FindConstructor(names.Select(MemberName).ToArray(), types); + public ConstructorInfo? FindExplicitConstructor() => _default.FindExplicitConstructor(); + public SqlMapper.IMemberMap? GetConstructorParameter(ConstructorInfo constructor, string columnName) => _default.GetConstructorParameter(constructor, MemberName(columnName)); + public SqlMapper.IMemberMap? GetMember(string columnName) => _default.GetMember(MemberName(columnName)); + } + + // -- name: UpsertPrimaryUser :exec + public async Task UpsertPrimaryUserAsync(UpsertPrimaryUserParams args, CancellationToken cancellationToken = default, int? commandTimeout = null) + { + var parameters = new YdbParameters( + new YdbParameter("$id", DbType.UInt64, args.ID), + new YdbParameter("$name", DbType.String, args.Name) + ); + + var command = new CommandDefinition( + commandText: """ + PRAGMA TablePathPrefix("/local/sqlc_namespaces/a"); + DECLARE $id AS Uint64; + DECLARE $name AS Utf8; + UPSERT INTO users (id, name) VALUES ($id, $name); + """, + parameters: parameters, + transaction: _transaction, + commandTimeout: commandTimeout, + cancellationToken: cancellationToken); + + await _connection.ExecuteAsync(command).ConfigureAwait(false); + } + + // -- name: UpsertSecondaryUser :exec + public async Task UpsertSecondaryUserAsync(UpsertSecondaryUserParams args, CancellationToken cancellationToken = default, int? commandTimeout = null) + { + var parameters = new YdbParameters( + new YdbParameter("$id", DbType.UInt64, args.ID), + new YdbParameter("$name", DbType.String, args.Name) + ); + + var command = new CommandDefinition( + commandText: """ + PRAGMA TablePathPrefix("/local/sqlc_namespaces/b"); + DECLARE $id AS Uint64; + DECLARE $name AS Utf8; + UPSERT INTO users (id, name) VALUES ($id, $name); + """, + parameters: parameters, + transaction: _transaction, + commandTimeout: commandTimeout, + cancellationToken: cancellationToken); + + await _connection.ExecuteAsync(command).ConfigureAwait(false); + } + + // -- name: GetPrimaryUser :one + public async Task GetPrimaryUserAsync(ulong id, CancellationToken cancellationToken = default, int? commandTimeout = null) + { + var parameters = new YdbParameters( + new YdbParameter("$id", DbType.UInt64, id) + ); + + var command = new CommandDefinition( + commandText: """ + PRAGMA TablePathPrefix("/local/sqlc_namespaces/a"); + SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id; + """, + parameters: parameters, + transaction: _transaction, + commandTimeout: commandTimeout, + cancellationToken: cancellationToken); + + return await _connection.QueryFirstAsync(command).ConfigureAwait(false); + } + + // -- name: GetSecondaryUser :one + public async Task GetSecondaryUserAsync(ulong id, CancellationToken cancellationToken = default, int? commandTimeout = null) + { + var parameters = new YdbParameters( + new YdbParameter("$id", DbType.UInt64, id) + ); + + var command = new CommandDefinition( + commandText: """ + PRAGMA TablePathPrefix("/local/sqlc_namespaces/b"); + DECLARE $id AS Uint64; + SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id; + """, + parameters: parameters, + transaction: _transaction, + commandTimeout: commandTimeout, + cancellationToken: cancellationToken); + + return await _connection.QueryFirstAsync(command).ConfigureAwait(false); + } + + // -- name: FindPrimaryUsersByName :many + public async Task> FindPrimaryUsersByNameAsync(string name, CancellationToken cancellationToken = default, int? commandTimeout = null) + { + var parameters = new YdbParameters( + new YdbParameter("$name", DbType.String, name) + ); + + var command = new CommandDefinition( + commandText: """ + PRAGMA TablePathPrefix("/local/sqlc_namespaces/a"); + SELECT u.`id` AS `id`, `u`.`name` AS `name` FROM users VIEW by_name AS u WHERE u.name = $name ORDER BY u.id; + """, + parameters: parameters, + transaction: _transaction, + commandTimeout: commandTimeout, + cancellationToken: cancellationToken); + + return (await _connection.QueryAsync(command).ConfigureAwait(false)).AsList(); + } + + // -- name: CompareUserNames :many + public async Task> CompareUserNamesAsync(CancellationToken cancellationToken = default, int? commandTimeout = null) + { + var command = new CommandDefinition( + commandText: """ + PRAGMA TablePathPrefix("/local/sqlc_namespaces/a"); + SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name + FROM users AS a + LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id + ORDER BY a.id; + """, + parameters: null, + transaction: _transaction, + commandTimeout: commandTimeout, + cancellationToken: cancellationToken); + + return (await _connection.QueryAsync(command).ConfigureAwait(false)).AsList(); + } + + private sealed class YdbParameters : SqlMapper.IDynamicParameters + { + private readonly YdbParameter[] _parameters; + + public YdbParameters(params YdbParameter[] parameters) => _parameters = parameters; + + public void AddParameters(IDbCommand command, SqlMapper.Identity identity) + { + foreach (var parameter in _parameters) + { + command.Parameters.Add(parameter); + } + } + } +} diff --git a/examples/namespaces/go/database/sql/db.go b/examples/namespaces/go/database/sql/db.go new file mode 100644 index 00000000..6283f495 --- /dev/null +++ b/examples/namespaces/go/database/sql/db.go @@ -0,0 +1,17 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...any) (sql.Result, error) + QueryContext(context.Context, string, ...any) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...any) *sql.Row +} +type Queries struct{ db DBTX } + +func New(db DBTX) *Queries { return &Queries{db: db} } +func (q *Queries) WithTx(tx *sql.Tx) *Queries { return &Queries{db: tx} } diff --git a/examples/namespaces/go/database/sql/models.go b/examples/namespaces/go/database/sql/models.go new file mode 100644 index 00000000..452f0af5 --- /dev/null +++ b/examples/namespaces/go/database/sql/models.go @@ -0,0 +1,33 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces + +type UpsertPrimaryUserParams struct { + ID uint64 + Name string +} + +type UpsertSecondaryUserParams struct { + ID uint64 + Name string +} + +type GetPrimaryUserRow struct { + ID uint64 + Name string +} + +type GetSecondaryUserRow struct { + ID uint64 + Name string +} + +type FindPrimaryUsersByNameRow struct { + ID uint64 + Name string +} + +type CompareUserNamesRow struct { + ID uint64 + PrimaryName string + SecondaryName *string +} diff --git a/examples/namespaces/go/database/sql/queries.sql.go b/examples/namespaces/go/database/sql/queries.sql.go new file mode 100644 index 00000000..331770eb --- /dev/null +++ b/examples/namespaces/go/database/sql/queries.sql.go @@ -0,0 +1,132 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +// source: queries.sql +package namespaces + +import ( + "context" + "database/sql" +) + +// -- name: UpsertPrimaryUser :exec +func (q *Queries) UpsertPrimaryUser(ctx context.Context, arg UpsertPrimaryUserParams) error { + _, err := q.db.ExecContext(ctx, ""+ + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n"+ + "DECLARE $id AS Uint64;\n"+ + "DECLARE $name AS Utf8;\n"+ + "UPSERT INTO users (id, name) VALUES ($id, $name);", + sql.Named("id", arg.ID), + sql.Named("name", arg.Name), + ) + + return err +} + +// -- name: UpsertSecondaryUser :exec +func (q *Queries) UpsertSecondaryUser(ctx context.Context, arg UpsertSecondaryUserParams) error { + _, err := q.db.ExecContext(ctx, ""+ + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n"+ + "DECLARE $id AS Uint64;\n"+ + "DECLARE $name AS Utf8;\n"+ + "UPSERT INTO users (id, name) VALUES ($id, $name);", + sql.Named("id", arg.ID), + sql.Named("name", arg.Name), + ) + + return err +} + +// -- name: GetPrimaryUser :one +func (q *Queries) GetPrimaryUser(ctx context.Context, arg uint64) (GetPrimaryUserRow, error) { + var row GetPrimaryUserRow + err := q.db.QueryRowContext(ctx, ""+ + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n"+ + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;", + sql.Named("id", arg), + ).Scan( + &row.ID, + &row.Name, + ) + + return row, err +} + +// -- name: GetSecondaryUser :one +func (q *Queries) GetSecondaryUser(ctx context.Context, arg uint64) (GetSecondaryUserRow, error) { + var row GetSecondaryUserRow + err := q.db.QueryRowContext(ctx, ""+ + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n"+ + "DECLARE $id AS Uint64;\n"+ + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;", + sql.Named("id", arg), + ).Scan( + &row.ID, + &row.Name, + ) + + return row, err +} + +// -- name: FindPrimaryUsersByName :many +func (q *Queries) FindPrimaryUsersByName(ctx context.Context, arg string) ([]FindPrimaryUsersByNameRow, error) { + rows, err := q.db.QueryContext(ctx, ""+ + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n"+ + "SELECT u.`id` AS `id`, `u`.`name` AS `name` FROM users VIEW by_name AS u WHERE u.name = $name ORDER BY u.id;", + sql.Named("name", arg), + ) + if err != nil { + return nil, err + } + defer rows.Close() + + items := []FindPrimaryUsersByNameRow(nil) + for rows.Next() { + var row FindPrimaryUsersByNameRow + if err := rows.Scan( + &row.ID, + &row.Name, + ); err != nil { + return nil, err + } + items = append(items, row) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return items, nil +} + +// -- name: CompareUserNames :many +func (q *Queries) CompareUserNames(ctx context.Context) ([]CompareUserNamesRow, error) { + rows, err := q.db.QueryContext(ctx, ""+ + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n"+ + "SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name\n"+ + "FROM users AS a\n"+ + "LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id\n"+ + "ORDER BY a.id;", + ) + if err != nil { + return nil, err + } + defer rows.Close() + + items := []CompareUserNamesRow(nil) + for rows.Next() { + var row CompareUserNamesRow + if err := rows.Scan( + &row.ID, + &row.PrimaryName, + &row.SecondaryName, + ); err != nil { + return nil, err + } + items = append(items, row) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return items, nil +} diff --git a/examples/namespaces/go/native/db.go b/examples/namespaces/go/native/db.go new file mode 100644 index 00000000..9dd37196 --- /dev/null +++ b/examples/namespaces/go/native/db.go @@ -0,0 +1,17 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces + +import ( + "context" + + "github.com/ydb-platform/ydb-go-sdk/v3/query" +) + +type DBTX interface { + Exec(context.Context, string, ...query.ExecuteOption) error + Query(context.Context, string, ...query.ExecuteOption) (query.Result, error) + QueryRow(context.Context, string, ...query.ExecuteOption) (query.Row, error) +} +type Queries struct{ db DBTX } + +func New(db DBTX) *Queries { return &Queries{db: db} } diff --git a/examples/namespaces/go/native/models.go b/examples/namespaces/go/native/models.go new file mode 100644 index 00000000..f978f219 --- /dev/null +++ b/examples/namespaces/go/native/models.go @@ -0,0 +1,47 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces + +import ( + "context" + "github.com/ydb-platform/ydb-go-sdk/v3/query" +) + +type UpsertPrimaryUserParams struct { + ID uint64 `json:"id"` + Name string `json:"name"` +} + +type UpsertSecondaryUserParams struct { + ID uint64 `json:"id"` + Name string `json:"name"` +} + +type GetPrimaryUserRow struct { + ID uint64 `json:"id"` + Name string `json:"name"` +} + +type GetSecondaryUserRow struct { + ID uint64 `json:"id"` + Name string `json:"name"` +} + +type FindPrimaryUsersByNameRow struct { + ID uint64 `json:"id"` + Name string `json:"name"` +} + +type CompareUserNamesRow struct { + ID uint64 `json:"id"` + PrimaryName string `json:"primary_name"` + SecondaryName *string `json:"secondary_name"` +} + +type Querier interface { + UpsertPrimaryUser(ctx context.Context, arg UpsertPrimaryUserParams, opts ...query.ExecuteOption) error + UpsertSecondaryUser(ctx context.Context, arg UpsertSecondaryUserParams, opts ...query.ExecuteOption) error + GetPrimaryUser(ctx context.Context, arg uint64, opts ...query.ExecuteOption) (GetPrimaryUserRow, error) + GetSecondaryUser(ctx context.Context, arg uint64, opts ...query.ExecuteOption) (GetSecondaryUserRow, error) + FindPrimaryUsersByName(ctx context.Context, arg string, opts ...query.ExecuteOption) ([]FindPrimaryUsersByNameRow, error) + CompareUserNames(ctx context.Context, opts ...query.ExecuteOption) ([]CompareUserNamesRow, error) +} diff --git a/examples/namespaces/go/native/queries.sql.go b/examples/namespaces/go/native/queries.sql.go new file mode 100644 index 00000000..1daaca5f --- /dev/null +++ b/examples/namespaces/go/native/queries.sql.go @@ -0,0 +1,210 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +// source: queries.sql +package namespaces + +import ( + "context" + "errors" + "io" + + ydb "github.com/ydb-platform/ydb-go-sdk/v3" + "github.com/ydb-platform/ydb-go-sdk/v3/pkg/xerrors" + "github.com/ydb-platform/ydb-go-sdk/v3/query" +) + +// -- name: UpsertPrimaryUser :exec +func (q *Queries) UpsertPrimaryUser(ctx context.Context, arg UpsertPrimaryUserParams, opts ...query.ExecuteOption) error { + parameters := ydb.ParamsBuilder() + parameters = parameters.Param("$id").Uint64(arg.ID) + parameters = parameters.Param("$name").Text(arg.Name) + + callOptions := append([]query.ExecuteOption(nil), opts...) + callOptions = append(callOptions, query.WithParameters(parameters.Build())) + + err := q.db.Exec(ctx, ""+ + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n"+ + "DECLARE $id AS Uint64;\n"+ + "DECLARE $name AS Utf8;\n"+ + "UPSERT INTO users (id, name) VALUES ($id, $name);", + callOptions..., + ) + + return xerrors.WithStackTrace(err) +} + +// -- name: UpsertSecondaryUser :exec +func (q *Queries) UpsertSecondaryUser(ctx context.Context, arg UpsertSecondaryUserParams, opts ...query.ExecuteOption) error { + parameters := ydb.ParamsBuilder() + parameters = parameters.Param("$id").Uint64(arg.ID) + parameters = parameters.Param("$name").Text(arg.Name) + + callOptions := append([]query.ExecuteOption(nil), opts...) + callOptions = append(callOptions, query.WithParameters(parameters.Build())) + + err := q.db.Exec(ctx, ""+ + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n"+ + "DECLARE $id AS Uint64;\n"+ + "DECLARE $name AS Utf8;\n"+ + "UPSERT INTO users (id, name) VALUES ($id, $name);", + callOptions..., + ) + + return xerrors.WithStackTrace(err) +} + +// -- name: GetPrimaryUser :one +func (q *Queries) GetPrimaryUser(ctx context.Context, arg uint64, opts ...query.ExecuteOption) (GetPrimaryUserRow, error) { + parameters := ydb.ParamsBuilder() + parameters = parameters.Param("$id").Uint64(arg) + + callOptions := append([]query.ExecuteOption(nil), opts...) + callOptions = append(callOptions, query.WithParameters(parameters.Build())) + + result, err := q.db.QueryRow(ctx, ""+ + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n"+ + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;", + callOptions..., + ) + if err != nil { + return GetPrimaryUserRow{}, xerrors.WithStackTrace(err) + } + + var row GetPrimaryUserRow + if err := result.ScanNamed( + query.Named("id", &row.ID), + query.Named("name", &row.Name), + ); err != nil { + return GetPrimaryUserRow{}, xerrors.WithStackTrace(err) + } + + return row, nil +} + +// -- name: GetSecondaryUser :one +func (q *Queries) GetSecondaryUser(ctx context.Context, arg uint64, opts ...query.ExecuteOption) (GetSecondaryUserRow, error) { + parameters := ydb.ParamsBuilder() + parameters = parameters.Param("$id").Uint64(arg) + + callOptions := append([]query.ExecuteOption(nil), opts...) + callOptions = append(callOptions, query.WithParameters(parameters.Build())) + + result, err := q.db.QueryRow(ctx, ""+ + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n"+ + "DECLARE $id AS Uint64;\n"+ + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;", + callOptions..., + ) + if err != nil { + return GetSecondaryUserRow{}, xerrors.WithStackTrace(err) + } + + var row GetSecondaryUserRow + if err := result.ScanNamed( + query.Named("id", &row.ID), + query.Named("name", &row.Name), + ); err != nil { + return GetSecondaryUserRow{}, xerrors.WithStackTrace(err) + } + + return row, nil +} + +// -- name: FindPrimaryUsersByName :many +func (q *Queries) FindPrimaryUsersByName(ctx context.Context, arg string, opts ...query.ExecuteOption) ([]FindPrimaryUsersByNameRow, error) { + parameters := ydb.ParamsBuilder() + parameters = parameters.Param("$name").Text(arg) + + callOptions := append([]query.ExecuteOption(nil), opts...) + callOptions = append(callOptions, query.WithParameters(parameters.Build())) + + result, err := q.db.Query(ctx, ""+ + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n"+ + "SELECT u.`id` AS `id`, `u`.`name` AS `name` FROM users VIEW by_name AS u WHERE u.name = $name ORDER BY u.id;", + callOptions..., + ) + if err != nil { + return nil, xerrors.WithStackTrace(err) + } + defer result.Close(ctx) + + resultSet, err := result.NextResultSet(ctx) + if errors.Is(err, io.EOF) { + return nil, xerrors.WithStackTrace(query.ErrNoResultSets) + } + if err != nil { + return nil, xerrors.WithStackTrace(err) + } + + items := make([]FindPrimaryUsersByNameRow, 0) + for r, err := range resultSet.Rows(ctx) { + if err != nil { + return nil, xerrors.WithStackTrace(err) + } + var row FindPrimaryUsersByNameRow + if err := r.ScanNamed( + query.Named("id", &row.ID), + query.Named("name", &row.Name), + ); err != nil { + return nil, xerrors.WithStackTrace(err) + } + items = append(items, row) + } + + _, err = result.NextResultSet(ctx) + if err == nil { + return nil, xerrors.WithStackTrace(query.ErrMoreThanOneResultSet) + } else if !errors.Is(err, io.EOF) { + return nil, xerrors.WithStackTrace(err) + } + + return items, nil +} + +// -- name: CompareUserNames :many +func (q *Queries) CompareUserNames(ctx context.Context, opts ...query.ExecuteOption) ([]CompareUserNamesRow, error) { + result, err := q.db.Query(ctx, ""+ + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n"+ + "SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name\n"+ + "FROM users AS a\n"+ + "LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id\n"+ + "ORDER BY a.id;", + opts..., + ) + if err != nil { + return nil, xerrors.WithStackTrace(err) + } + defer result.Close(ctx) + + resultSet, err := result.NextResultSet(ctx) + if errors.Is(err, io.EOF) { + return nil, xerrors.WithStackTrace(query.ErrNoResultSets) + } + if err != nil { + return nil, xerrors.WithStackTrace(err) + } + + items := make([]CompareUserNamesRow, 0) + for r, err := range resultSet.Rows(ctx) { + if err != nil { + return nil, xerrors.WithStackTrace(err) + } + var row CompareUserNamesRow + if err := r.ScanNamed( + query.Named("id", &row.ID), + query.Named("primary_name", &row.PrimaryName), + query.Named("secondary_name", &row.SecondaryName), + ); err != nil { + return nil, xerrors.WithStackTrace(err) + } + items = append(items, row) + } + + _, err = result.NextResultSet(ctx) + if err == nil { + return nil, xerrors.WithStackTrace(query.ErrMoreThanOneResultSet) + } else if !errors.Is(err, io.EOF) { + return nil, xerrors.WithStackTrace(err) + } + + return items, nil +} diff --git a/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/CompareUserNamesRow.java b/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/CompareUserNamesRow.java new file mode 100644 index 00000000..f9dd8301 --- /dev/null +++ b/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/CompareUserNamesRow.java @@ -0,0 +1,4 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jdbc; + +public record CompareUserNamesRow(long id, String primaryName, String secondaryName) {} diff --git a/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/FindPrimaryUsersByNameRow.java b/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/FindPrimaryUsersByNameRow.java new file mode 100644 index 00000000..a822f547 --- /dev/null +++ b/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/FindPrimaryUsersByNameRow.java @@ -0,0 +1,4 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jdbc; + +public record FindPrimaryUsersByNameRow(long id, String name) {} diff --git a/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/GetPrimaryUserRow.java b/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/GetPrimaryUserRow.java new file mode 100644 index 00000000..9ee108fb --- /dev/null +++ b/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/GetPrimaryUserRow.java @@ -0,0 +1,4 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jdbc; + +public record GetPrimaryUserRow(long id, String name) {} diff --git a/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/GetSecondaryUserRow.java b/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/GetSecondaryUserRow.java new file mode 100644 index 00000000..14bd261f --- /dev/null +++ b/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/GetSecondaryUserRow.java @@ -0,0 +1,4 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jdbc; + +public record GetSecondaryUserRow(long id, String name) {} diff --git a/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/LocalSqlcNamespacesAUsers.java b/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/LocalSqlcNamespacesAUsers.java new file mode 100644 index 00000000..984cd693 --- /dev/null +++ b/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/LocalSqlcNamespacesAUsers.java @@ -0,0 +1,4 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jdbc; + +public record LocalSqlcNamespacesAUsers(long id, String name) {} diff --git a/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/LocalSqlcNamespacesBUsers.java b/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/LocalSqlcNamespacesBUsers.java new file mode 100644 index 00000000..3376e933 --- /dev/null +++ b/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/LocalSqlcNamespacesBUsers.java @@ -0,0 +1,4 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jdbc; + +public record LocalSqlcNamespacesBUsers(long id, String name) {} diff --git a/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/Queries.java b/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/Queries.java new file mode 100644 index 00000000..56017868 --- /dev/null +++ b/examples/namespaces/java/jdbc/src/main/java/namespaces/jdbc/Queries.java @@ -0,0 +1,115 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jdbc; + +import tech.ydb.table.values.PrimitiveValue; + +// The caller owns the injected client and its lifecycle. +public final class Queries { + private final java.sql.Connection client; + + public Queries(java.sql.Connection client) { + this.client = java.util.Objects.requireNonNull(client); + } + + // -- name: UpsertPrimaryUser :exec + public void upsertPrimaryUser(long id, String name) throws java.sql.SQLException { + try (var _prepared = client.unwrap(tech.ydb.jdbc.YdbConnection.class).prepareStatement(""" + PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\"); + DECLARE $id AS Uint64; + DECLARE $name AS Utf8; + UPSERT INTO users (id, name) VALUES ($id, $name);\ + """, tech.ydb.jdbc.YdbPrepareMode.DATA_QUERY)) { + _prepared.setObject("id", PrimitiveValue.newUint64(id)); + _prepared.setString("name", name); + _prepared.execute(); + } + } + + // -- name: UpsertSecondaryUser :exec + public void upsertSecondaryUser(long id, String name) throws java.sql.SQLException { + try (var _prepared = client.unwrap(tech.ydb.jdbc.YdbConnection.class).prepareStatement(""" + PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\"); + DECLARE $id AS Uint64; + DECLARE $name AS Utf8; + UPSERT INTO users (id, name) VALUES ($id, $name);\ + """, tech.ydb.jdbc.YdbPrepareMode.DATA_QUERY)) { + _prepared.setObject("id", PrimitiveValue.newUint64(id)); + _prepared.setString("name", name); + _prepared.execute(); + } + } + + // -- name: GetPrimaryUser :one + public java.util.Optional getPrimaryUser(long id) throws java.sql.SQLException { + try (var _prepared = client.prepareStatement(""" + PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\"); + SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = ?;\ + """)) { + _prepared.setObject(1, PrimitiveValue.newUint64(id)); + try (var _rows = _prepared.executeQuery()) { + if (!_rows.next()) return java.util.Optional.empty(); + long _value0 = _rows.getLong(1); + String _value1 = _rows.getString(2); + return java.util.Optional.of(new GetPrimaryUserRow(_value0, _value1)); + } + } + } + + // -- name: GetSecondaryUser :one + public java.util.Optional getSecondaryUser(long id) throws java.sql.SQLException { + try (var _prepared = client.unwrap(tech.ydb.jdbc.YdbConnection.class).prepareStatement(""" + PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\"); + DECLARE $id AS Uint64; + SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;\ + """, tech.ydb.jdbc.YdbPrepareMode.DATA_QUERY)) { + _prepared.setObject("id", PrimitiveValue.newUint64(id)); + try (var _rows = _prepared.executeQuery()) { + if (!_rows.next()) return java.util.Optional.empty(); + long _value0 = _rows.getLong(1); + String _value1 = _rows.getString(2); + return java.util.Optional.of(new GetSecondaryUserRow(_value0, _value1)); + } + } + } + + // -- name: FindPrimaryUsersByName :many + public java.util.List findPrimaryUsersByName(String name) throws java.sql.SQLException { + try (var _prepared = client.prepareStatement(""" + PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\"); + SELECT u.`id` AS `id`, `u`.`name` AS `name` FROM users VIEW by_name AS u WHERE u.name = ? ORDER BY u.id;\ + """)) { + _prepared.setString(1, name); + try (var _rows = _prepared.executeQuery()) { + var _items = new java.util.ArrayList(); + while (_rows.next()) { + long _value0 = _rows.getLong(1); + String _value1 = _rows.getString(2); + _items.add(new FindPrimaryUsersByNameRow(_value0, _value1)); + } + return _items; + } + } + } + + // -- name: CompareUserNames :many + public java.util.List compareUserNames() throws java.sql.SQLException { + try (var _prepared = client.prepareStatement(""" + PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\"); + SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name + FROM users AS a + LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id + ORDER BY a.id;\ + """)) { + try (var _rows = _prepared.executeQuery()) { + var _items = new java.util.ArrayList(); + while (_rows.next()) { + long _value0 = _rows.getLong(1); + String _value1 = _rows.getString(2); + String _value2 = _rows.getString(3); + _items.add(new CompareUserNamesRow(_value0, _value1, _value2)); + } + return _items; + } + } + } +} diff --git a/examples/namespaces/java/jooq/CompareUserNamesRow.java b/examples/namespaces/java/jooq/CompareUserNamesRow.java new file mode 100644 index 00000000..ead9b048 --- /dev/null +++ b/examples/namespaces/java/jooq/CompareUserNamesRow.java @@ -0,0 +1,6 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jooq; + +import org.jooq.types.ULong; + +public record CompareUserNamesRow(ULong id, String primaryName, String secondaryName) {} diff --git a/examples/namespaces/java/jooq/FindPrimaryUsersByNameRow.java b/examples/namespaces/java/jooq/FindPrimaryUsersByNameRow.java new file mode 100644 index 00000000..1a144103 --- /dev/null +++ b/examples/namespaces/java/jooq/FindPrimaryUsersByNameRow.java @@ -0,0 +1,6 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jooq; + +import org.jooq.types.ULong; + +public record FindPrimaryUsersByNameRow(ULong id, String name) {} diff --git a/examples/namespaces/java/jooq/GetPrimaryUserRow.java b/examples/namespaces/java/jooq/GetPrimaryUserRow.java new file mode 100644 index 00000000..10144cc8 --- /dev/null +++ b/examples/namespaces/java/jooq/GetPrimaryUserRow.java @@ -0,0 +1,6 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jooq; + +import org.jooq.types.ULong; + +public record GetPrimaryUserRow(ULong id, String name) {} diff --git a/examples/namespaces/java/jooq/GetSecondaryUserRow.java b/examples/namespaces/java/jooq/GetSecondaryUserRow.java new file mode 100644 index 00000000..7137cfc9 --- /dev/null +++ b/examples/namespaces/java/jooq/GetSecondaryUserRow.java @@ -0,0 +1,6 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jooq; + +import org.jooq.types.ULong; + +public record GetSecondaryUserRow(ULong id, String name) {} diff --git a/examples/namespaces/java/jooq/Queries.java b/examples/namespaces/java/jooq/Queries.java new file mode 100644 index 00000000..7a51e327 --- /dev/null +++ b/examples/namespaces/java/jooq/Queries.java @@ -0,0 +1,126 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jooq; + +import org.jooq.types.ULong; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.jooq.impl.SQLDataType; +import tech.ydb.jooq.YdbDSLContext; +import tech.ydb.jooq.YdbTypes; + +import static org.jooq.impl.DSL.*; +import static org.jooq.Records.mapping; +import static namespaces.jooq.Tables.*; + +public final class Queries { + private final YdbDSLContext dsl; + + public Queries(YdbDSLContext dsl) { + this.dsl = Objects.requireNonNull(dsl); + } + + // -- name: UpsertPrimaryUser :exec + public void upsertPrimaryUser(ULong id, String name) { + dsl.connection(_connection -> { + try (var _prepared = _connection.unwrap(tech.ydb.jdbc.YdbConnection.class).prepareStatement(""" + PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\"); + DECLARE $id AS Uint64; + DECLARE $name AS Utf8; + UPSERT INTO\s\ + """ + dsl.render(LOCAL_SQLC_NAMESPACES_A_USERS) + """ + (id, name) VALUES ($id, $name);\ + """, tech.ydb.jdbc.YdbPrepareMode.DATA_QUERY)) { + _prepared.setObject("id", tech.ydb.table.values.PrimitiveValue.newUint64(id.longValue())); + _prepared.setString("name", name); + _prepared.execute(); + } + }); + } + + // -- name: UpsertSecondaryUser :exec + public void upsertSecondaryUser(ULong id, String name) { + dsl.connection(_connection -> { + try (var _prepared = _connection.unwrap(tech.ydb.jdbc.YdbConnection.class).prepareStatement(""" + PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\"); + DECLARE $id AS Uint64; + DECLARE $name AS Utf8; + UPSERT INTO\s\ + """ + dsl.render(LOCAL_SQLC_NAMESPACES_B_USERS) + """ + (id, name) VALUES ($id, $name);\ + """, tech.ydb.jdbc.YdbPrepareMode.DATA_QUERY)) { + _prepared.setObject("id", tech.ydb.table.values.PrimitiveValue.newUint64(id.longValue())); + _prepared.setString("name", name); + _prepared.execute(); + } + }); + } + + // -- name: GetPrimaryUser :one + public Optional getPrimaryUser(ULong id) { + var users = LOCAL_SQLC_NAMESPACES_A_USERS.as("users"); + var stmt = dsl.select(users.ID.as("id"), users.NAME.as("name")) + .from(users) + .where(users.ID.eq(val(id, YdbTypes.UINT64))); + + return dsl.resultQuery("{0};\n{1}", sql(""" + PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\")\ + """), stmt) + .coerce(field(name("id"), YdbTypes.UINT64), field(name("name"), YdbTypes.UTF8)) + .fetchOptional(mapping(GetPrimaryUserRow::new)); + } + + // -- name: GetSecondaryUser :one + public Optional getSecondaryUser(ULong id) { + return dsl.connectionResult(_connection -> { + try (var _prepared = _connection.unwrap(tech.ydb.jdbc.YdbConnection.class).prepareStatement(""" + PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\"); + DECLARE $id AS Uint64; + SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM\s\ + """ + dsl.render(LOCAL_SQLC_NAMESPACES_B_USERS) + " AS `users`" + """ + WHERE id = $id;\ + """, tech.ydb.jdbc.YdbPrepareMode.DATA_QUERY)) { + _prepared.setObject("id", tech.ydb.table.values.PrimitiveValue.newUint64(id.longValue())); + try (var _rows = _prepared.executeQuery()) { + var _result = dsl.fetch(_rows, YdbTypes.UINT64, YdbTypes.UTF8).map(_row -> new GetSecondaryUserRow(_row.get(0, org.jooq.types.ULong.class), _row.get(1, String.class))); + if (_result.size() > 1) throw new org.jooq.exception.TooManyRowsException("Expected at most one row"); + return _result.stream().findFirst(); + } + } + }); + } + + // -- name: FindPrimaryUsersByName :many + public List findPrimaryUsersByName(String name) { + var u = LOCAL_SQLC_NAMESPACES_A_USERS.as("u"); + var stmt = dsl.select(u.ID.as("id"), u.NAME.as("name")) + .from(table("{0} VIEW {1}", LOCAL_SQLC_NAMESPACES_A_USERS, name("by_name")).as("u")) + .where(u.NAME.eq(val(name, YdbTypes.UTF8))) + .orderBy(u.ID); + + return dsl.resultQuery("{0};\n{1}", sql(""" + PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\")\ + """), stmt) + .coerce(field(name("id"), YdbTypes.UINT64), field(name("name"), YdbTypes.UTF8)) + .fetch(mapping(FindPrimaryUsersByNameRow::new)); + } + + // -- name: CompareUserNames :many + public List compareUserNames() { + var a = LOCAL_SQLC_NAMESPACES_A_USERS.as("a"); + var b = LOCAL_SQLC_NAMESPACES_B_USERS.as("b"); + var stmt = dsl.select(a.ID.as("id"), a.NAME.as("primary_name"), b.NAME.as("secondary_name")) + .from(a) + .leftJoin(b) + .on(a.ID.eq(b.ID)) + .orderBy(a.ID); + + return dsl.resultQuery("{0};\n{1}", sql(""" + PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\")\ + """), stmt) + .coerce(field(name("id"), YdbTypes.UINT64), field(name("primary_name"), YdbTypes.UTF8), field(name("secondary_name"), YdbTypes.UTF8)) + .fetch(mapping(CompareUserNamesRow::new)); + } +} diff --git a/examples/namespaces/java/jooq/Tables.java b/examples/namespaces/java/jooq/Tables.java new file mode 100644 index 00000000..2775ce8b --- /dev/null +++ b/examples/namespaces/java/jooq/Tables.java @@ -0,0 +1,48 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jooq; + +import org.jooq.types.ULong; + +import org.jooq.Record; +import org.jooq.Table; +import org.jooq.TableField; +import org.jooq.impl.TableImpl; +import tech.ydb.jooq.YdbTypes; + +import static org.jooq.impl.DSL.name; + +public final class Tables { + private Tables() {} + + public static final LocalSqlcNamespacesAUsersTable LOCAL_SQLC_NAMESPACES_A_USERS = new LocalSqlcNamespacesAUsersTable("/local/sqlc_namespaces/a/users", null); + + public static final class LocalSqlcNamespacesAUsersTable extends TableImpl { + public final TableField ID = createField(name("id"), YdbTypes.UINT64.nullable(false), this); + public final TableField NAME = createField(name("name"), YdbTypes.UTF8.nullable(false), this); + + private LocalSqlcNamespacesAUsersTable(String name, Table aliased) { + super(name(name), null, aliased); + } + + @Override + public LocalSqlcNamespacesAUsersTable as(String alias) { + return new LocalSqlcNamespacesAUsersTable(alias, this); + } + } + + public static final LocalSqlcNamespacesBUsersTable LOCAL_SQLC_NAMESPACES_B_USERS = new LocalSqlcNamespacesBUsersTable("/local/sqlc_namespaces/b/users", null); + + public static final class LocalSqlcNamespacesBUsersTable extends TableImpl { + public final TableField ID = createField(name("id"), YdbTypes.UINT64.nullable(false), this); + public final TableField NAME = createField(name("name"), YdbTypes.UTF8.nullable(false), this); + + private LocalSqlcNamespacesBUsersTable(String name, Table aliased) { + super(name(name), null, aliased); + } + + @Override + public LocalSqlcNamespacesBUsersTable as(String alias) { + return new LocalSqlcNamespacesBUsersTable(alias, this); + } + } +} diff --git a/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/CompareUserNamesRow.java b/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/CompareUserNamesRow.java new file mode 100644 index 00000000..d3b95d1f --- /dev/null +++ b/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/CompareUserNamesRow.java @@ -0,0 +1,4 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.nativeapi; + +public record CompareUserNamesRow(long id, String primaryName, String secondaryName) {} diff --git a/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/FindPrimaryUsersByNameRow.java b/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/FindPrimaryUsersByNameRow.java new file mode 100644 index 00000000..302132a1 --- /dev/null +++ b/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/FindPrimaryUsersByNameRow.java @@ -0,0 +1,4 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.nativeapi; + +public record FindPrimaryUsersByNameRow(long id, String name) {} diff --git a/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/GetPrimaryUserRow.java b/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/GetPrimaryUserRow.java new file mode 100644 index 00000000..5dca36f5 --- /dev/null +++ b/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/GetPrimaryUserRow.java @@ -0,0 +1,4 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.nativeapi; + +public record GetPrimaryUserRow(long id, String name) {} diff --git a/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/GetSecondaryUserRow.java b/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/GetSecondaryUserRow.java new file mode 100644 index 00000000..ec8f32a5 --- /dev/null +++ b/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/GetSecondaryUserRow.java @@ -0,0 +1,4 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.nativeapi; + +public record GetSecondaryUserRow(long id, String name) {} diff --git a/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/LocalSqlcNamespacesAUsers.java b/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/LocalSqlcNamespacesAUsers.java new file mode 100644 index 00000000..5442f99d --- /dev/null +++ b/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/LocalSqlcNamespacesAUsers.java @@ -0,0 +1,4 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.nativeapi; + +public record LocalSqlcNamespacesAUsers(long id, String name) {} diff --git a/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/LocalSqlcNamespacesBUsers.java b/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/LocalSqlcNamespacesBUsers.java new file mode 100644 index 00000000..66fa2e3b --- /dev/null +++ b/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/LocalSqlcNamespacesBUsers.java @@ -0,0 +1,4 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.nativeapi; + +public record LocalSqlcNamespacesBUsers(long id, String name) {} diff --git a/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/Queries.java b/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/Queries.java new file mode 100644 index 00000000..5c8f008d --- /dev/null +++ b/examples/namespaces/java/native/src/main/java/namespaces/nativeapi/Queries.java @@ -0,0 +1,120 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.nativeapi; + +import tech.ydb.query.QueryTransaction; +import tech.ydb.query.tools.QueryReader; +import tech.ydb.table.query.Params; +import tech.ydb.table.values.PrimitiveValue; + +// The caller owns the injected client and its lifecycle. +public final class Queries { + private final QueryTransaction client; + + public Queries(QueryTransaction client) { + this.client = java.util.Objects.requireNonNull(client); + } + + // -- name: UpsertPrimaryUser :exec + public void upsertPrimaryUser(long id, String name) { + var _params = Params.create(); + _params.put("$id", PrimitiveValue.newUint64(id)); + _params.put("$name", PrimitiveValue.newText(name)); + client.createQuery(""" + PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\"); + DECLARE $id AS Uint64; + DECLARE $name AS Utf8; + UPSERT INTO users (id, name) VALUES ($id, $name);\ + """, _params).execute().join().getStatus().expectSuccess(); + } + + // -- name: UpsertSecondaryUser :exec + public void upsertSecondaryUser(long id, String name) { + var _params = Params.create(); + _params.put("$id", PrimitiveValue.newUint64(id)); + _params.put("$name", PrimitiveValue.newText(name)); + client.createQuery(""" + PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\"); + DECLARE $id AS Uint64; + DECLARE $name AS Utf8; + UPSERT INTO users (id, name) VALUES ($id, $name);\ + """, _params).execute().join().getStatus().expectSuccess(); + } + + // -- name: GetPrimaryUser :one + public java.util.Optional getPrimaryUser(long id) { + var _params = Params.create(); + _params.put("$id", PrimitiveValue.newUint64(id)); + var _query = QueryReader.readFrom( + client.createQuery(""" + PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\"); + SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;\ + """, _params)).join().getValue(); + if (_query.getResultSetCount() != 1) throw new IllegalStateException("Expected one result set"); + var _rows = _query.getResultSet(0); + if (!_rows.next()) return java.util.Optional.empty(); + long _value0 = _rows.getColumn(0).getUint64(); + String _value1 = _rows.getColumn(1).getText(); + return java.util.Optional.of(new GetPrimaryUserRow(_value0, _value1)); + } + + // -- name: GetSecondaryUser :one + public java.util.Optional getSecondaryUser(long id) { + var _params = Params.create(); + _params.put("$id", PrimitiveValue.newUint64(id)); + var _query = QueryReader.readFrom( + client.createQuery(""" + PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\"); + DECLARE $id AS Uint64; + SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;\ + """, _params)).join().getValue(); + if (_query.getResultSetCount() != 1) throw new IllegalStateException("Expected one result set"); + var _rows = _query.getResultSet(0); + if (!_rows.next()) return java.util.Optional.empty(); + long _value0 = _rows.getColumn(0).getUint64(); + String _value1 = _rows.getColumn(1).getText(); + return java.util.Optional.of(new GetSecondaryUserRow(_value0, _value1)); + } + + // -- name: FindPrimaryUsersByName :many + public java.util.List findPrimaryUsersByName(String name) { + var _params = Params.create(); + _params.put("$name", PrimitiveValue.newText(name)); + var _query = QueryReader.readFrom( + client.createQuery(""" + PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\"); + SELECT u.`id` AS `id`, `u`.`name` AS `name` FROM users VIEW by_name AS u WHERE u.name = $name ORDER BY u.id;\ + """, _params)).join().getValue(); + if (_query.getResultSetCount() != 1) throw new IllegalStateException("Expected one result set"); + var _rows = _query.getResultSet(0); + var _items = new java.util.ArrayList(); + while (_rows.next()) { + long _value0 = _rows.getColumn(0).getUint64(); + String _value1 = _rows.getColumn(1).getText(); + _items.add(new FindPrimaryUsersByNameRow(_value0, _value1)); + } + return _items; + } + + // -- name: CompareUserNames :many + public java.util.List compareUserNames() { + var _params = Params.create(); + var _query = QueryReader.readFrom( + client.createQuery(""" + PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\"); + SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name + FROM users AS a + LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id + ORDER BY a.id;\ + """, _params)).join().getValue(); + if (_query.getResultSetCount() != 1) throw new IllegalStateException("Expected one result set"); + var _rows = _query.getResultSet(0); + var _items = new java.util.ArrayList(); + while (_rows.next()) { + long _value0 = _rows.getColumn(0).getUint64(); + String _value1 = _rows.getColumn(1).getText(); + String _value2 = _rows.getColumn(2).getText(); + _items.add(new CompareUserNamesRow(_value0, _value1, _value2)); + } + return _items; + } +} diff --git a/examples/namespaces/kotlin/exposed/CompareUserNamesRow.kt b/examples/namespaces/kotlin/exposed/CompareUserNamesRow.kt new file mode 100644 index 00000000..59f53543 --- /dev/null +++ b/examples/namespaces/kotlin/exposed/CompareUserNamesRow.kt @@ -0,0 +1,8 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.exposed + +data class CompareUserNamesRow( + val id: Long, + val primaryName: String, + val secondaryName: String? +) diff --git a/examples/namespaces/kotlin/exposed/FindPrimaryUsersByNameRow.kt b/examples/namespaces/kotlin/exposed/FindPrimaryUsersByNameRow.kt new file mode 100644 index 00000000..7ab5c154 --- /dev/null +++ b/examples/namespaces/kotlin/exposed/FindPrimaryUsersByNameRow.kt @@ -0,0 +1,7 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.exposed + +data class FindPrimaryUsersByNameRow( + val id: Long, + val name: String +) diff --git a/examples/namespaces/kotlin/exposed/GetPrimaryUserRow.kt b/examples/namespaces/kotlin/exposed/GetPrimaryUserRow.kt new file mode 100644 index 00000000..9c023ac2 --- /dev/null +++ b/examples/namespaces/kotlin/exposed/GetPrimaryUserRow.kt @@ -0,0 +1,7 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.exposed + +data class GetPrimaryUserRow( + val id: Long, + val name: String +) diff --git a/examples/namespaces/kotlin/exposed/GetSecondaryUserRow.kt b/examples/namespaces/kotlin/exposed/GetSecondaryUserRow.kt new file mode 100644 index 00000000..a2630c6b --- /dev/null +++ b/examples/namespaces/kotlin/exposed/GetSecondaryUserRow.kt @@ -0,0 +1,7 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.exposed + +data class GetSecondaryUserRow( + val id: Long, + val name: String +) diff --git a/examples/namespaces/kotlin/exposed/LocalSqlcNamespacesAUsers.kt b/examples/namespaces/kotlin/exposed/LocalSqlcNamespacesAUsers.kt new file mode 100644 index 00000000..cf61f3dd --- /dev/null +++ b/examples/namespaces/kotlin/exposed/LocalSqlcNamespacesAUsers.kt @@ -0,0 +1,7 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.exposed + +data class LocalSqlcNamespacesAUsers( + val id: Long, + val name: String +) diff --git a/examples/namespaces/kotlin/exposed/LocalSqlcNamespacesBUsers.kt b/examples/namespaces/kotlin/exposed/LocalSqlcNamespacesBUsers.kt new file mode 100644 index 00000000..4194548b --- /dev/null +++ b/examples/namespaces/kotlin/exposed/LocalSqlcNamespacesBUsers.kt @@ -0,0 +1,7 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.exposed + +data class LocalSqlcNamespacesBUsers( + val id: Long, + val name: String +) diff --git a/examples/namespaces/kotlin/exposed/Queries.kt b/examples/namespaces/kotlin/exposed/Queries.kt new file mode 100644 index 00000000..c8cced0e --- /dev/null +++ b/examples/namespaces/kotlin/exposed/Queries.kt @@ -0,0 +1,112 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.exposed + +import tech.ydb.table.values.PrimitiveValue +import tech.ydb.table.values.PrimitiveType +import tech.ydb.table.values.OptionalType + +// The caller owns the injected client and its lifecycle. +class Queries(private val client: org.jetbrains.exposed.v1.jdbc.JdbcTransaction) { + + // -- name: UpsertPrimaryUser :exec + fun upsertPrimaryUser(id: Long, name: String): Unit { + val _connection = client.connection.connection as java.sql.Connection + _connection.unwrap(tech.ydb.jdbc.YdbConnection::class.java).prepareStatement( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "DECLARE \$id AS Uint64;\n" + + "DECLARE \$name AS Utf8;\n" + + "UPSERT INTO users (id, name) VALUES (\$id, \$name);", tech.ydb.jdbc.YdbPrepareMode.DATA_QUERY).use { _prepared -> + _prepared.setObject("id", PrimitiveValue.newUint64(id)) + _prepared.setString("name", name) + _prepared.execute() + } + } + + // -- name: UpsertSecondaryUser :exec + fun upsertSecondaryUser(id: Long, name: String): Unit { + val _connection = client.connection.connection as java.sql.Connection + _connection.unwrap(tech.ydb.jdbc.YdbConnection::class.java).prepareStatement( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + + "DECLARE \$id AS Uint64;\n" + + "DECLARE \$name AS Utf8;\n" + + "UPSERT INTO users (id, name) VALUES (\$id, \$name);", tech.ydb.jdbc.YdbPrepareMode.DATA_QUERY).use { _prepared -> + _prepared.setObject("id", PrimitiveValue.newUint64(id)) + _prepared.setString("name", name) + _prepared.execute() + } + } + + // -- name: GetPrimaryUser :one + fun getPrimaryUser(id: Long): GetPrimaryUserRow? { + val _connection = client.connection.connection as java.sql.Connection + _connection.prepareStatement( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = ?;").use { _prepared -> + _prepared.setObject(1, PrimitiveValue.newUint64(id)) + _prepared.executeQuery().use { _rows -> + if (!_rows.next()) return null + val _value0: Long = _rows.getLong(1) + val _value1: String = _rows.getString(2) + return GetPrimaryUserRow(_value0, _value1) + } + } + } + + // -- name: GetSecondaryUser :one + fun getSecondaryUser(id: Long): GetSecondaryUserRow? { + val _connection = client.connection.connection as java.sql.Connection + _connection.unwrap(tech.ydb.jdbc.YdbConnection::class.java).prepareStatement( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + + "DECLARE \$id AS Uint64;\n" + + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = \$id;", tech.ydb.jdbc.YdbPrepareMode.DATA_QUERY).use { _prepared -> + _prepared.setObject("id", PrimitiveValue.newUint64(id)) + _prepared.executeQuery().use { _rows -> + if (!_rows.next()) return null + val _value0: Long = _rows.getLong(1) + val _value1: String = _rows.getString(2) + return GetSecondaryUserRow(_value0, _value1) + } + } + } + + // -- name: FindPrimaryUsersByName :many + fun findPrimaryUsersByName(name: String): List { + val _connection = client.connection.connection as java.sql.Connection + _connection.prepareStatement( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "SELECT u.`id` AS `id`, `u`.`name` AS `name` FROM users VIEW by_name AS u WHERE u.name = ? ORDER BY u.id;").use { _prepared -> + _prepared.setString(1, name) + _prepared.executeQuery().use { _rows -> + val _items = ArrayList() + while (_rows.next()) { + val _value0: Long = _rows.getLong(1) + val _value1: String = _rows.getString(2) + _items.add(FindPrimaryUsersByNameRow(_value0, _value1)) + } + return _items + } + } + } + + // -- name: CompareUserNames :many + fun compareUserNames(): List { + val _connection = client.connection.connection as java.sql.Connection + _connection.prepareStatement( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name\n" + + "FROM users AS a\n" + + "LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id\n" + + "ORDER BY a.id;").use { _prepared -> + _prepared.executeQuery().use { _rows -> + val _items = ArrayList() + while (_rows.next()) { + val _value0: Long = _rows.getLong(1) + val _value1: String = _rows.getString(2) + val _value2: String? = _rows.getString(3) + _items.add(CompareUserNamesRow(_value0, _value1, _value2)) + } + return _items + } + } + } +} diff --git a/examples/namespaces/kotlin/jdbc/CompareUserNamesRow.kt b/examples/namespaces/kotlin/jdbc/CompareUserNamesRow.kt new file mode 100644 index 00000000..7215a7b6 --- /dev/null +++ b/examples/namespaces/kotlin/jdbc/CompareUserNamesRow.kt @@ -0,0 +1,8 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jdbc + +data class CompareUserNamesRow( + val id: Long, + val primaryName: String, + val secondaryName: String? +) diff --git a/examples/namespaces/kotlin/jdbc/FindPrimaryUsersByNameRow.kt b/examples/namespaces/kotlin/jdbc/FindPrimaryUsersByNameRow.kt new file mode 100644 index 00000000..3680a1f6 --- /dev/null +++ b/examples/namespaces/kotlin/jdbc/FindPrimaryUsersByNameRow.kt @@ -0,0 +1,7 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jdbc + +data class FindPrimaryUsersByNameRow( + val id: Long, + val name: String +) diff --git a/examples/namespaces/kotlin/jdbc/GetPrimaryUserRow.kt b/examples/namespaces/kotlin/jdbc/GetPrimaryUserRow.kt new file mode 100644 index 00000000..5bf445be --- /dev/null +++ b/examples/namespaces/kotlin/jdbc/GetPrimaryUserRow.kt @@ -0,0 +1,7 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jdbc + +data class GetPrimaryUserRow( + val id: Long, + val name: String +) diff --git a/examples/namespaces/kotlin/jdbc/GetSecondaryUserRow.kt b/examples/namespaces/kotlin/jdbc/GetSecondaryUserRow.kt new file mode 100644 index 00000000..33ecbeb4 --- /dev/null +++ b/examples/namespaces/kotlin/jdbc/GetSecondaryUserRow.kt @@ -0,0 +1,7 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jdbc + +data class GetSecondaryUserRow( + val id: Long, + val name: String +) diff --git a/examples/namespaces/kotlin/jdbc/LocalSqlcNamespacesAUsers.kt b/examples/namespaces/kotlin/jdbc/LocalSqlcNamespacesAUsers.kt new file mode 100644 index 00000000..9726d90a --- /dev/null +++ b/examples/namespaces/kotlin/jdbc/LocalSqlcNamespacesAUsers.kt @@ -0,0 +1,7 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jdbc + +data class LocalSqlcNamespacesAUsers( + val id: Long, + val name: String +) diff --git a/examples/namespaces/kotlin/jdbc/LocalSqlcNamespacesBUsers.kt b/examples/namespaces/kotlin/jdbc/LocalSqlcNamespacesBUsers.kt new file mode 100644 index 00000000..f8ffb01a --- /dev/null +++ b/examples/namespaces/kotlin/jdbc/LocalSqlcNamespacesBUsers.kt @@ -0,0 +1,7 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jdbc + +data class LocalSqlcNamespacesBUsers( + val id: Long, + val name: String +) diff --git a/examples/namespaces/kotlin/jdbc/Queries.kt b/examples/namespaces/kotlin/jdbc/Queries.kt new file mode 100644 index 00000000..6876da46 --- /dev/null +++ b/examples/namespaces/kotlin/jdbc/Queries.kt @@ -0,0 +1,106 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.jdbc + +import tech.ydb.table.values.PrimitiveValue +import tech.ydb.table.values.PrimitiveType +import tech.ydb.table.values.OptionalType + +// The caller owns the injected client and its lifecycle. +class Queries(private val client: java.sql.Connection) { + + // -- name: UpsertPrimaryUser :exec + fun upsertPrimaryUser(id: Long, name: String): Unit { + client.unwrap(tech.ydb.jdbc.YdbConnection::class.java).prepareStatement( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "DECLARE \$id AS Uint64;\n" + + "DECLARE \$name AS Utf8;\n" + + "UPSERT INTO users (id, name) VALUES (\$id, \$name);", tech.ydb.jdbc.YdbPrepareMode.DATA_QUERY).use { _prepared -> + _prepared.setObject("id", PrimitiveValue.newUint64(id)) + _prepared.setString("name", name) + _prepared.execute() + } + } + + // -- name: UpsertSecondaryUser :exec + fun upsertSecondaryUser(id: Long, name: String): Unit { + client.unwrap(tech.ydb.jdbc.YdbConnection::class.java).prepareStatement( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + + "DECLARE \$id AS Uint64;\n" + + "DECLARE \$name AS Utf8;\n" + + "UPSERT INTO users (id, name) VALUES (\$id, \$name);", tech.ydb.jdbc.YdbPrepareMode.DATA_QUERY).use { _prepared -> + _prepared.setObject("id", PrimitiveValue.newUint64(id)) + _prepared.setString("name", name) + _prepared.execute() + } + } + + // -- name: GetPrimaryUser :one + fun getPrimaryUser(id: Long): GetPrimaryUserRow? { + client.prepareStatement( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = ?;").use { _prepared -> + _prepared.setObject(1, PrimitiveValue.newUint64(id)) + _prepared.executeQuery().use { _rows -> + if (!_rows.next()) return null + val _value0: Long = _rows.getLong(1) + val _value1: String = _rows.getString(2) + return GetPrimaryUserRow(_value0, _value1) + } + } + } + + // -- name: GetSecondaryUser :one + fun getSecondaryUser(id: Long): GetSecondaryUserRow? { + client.unwrap(tech.ydb.jdbc.YdbConnection::class.java).prepareStatement( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + + "DECLARE \$id AS Uint64;\n" + + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = \$id;", tech.ydb.jdbc.YdbPrepareMode.DATA_QUERY).use { _prepared -> + _prepared.setObject("id", PrimitiveValue.newUint64(id)) + _prepared.executeQuery().use { _rows -> + if (!_rows.next()) return null + val _value0: Long = _rows.getLong(1) + val _value1: String = _rows.getString(2) + return GetSecondaryUserRow(_value0, _value1) + } + } + } + + // -- name: FindPrimaryUsersByName :many + fun findPrimaryUsersByName(name: String): List { + client.prepareStatement( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "SELECT u.`id` AS `id`, `u`.`name` AS `name` FROM users VIEW by_name AS u WHERE u.name = ? ORDER BY u.id;").use { _prepared -> + _prepared.setString(1, name) + _prepared.executeQuery().use { _rows -> + val _items = ArrayList() + while (_rows.next()) { + val _value0: Long = _rows.getLong(1) + val _value1: String = _rows.getString(2) + _items.add(FindPrimaryUsersByNameRow(_value0, _value1)) + } + return _items + } + } + } + + // -- name: CompareUserNames :many + fun compareUserNames(): List { + client.prepareStatement( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name\n" + + "FROM users AS a\n" + + "LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id\n" + + "ORDER BY a.id;").use { _prepared -> + _prepared.executeQuery().use { _rows -> + val _items = ArrayList() + while (_rows.next()) { + val _value0: Long = _rows.getLong(1) + val _value1: String = _rows.getString(2) + val _value2: String? = _rows.getString(3) + _items.add(CompareUserNamesRow(_value0, _value1, _value2)) + } + return _items + } + } + } +} diff --git a/examples/namespaces/kotlin/nativeapi/CompareUserNamesRow.kt b/examples/namespaces/kotlin/nativeapi/CompareUserNamesRow.kt new file mode 100644 index 00000000..4b1efb51 --- /dev/null +++ b/examples/namespaces/kotlin/nativeapi/CompareUserNamesRow.kt @@ -0,0 +1,8 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.nativeapi + +data class CompareUserNamesRow( + val id: Long, + val primaryName: String, + val secondaryName: String? +) diff --git a/examples/namespaces/kotlin/nativeapi/FindPrimaryUsersByNameRow.kt b/examples/namespaces/kotlin/nativeapi/FindPrimaryUsersByNameRow.kt new file mode 100644 index 00000000..0fa52574 --- /dev/null +++ b/examples/namespaces/kotlin/nativeapi/FindPrimaryUsersByNameRow.kt @@ -0,0 +1,7 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.nativeapi + +data class FindPrimaryUsersByNameRow( + val id: Long, + val name: String +) diff --git a/examples/namespaces/kotlin/nativeapi/GetPrimaryUserRow.kt b/examples/namespaces/kotlin/nativeapi/GetPrimaryUserRow.kt new file mode 100644 index 00000000..4c1e172b --- /dev/null +++ b/examples/namespaces/kotlin/nativeapi/GetPrimaryUserRow.kt @@ -0,0 +1,7 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.nativeapi + +data class GetPrimaryUserRow( + val id: Long, + val name: String +) diff --git a/examples/namespaces/kotlin/nativeapi/GetSecondaryUserRow.kt b/examples/namespaces/kotlin/nativeapi/GetSecondaryUserRow.kt new file mode 100644 index 00000000..5877e769 --- /dev/null +++ b/examples/namespaces/kotlin/nativeapi/GetSecondaryUserRow.kt @@ -0,0 +1,7 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.nativeapi + +data class GetSecondaryUserRow( + val id: Long, + val name: String +) diff --git a/examples/namespaces/kotlin/nativeapi/LocalSqlcNamespacesAUsers.kt b/examples/namespaces/kotlin/nativeapi/LocalSqlcNamespacesAUsers.kt new file mode 100644 index 00000000..f2bb28fc --- /dev/null +++ b/examples/namespaces/kotlin/nativeapi/LocalSqlcNamespacesAUsers.kt @@ -0,0 +1,7 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.nativeapi + +data class LocalSqlcNamespacesAUsers( + val id: Long, + val name: String +) diff --git a/examples/namespaces/kotlin/nativeapi/LocalSqlcNamespacesBUsers.kt b/examples/namespaces/kotlin/nativeapi/LocalSqlcNamespacesBUsers.kt new file mode 100644 index 00000000..febfb4e1 --- /dev/null +++ b/examples/namespaces/kotlin/nativeapi/LocalSqlcNamespacesBUsers.kt @@ -0,0 +1,7 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.nativeapi + +data class LocalSqlcNamespacesBUsers( + val id: Long, + val name: String +) diff --git a/examples/namespaces/kotlin/nativeapi/Queries.kt b/examples/namespaces/kotlin/nativeapi/Queries.kt new file mode 100644 index 00000000..65415942 --- /dev/null +++ b/examples/namespaces/kotlin/nativeapi/Queries.kt @@ -0,0 +1,177 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +package namespaces.nativeapi + +import tech.ydb.query.QueryTransaction +import tech.ydb.query.tools.SessionRetryContext +import tech.ydb.query.tools.QueryReader +import tech.ydb.common.transaction.TxMode +import tech.ydb.table.query.Params +import tech.ydb.table.values.PrimitiveValue +import tech.ydb.table.values.PrimitiveType +import tech.ydb.table.values.OptionalType + +// The caller owns the injected executor and its lifecycle. +class Queries { + private val client: SessionRetryContext? + private val transaction: QueryTransaction? + + constructor(client: SessionRetryContext) { + this.client = client + this.transaction = null + } + + constructor(transaction: QueryTransaction) { + this.client = null + this.transaction = transaction + } + + // -- name: UpsertPrimaryUser :exec + fun upsertPrimaryUser(id: Long, name: String): Unit { + val _params = Params.create() + _params.put("\$id", PrimitiveValue.newUint64(id)) + _params.put("\$name", PrimitiveValue.newText(name)) + if (transaction != null) { + transaction.createQuery( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "DECLARE \$id AS Uint64;\n" + + "DECLARE \$name AS Utf8;\n" + + "UPSERT INTO users (id, name) VALUES (\$id, \$name);", _params).execute().join().getStatus().expectSuccess() + } else { + client!!.supplyResult { _session -> + _session.createQuery( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "DECLARE \$id AS Uint64;\n" + + "DECLARE \$name AS Utf8;\n" + + "UPSERT INTO users (id, name) VALUES (\$id, \$name);", TxMode.SERIALIZABLE_RW, _params).execute() + }.join().getStatus().expectSuccess() + } + } + + // -- name: UpsertSecondaryUser :exec + fun upsertSecondaryUser(id: Long, name: String): Unit { + val _params = Params.create() + _params.put("\$id", PrimitiveValue.newUint64(id)) + _params.put("\$name", PrimitiveValue.newText(name)) + if (transaction != null) { + transaction.createQuery( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + + "DECLARE \$id AS Uint64;\n" + + "DECLARE \$name AS Utf8;\n" + + "UPSERT INTO users (id, name) VALUES (\$id, \$name);", _params).execute().join().getStatus().expectSuccess() + } else { + client!!.supplyResult { _session -> + _session.createQuery( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + + "DECLARE \$id AS Uint64;\n" + + "DECLARE \$name AS Utf8;\n" + + "UPSERT INTO users (id, name) VALUES (\$id, \$name);", TxMode.SERIALIZABLE_RW, _params).execute() + }.join().getStatus().expectSuccess() + } + } + + // -- name: GetPrimaryUser :one + fun getPrimaryUser(id: Long): GetPrimaryUserRow? { + val _params = Params.create() + _params.put("\$id", PrimitiveValue.newUint64(id)) + val _query = if (transaction != null) { + QueryReader.readFrom(transaction.createQuery( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = \$id;", _params)).join().getValue() + } else { + client!!.supplyResult { _session -> + QueryReader.readFrom(_session.createQuery( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = \$id;", TxMode.SERIALIZABLE_RW, _params)) + }.join().getValue() + } + kotlin.check(_query.getResultSetCount() == 1) { "Expected one result set" } + val _rows = _query.getResultSet(0) + if (!_rows.next()) return null + val _value0: Long = _rows.getColumn(0).getUint64() + val _value1: String = _rows.getColumn(1).getText() + return GetPrimaryUserRow(_value0, _value1) + } + + // -- name: GetSecondaryUser :one + fun getSecondaryUser(id: Long): GetSecondaryUserRow? { + val _params = Params.create() + _params.put("\$id", PrimitiveValue.newUint64(id)) + val _query = if (transaction != null) { + QueryReader.readFrom(transaction.createQuery( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + + "DECLARE \$id AS Uint64;\n" + + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = \$id;", _params)).join().getValue() + } else { + client!!.supplyResult { _session -> + QueryReader.readFrom(_session.createQuery( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + + "DECLARE \$id AS Uint64;\n" + + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = \$id;", TxMode.SERIALIZABLE_RW, _params)) + }.join().getValue() + } + kotlin.check(_query.getResultSetCount() == 1) { "Expected one result set" } + val _rows = _query.getResultSet(0) + if (!_rows.next()) return null + val _value0: Long = _rows.getColumn(0).getUint64() + val _value1: String = _rows.getColumn(1).getText() + return GetSecondaryUserRow(_value0, _value1) + } + + // -- name: FindPrimaryUsersByName :many + fun findPrimaryUsersByName(name: String): List { + val _params = Params.create() + _params.put("\$name", PrimitiveValue.newText(name)) + val _query = if (transaction != null) { + QueryReader.readFrom(transaction.createQuery( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "SELECT u.`id` AS `id`, `u`.`name` AS `name` FROM users VIEW by_name AS u WHERE u.name = \$name ORDER BY u.id;", _params)).join().getValue() + } else { + client!!.supplyResult { _session -> + QueryReader.readFrom(_session.createQuery( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "SELECT u.`id` AS `id`, `u`.`name` AS `name` FROM users VIEW by_name AS u WHERE u.name = \$name ORDER BY u.id;", TxMode.SERIALIZABLE_RW, _params)) + }.join().getValue() + } + kotlin.check(_query.getResultSetCount() == 1) { "Expected one result set" } + val _rows = _query.getResultSet(0) + val _items = ArrayList() + while (_rows.next()) { + val _value0: Long = _rows.getColumn(0).getUint64() + val _value1: String = _rows.getColumn(1).getText() + _items.add(FindPrimaryUsersByNameRow(_value0, _value1)) + } + return _items + } + + // -- name: CompareUserNames :many + fun compareUserNames(): List { + val _params = Params.create() + val _query = if (transaction != null) { + QueryReader.readFrom(transaction.createQuery( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name\n" + + "FROM users AS a\n" + + "LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id\n" + + "ORDER BY a.id;", _params)).join().getValue() + } else { + client!!.supplyResult { _session -> + QueryReader.readFrom(_session.createQuery( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name\n" + + "FROM users AS a\n" + + "LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id\n" + + "ORDER BY a.id;", TxMode.SERIALIZABLE_RW, _params)) + }.join().getValue() + } + kotlin.check(_query.getResultSetCount() == 1) { "Expected one result set" } + val _rows = _query.getResultSet(0) + val _items = ArrayList() + while (_rows.next()) { + val _value0: Long = _rows.getColumn(0).getUint64() + val _value1: String = _rows.getColumn(1).getText() + val _value2: String? = _rows.getColumn(2).getText() + _items.add(CompareUserNamesRow(_value0, _value1, _value2)) + } + return _items + } +} diff --git a/examples/namespaces/php/native/CompareUserNamesRow.php b/examples/namespaces/php/native/CompareUserNamesRow.php new file mode 100644 index 00000000..d4204302 --- /dev/null +++ b/examples/namespaces/php/native/CompareUserNamesRow.php @@ -0,0 +1,14 @@ +session !== null) { + throw new \LogicException('Queries is already bound to a transaction; call withTx on an unbound helper'); + } + if ($txId === '') { + throw new \InvalidArgumentException('Transaction ID must not be empty'); + } + $queries = clone $this; + $queries->session = $session->take(); + $queries->txId = $txId; + return $queries; + } + + private function execute(Closure $operation): ExecuteQueryResult + { + if ($this->session !== null) { + return $operation($this->session); + } + return $this->table->retrySession($operation, $this->idempotent, $this->retryParams); + } + + // -- name: UpsertPrimaryUser :exec + public function upsertPrimaryUser(UpsertPrimaryUserParams $params): void + { + $parameters = [ + '$id' => YdbValueCodec::typedUint64($params->id, 'id'), + '$name' => YdbValueCodec::typedUtf8($params->name, 'name'), + ]; + + $this->execute(function (Session $session) use ($parameters): ExecuteQueryResult { + $query = $session->newQuery(<<<'SQLC_YDB_YQL' + PRAGMA TablePathPrefix("/local/sqlc_namespaces/a"); + DECLARE $id AS Uint64; + DECLARE $name AS Utf8; + UPSERT INTO users (id, name) VALUES ($id, $name); + SQLC_YDB_YQL) + ->parameters($parameters) + ->keepInCache(count($parameters) > 0); + if ($this->txId !== null) { + $query->txControl(new TransactionControl(['tx_id' => $this->txId])); + $txControl = $query->getRequestData()['tx_control']->serializeToString(); + } else { + $query->beginTx('serializable_read_write'); + } + if ($this->configure !== null) { + ($this->configure)($query); + } + if ($this->txId !== null && $query->getRequestData()['tx_control']->serializeToString() !== $txControl) { + throw new \LogicException('configure must not change transaction control on a transaction-bound Queries'); + } + + return (new YdbRawExecutor($this->table))->execute($session, $query, $this->txId === null); + }); + } + + // -- name: UpsertSecondaryUser :exec + public function upsertSecondaryUser(UpsertSecondaryUserParams $params): void + { + $parameters = [ + '$id' => YdbValueCodec::typedUint64($params->id, 'id'), + '$name' => YdbValueCodec::typedUtf8($params->name, 'name'), + ]; + + $this->execute(function (Session $session) use ($parameters): ExecuteQueryResult { + $query = $session->newQuery(<<<'SQLC_YDB_YQL' + PRAGMA TablePathPrefix("/local/sqlc_namespaces/b"); + DECLARE $id AS Uint64; + DECLARE $name AS Utf8; + UPSERT INTO users (id, name) VALUES ($id, $name); + SQLC_YDB_YQL) + ->parameters($parameters) + ->keepInCache(count($parameters) > 0); + if ($this->txId !== null) { + $query->txControl(new TransactionControl(['tx_id' => $this->txId])); + $txControl = $query->getRequestData()['tx_control']->serializeToString(); + } else { + $query->beginTx('serializable_read_write'); + } + if ($this->configure !== null) { + ($this->configure)($query); + } + if ($this->txId !== null && $query->getRequestData()['tx_control']->serializeToString() !== $txControl) { + throw new \LogicException('configure must not change transaction control on a transaction-bound Queries'); + } + + return (new YdbRawExecutor($this->table))->execute($session, $query, $this->txId === null); + }); + } + + // -- name: GetPrimaryUser :one + public function getPrimaryUser(string $id): ?GetPrimaryUserRow + { + $parameters = [ + '$id' => YdbValueCodec::typedUint64($id, 'id'), + ]; + + $result = $this->execute(function (Session $session) use ($parameters): ExecuteQueryResult { + $query = $session->newQuery(<<<'SQLC_YDB_YQL' + PRAGMA TablePathPrefix("/local/sqlc_namespaces/a"); + SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id; + SQLC_YDB_YQL) + ->parameters($parameters) + ->keepInCache(count($parameters) > 0); + if ($this->txId !== null) { + $query->txControl(new TransactionControl(['tx_id' => $this->txId])); + $txControl = $query->getRequestData()['tx_control']->serializeToString(); + } else { + $query->beginTx('serializable_read_write'); + } + if ($this->configure !== null) { + ($this->configure)($query); + } + if ($this->txId !== null && $query->getRequestData()['tx_control']->serializeToString() !== $txControl) { + throw new \LogicException('configure must not change transaction control on a transaction-bound Queries'); + } + + return (new YdbRawExecutor($this->table))->execute($session, $query, $this->txId === null); + }); + + $rows = $this->decodeRows( + $result, + 'GetPrimaryUser', + [ + ['id', PrimitiveTypeId::UINT64, false], + ['name', PrimitiveTypeId::UTF8, false], + ], + static fn($items): GetPrimaryUserRow => new GetPrimaryUserRow( + YdbValueCodec::uint64($items->offsetGet(0), 'GetPrimaryUser.id'), + YdbValueCodec::utf8($items->offsetGet(1), 'GetPrimaryUser.name'), + ), + ); + + return $rows[0] ?? null; + } + + // -- name: GetSecondaryUser :one + public function getSecondaryUser(string $id): ?GetSecondaryUserRow + { + $parameters = [ + '$id' => YdbValueCodec::typedUint64($id, 'id'), + ]; + + $result = $this->execute(function (Session $session) use ($parameters): ExecuteQueryResult { + $query = $session->newQuery(<<<'SQLC_YDB_YQL' + PRAGMA TablePathPrefix("/local/sqlc_namespaces/b"); + DECLARE $id AS Uint64; + SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id; + SQLC_YDB_YQL) + ->parameters($parameters) + ->keepInCache(count($parameters) > 0); + if ($this->txId !== null) { + $query->txControl(new TransactionControl(['tx_id' => $this->txId])); + $txControl = $query->getRequestData()['tx_control']->serializeToString(); + } else { + $query->beginTx('serializable_read_write'); + } + if ($this->configure !== null) { + ($this->configure)($query); + } + if ($this->txId !== null && $query->getRequestData()['tx_control']->serializeToString() !== $txControl) { + throw new \LogicException('configure must not change transaction control on a transaction-bound Queries'); + } + + return (new YdbRawExecutor($this->table))->execute($session, $query, $this->txId === null); + }); + + $rows = $this->decodeRows( + $result, + 'GetSecondaryUser', + [ + ['id', PrimitiveTypeId::UINT64, false], + ['name', PrimitiveTypeId::UTF8, false], + ], + static fn($items): GetSecondaryUserRow => new GetSecondaryUserRow( + YdbValueCodec::uint64($items->offsetGet(0), 'GetSecondaryUser.id'), + YdbValueCodec::utf8($items->offsetGet(1), 'GetSecondaryUser.name'), + ), + ); + + return $rows[0] ?? null; + } + + /** @return list */ + // -- name: FindPrimaryUsersByName :many + public function findPrimaryUsersByName(string $name): array + { + $parameters = [ + '$name' => YdbValueCodec::typedUtf8($name, 'name'), + ]; + + $result = $this->execute(function (Session $session) use ($parameters): ExecuteQueryResult { + $query = $session->newQuery(<<<'SQLC_YDB_YQL' + PRAGMA TablePathPrefix("/local/sqlc_namespaces/a"); + SELECT u.`id` AS `id`, `u`.`name` AS `name` FROM users VIEW by_name AS u WHERE u.name = $name ORDER BY u.id; + SQLC_YDB_YQL) + ->parameters($parameters) + ->keepInCache(count($parameters) > 0); + if ($this->txId !== null) { + $query->txControl(new TransactionControl(['tx_id' => $this->txId])); + $txControl = $query->getRequestData()['tx_control']->serializeToString(); + } else { + $query->beginTx('serializable_read_write'); + } + if ($this->configure !== null) { + ($this->configure)($query); + } + if ($this->txId !== null && $query->getRequestData()['tx_control']->serializeToString() !== $txControl) { + throw new \LogicException('configure must not change transaction control on a transaction-bound Queries'); + } + + return (new YdbRawExecutor($this->table))->execute($session, $query, $this->txId === null); + }); + + $rows = $this->decodeRows( + $result, + 'FindPrimaryUsersByName', + [ + ['id', PrimitiveTypeId::UINT64, false], + ['name', PrimitiveTypeId::UTF8, false], + ], + static fn($items): FindPrimaryUsersByNameRow => new FindPrimaryUsersByNameRow( + YdbValueCodec::uint64($items->offsetGet(0), 'FindPrimaryUsersByName.id'), + YdbValueCodec::utf8($items->offsetGet(1), 'FindPrimaryUsersByName.name'), + ), + ); + + return $rows; + } + + /** @return list */ + // -- name: CompareUserNames :many + public function compareUserNames(): array + { + $parameters = [ + ]; + + $result = $this->execute(function (Session $session) use ($parameters): ExecuteQueryResult { + $query = $session->newQuery(<<<'SQLC_YDB_YQL' + PRAGMA TablePathPrefix("/local/sqlc_namespaces/a"); + SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name + FROM users AS a + LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id + ORDER BY a.id; + SQLC_YDB_YQL) + ->parameters($parameters) + ->keepInCache(count($parameters) > 0); + if ($this->txId !== null) { + $query->txControl(new TransactionControl(['tx_id' => $this->txId])); + $txControl = $query->getRequestData()['tx_control']->serializeToString(); + } else { + $query->beginTx('serializable_read_write'); + } + if ($this->configure !== null) { + ($this->configure)($query); + } + if ($this->txId !== null && $query->getRequestData()['tx_control']->serializeToString() !== $txControl) { + throw new \LogicException('configure must not change transaction control on a transaction-bound Queries'); + } + + return (new YdbRawExecutor($this->table))->execute($session, $query, $this->txId === null); + }); + + $rows = $this->decodeRows( + $result, + 'CompareUserNames', + [ + ['id', PrimitiveTypeId::UINT64, false], + ['primary_name', PrimitiveTypeId::UTF8, false], + ['secondary_name', PrimitiveTypeId::UTF8, true], + ], + static fn($items): CompareUserNamesRow => new CompareUserNamesRow( + YdbValueCodec::uint64($items->offsetGet(0), 'CompareUserNames.id'), + YdbValueCodec::utf8($items->offsetGet(1), 'CompareUserNames.primary_name'), + YdbValueCodec::optionalUtf8($items->offsetGet(2), 'CompareUserNames.secondary_name'), + ), + ); + + return $rows; + } + + /** + * @param array $expectedColumns + * @return list + */ + private function decodeRows(ExecuteQueryResult $result, string $query, array $expectedColumns, Closure $decode): array + { + $sets = $result->getResultSets(); + if (count($sets) !== 1) { + throw new UnexpectedValueException(sprintf('%s: expected one YDB result set, got %d', $query, count($sets))); + } + $set = $sets->offsetGet(0); + if ($set->getTruncated()) { + throw new UnexpectedValueException($query . ': YDB result is truncated; use a bounded query or pagination'); + } + + $columns = $set->getColumns(); + if (count($columns) !== count($expectedColumns)) { + throw new UnexpectedValueException(sprintf('%s: expected %d result columns, got %d', $query, count($expectedColumns), count($columns))); + } + foreach ($expectedColumns as $index => [$name, $typeId, $optional]) { + $column = $columns->offsetGet($index); + if ($column->getName() !== $name) { + throw new UnexpectedValueException(sprintf('%s: result column %d is %s, expected %s', $query, $index, $column->getName(), $name)); + } + YdbValueCodec::assertType($column->getType(), $typeId, $optional, $query . '.' . $name); + } + $rows = []; + foreach ($set->getRows() as $rowIndex => $row) { + $items = $row->getItems(); + if (count($items) !== count($expectedColumns)) { + throw new UnexpectedValueException(sprintf('%s: row %d has %d values, expected %d', $query, $rowIndex, count($items), count($expectedColumns))); + } + $rows[] = $decode($items); + } + return $rows; + } +} diff --git a/examples/namespaces/php/native/UpsertPrimaryUserParams.php b/examples/namespaces/php/native/UpsertPrimaryUserParams.php new file mode 100644 index 00000000..6a74a646 --- /dev/null +++ b/examples/namespaces/php/native/UpsertPrimaryUserParams.php @@ -0,0 +1,13 @@ +client = $table->client(); + $this->meta = $table->meta(); + $this->credentials = $table->credentials(); + $this->ydb = $table->ydb(); + $this->logger = $table->getLogger() ?? new NullLogger(); + } + + public function execute(Session $session, YdbQuery $query, bool $releaseSession = true): ExecuteQueryResult + { + $data = $query->getRequestData(); + $data['session_id'] = $session->id(); + $session->take(); + try { + $result = $this->doRequest('Table', 'ExecuteDataQuery', $data); + } finally { + if ($releaseSession) { + $session->release(); + } + } + if (!$result instanceof ExecuteQueryResult) { + throw new UnexpectedValueException('YDB ExecuteDataQuery returned an unexpected result'); + } + return $result; + } +} + +/** @internal Exact protobuf binding and decoding used by generated methods. */ +final class YdbValueCodec +{ + private const UINT64_MAX = '18446744073709551615'; + private const UINT64_MODULUS = '18446744073709551616'; + private const TIMESTAMP_MAX_MICROSECONDS = '4291747199999999'; + + public static function assertType(Type $type, int $expectedTypeId, bool $optional, string $where): void + { + if ($optional) { + if ($type->getType() !== 'optional_type' || !$type->hasOptionalType()) { + throw new UnexpectedValueException($where . ': expected an Optional YDB type'); + } + $optionalType = $type->getOptionalType(); + if (!$optionalType instanceof OptionalType || !$optionalType->hasItem()) { + throw new UnexpectedValueException($where . ': malformed Optional YDB type'); + } + $type = $optionalType->getItem(); + } + if ($type->getType() !== 'type_id' || !$type->hasTypeId() || $type->getTypeId() !== $expectedTypeId) { + throw new UnexpectedValueException($where . ': result has an unexpected YDB type'); + } + } + + public static function typedUint64(string $value, string $where): TypedValue + { + self::validateUint64($value, $where); + return self::typed(PrimitiveTypeId::UINT64, 'uint64_value', self::encodedUint64($value)); + } + public static function typedUtf8(string $value, string $where): TypedValue + { + self::validUtf8($value, $where); + + return self::typed(PrimitiveTypeId::UTF8, 'text_value', $value); + } + + public static function uint64(Value $value, string $where): string + { + $raw = self::read($value, 'uint64_value', 'getUint64Value', $where); + if (is_int($raw) && $raw < 0) { + return bcadd((string) $raw, '18446744073709551616', 0); + } + $text = (string) $raw; + self::validateUint64($text, $where); + return $text; + } + public static function utf8(Value $value, string $where): string + { + $raw = self::decodedString($value, 'text_value', 'getTextValue', $where); + self::validUtf8($raw, $where); + + return $raw; + } + + public static function optionalUtf8(Value $value, string $where): ?string + { + return self::isNull($value) ? null : self::utf8($value, $where); + } + + private static function typed(int $typeId, string $case, mixed $value): TypedValue + { + return new TypedValue(['type' => self::type($typeId), 'value' => new Value([$case => $value])]); + } + + private static function type(int $typeId): Type + { + return new Type(['type_id' => $typeId]); + } + + private static function validUtf8(string $value, string $where): void + { + if (preg_match('//u', $value) !== 1) { + throw new \InvalidArgumentException($where . ': value is not valid UTF-8'); + } + } + + private static function validateUint64(string $value, string $where): void + { + if (!preg_match('/^(0|[1-9][0-9]*)$/D', $value) || bccomp($value, self::UINT64_MAX, 0) > 0) { + throw new \RangeException($where . ': value is outside YQL Uint64 range'); + } + } + private static function encodedUint64(string $value): string + { + return bccomp($value, (string) PHP_INT_MAX, 0) > 0 + ? bcsub($value, self::UINT64_MODULUS, 0) + : $value; + } + private static function read(Value $value, string $case, string $getter, string $where): mixed + { + if ($value->getValue() !== $case) throw new UnexpectedValueException($where . ': expected YDB value case ' . $case . ', got ' . $value->getValue()); + return $value->$getter(); + } + private static function isNull(Value $value): bool + { + return $value->getValue() === 'null_flag_value'; + } + + private static function decodedString(Value $value, string $case, string $getter, string $where): string + { + $raw = self::read($value, $case, $getter, $where); + if (!is_string($raw)) throw new UnexpectedValueException($where . ': invalid string value'); + return $raw; + } +} diff --git a/examples/namespaces/python/dbapi/__init__.py b/examples/namespaces/python/dbapi/__init__.py new file mode 100644 index 00000000..f7188d56 --- /dev/null +++ b/examples/namespaces/python/dbapi/__init__.py @@ -0,0 +1 @@ +# Code generated by sqlc-ydb. DO NOT EDIT. diff --git a/examples/namespaces/python/dbapi/models.py b/examples/namespaces/python/dbapi/models.py new file mode 100644 index 00000000..b9c432b5 --- /dev/null +++ b/examples/namespaces/python/dbapi/models.py @@ -0,0 +1,21 @@ +# Code generated by sqlc-ydb. DO NOT EDIT. +from dataclasses import dataclass +from datetime import date, datetime, timedelta +from uuid import UUID +from typing import Optional + +@dataclass +class LocalSqlcNamespacesAUsers: + id: int + name: str + +@dataclass +class LocalSqlcNamespacesBUsers: + id: int + name: str + +@dataclass +class CompareUserNamesRow: + id: int + primary_name: str + secondary_name: Optional[str] diff --git a/examples/namespaces/python/dbapi/queries.py b/examples/namespaces/python/dbapi/queries.py new file mode 100644 index 00000000..2b44ddbf --- /dev/null +++ b/examples/namespaces/python/dbapi/queries.py @@ -0,0 +1,136 @@ +# Code generated by sqlc-ydb. DO NOT EDIT. +from __future__ import annotations +from typing import Optional +from . import models as _models +import ydb as _ydb + + +class Querier: + def __init__(self, connection): + self._connection = connection + + # -- name: UpsertPrimaryUser :exec + def upsert_primary_user(self, id: int, name: str) -> None: + parameters = { + "$id": (id, _ydb.PrimitiveType.Uint64), + "$name": (name, _ydb.PrimitiveType.Utf8), + } + cursor = self._connection.cursor() + try: + cursor.execute( + ("PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "DECLARE $id AS Uint64;\n" + "DECLARE $name AS Utf8;\n" + "UPSERT INTO users (id, name) VALUES ($id, $name);"), + parameters, + ) + return None + finally: + cursor.close() + + # -- name: UpsertSecondaryUser :exec + def upsert_secondary_user(self, id: int, name: str) -> None: + parameters = { + "$id": (id, _ydb.PrimitiveType.Uint64), + "$name": (name, _ydb.PrimitiveType.Utf8), + } + cursor = self._connection.cursor() + try: + cursor.execute( + ("PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + "DECLARE $id AS Uint64;\n" + "DECLARE $name AS Utf8;\n" + "UPSERT INTO users (id, name) VALUES ($id, $name);"), + parameters, + ) + return None + finally: + cursor.close() + + # -- name: GetPrimaryUser :one + def get_primary_user(self, id: int) -> Optional[_models.LocalSqlcNamespacesAUsers]: + parameters = { + "$id": (id, _ydb.PrimitiveType.Uint64), + } + cursor = self._connection.cursor() + try: + cursor.execute( + ("PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;"), + parameters, + ) + row = cursor.fetchone() + if row is None: + return None + return _models.LocalSqlcNamespacesAUsers( + id=row[0], + name=row[1], + ) + finally: + cursor.close() + + # -- name: GetSecondaryUser :one + def get_secondary_user(self, id: int) -> Optional[_models.LocalSqlcNamespacesBUsers]: + parameters = { + "$id": (id, _ydb.PrimitiveType.Uint64), + } + cursor = self._connection.cursor() + try: + cursor.execute( + ("PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + "DECLARE $id AS Uint64;\n" + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;"), + parameters, + ) + row = cursor.fetchone() + if row is None: + return None + return _models.LocalSqlcNamespacesBUsers( + id=row[0], + name=row[1], + ) + finally: + cursor.close() + + # -- name: FindPrimaryUsersByName :many + def find_primary_users_by_name(self, name: str) -> list[_models.LocalSqlcNamespacesAUsers]: + parameters = { + "$name": (name, _ydb.PrimitiveType.Utf8), + } + cursor = self._connection.cursor() + try: + cursor.execute( + ("PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "SELECT u.`id` AS `id`, `u`.`name` AS `name` FROM users VIEW by_name AS u WHERE u.name = $name ORDER BY u.id;"), + parameters, + ) + rows = cursor.fetchall() + return [_models.LocalSqlcNamespacesAUsers( + id=row[0], + name=row[1], + ) for row in rows] + finally: + cursor.close() + + # -- name: CompareUserNames :many + def compare_user_names(self) -> list[_models.CompareUserNamesRow]: + parameters = { + } + cursor = self._connection.cursor() + try: + cursor.execute( + ("PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name\n" + "FROM users AS a\n" + "LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id\n" + "ORDER BY a.id;"), + parameters, + ) + rows = cursor.fetchall() + return [_models.CompareUserNamesRow( + id=row[0], + primary_name=row[1], + secondary_name=row[2], + ) for row in rows] + finally: + cursor.close() diff --git a/examples/namespaces/python/native/__init__.py b/examples/namespaces/python/native/__init__.py new file mode 100644 index 00000000..f7188d56 --- /dev/null +++ b/examples/namespaces/python/native/__init__.py @@ -0,0 +1 @@ +# Code generated by sqlc-ydb. DO NOT EDIT. diff --git a/examples/namespaces/python/native/models.py b/examples/namespaces/python/native/models.py new file mode 100644 index 00000000..b9c432b5 --- /dev/null +++ b/examples/namespaces/python/native/models.py @@ -0,0 +1,21 @@ +# Code generated by sqlc-ydb. DO NOT EDIT. +from dataclasses import dataclass +from datetime import date, datetime, timedelta +from uuid import UUID +from typing import Optional + +@dataclass +class LocalSqlcNamespacesAUsers: + id: int + name: str + +@dataclass +class LocalSqlcNamespacesBUsers: + id: int + name: str + +@dataclass +class CompareUserNamesRow: + id: int + primary_name: str + secondary_name: Optional[str] diff --git a/examples/namespaces/python/native/queries.py b/examples/namespaces/python/native/queries.py new file mode 100644 index 00000000..faa7522f --- /dev/null +++ b/examples/namespaces/python/native/queries.py @@ -0,0 +1,132 @@ +# Code generated by sqlc-ydb. DO NOT EDIT. +from __future__ import annotations +from typing import Optional, Union +from . import models as _models +import ydb as _ydb + + +class Querier: + def __init__(self, executor: Union[_ydb.QuerySessionPool, _ydb.QueryTxContext], *, retry_settings: Optional[_ydb.RetrySettings] = None): + if retry_settings is not None and not isinstance(executor, _ydb.QuerySessionPool): + raise ValueError("retry_settings belongs to the pool; configure retries around the whole transaction") + self._executor = executor + self._retry_settings = retry_settings + if isinstance(executor, _ydb.QuerySessionPool) and retry_settings is None: + self._retry_settings = _ydb.RetrySettings(max_retries=0) + + def _execute(self, query: str, parameters: dict): + if isinstance(self._executor, _ydb.QuerySessionPool): + return self._executor.execute_with_retries(query, parameters, retry_settings=self._retry_settings) + return list(self._executor.execute(query, parameters)) + + # -- name: UpsertPrimaryUser :exec + def upsert_primary_user(self, id: int, name: str) -> None: + parameters = { + "$id": _ydb.TypedValue(id, _ydb.PrimitiveType.Uint64), + "$name": _ydb.TypedValue(name, _ydb.PrimitiveType.Utf8), + } + result_sets = self._execute( + ("PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "DECLARE $id AS Uint64;\n" + "DECLARE $name AS Utf8;\n" + "UPSERT INTO users (id, name) VALUES ($id, $name);"), + parameters, + ) + return None + + # -- name: UpsertSecondaryUser :exec + def upsert_secondary_user(self, id: int, name: str) -> None: + parameters = { + "$id": _ydb.TypedValue(id, _ydb.PrimitiveType.Uint64), + "$name": _ydb.TypedValue(name, _ydb.PrimitiveType.Utf8), + } + result_sets = self._execute( + ("PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + "DECLARE $id AS Uint64;\n" + "DECLARE $name AS Utf8;\n" + "UPSERT INTO users (id, name) VALUES ($id, $name);"), + parameters, + ) + return None + + # -- name: GetPrimaryUser :one + def get_primary_user(self, id: int) -> Optional[_models.LocalSqlcNamespacesAUsers]: + parameters = { + "$id": _ydb.TypedValue(id, _ydb.PrimitiveType.Uint64), + } + result_sets = self._execute( + ("PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;"), + parameters, + ) + if len(result_sets) != 1: + raise ValueError("expected exactly one YDB result set") + rows = result_sets[0].rows + row = rows[0] if rows else None + if row is None: + return None + return _models.LocalSqlcNamespacesAUsers( + id=row["id"], + name=row["name"], + ) + + # -- name: GetSecondaryUser :one + def get_secondary_user(self, id: int) -> Optional[_models.LocalSqlcNamespacesBUsers]: + parameters = { + "$id": _ydb.TypedValue(id, _ydb.PrimitiveType.Uint64), + } + result_sets = self._execute( + ("PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + "DECLARE $id AS Uint64;\n" + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;"), + parameters, + ) + if len(result_sets) != 1: + raise ValueError("expected exactly one YDB result set") + rows = result_sets[0].rows + row = rows[0] if rows else None + if row is None: + return None + return _models.LocalSqlcNamespacesBUsers( + id=row["id"], + name=row["name"], + ) + + # -- name: FindPrimaryUsersByName :many + def find_primary_users_by_name(self, name: str) -> list[_models.LocalSqlcNamespacesAUsers]: + parameters = { + "$name": _ydb.TypedValue(name, _ydb.PrimitiveType.Utf8), + } + result_sets = self._execute( + ("PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "SELECT u.`id` AS `id`, `u`.`name` AS `name` FROM users VIEW by_name AS u WHERE u.name = $name ORDER BY u.id;"), + parameters, + ) + if len(result_sets) != 1: + raise ValueError("expected exactly one YDB result set") + rows = result_sets[0].rows + return [_models.LocalSqlcNamespacesAUsers( + id=row["id"], + name=row["name"], + ) for row in rows] + + # -- name: CompareUserNames :many + def compare_user_names(self) -> list[_models.CompareUserNamesRow]: + parameters = { + } + result_sets = self._execute( + ("PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name\n" + "FROM users AS a\n" + "LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id\n" + "ORDER BY a.id;"), + parameters, + ) + if len(result_sets) != 1: + raise ValueError("expected exactly one YDB result set") + rows = result_sets[0].rows + return [_models.CompareUserNamesRow( + id=row["id"], + primary_name=row["primary_name"], + secondary_name=row["secondary_name"], + ) for row in rows] diff --git a/examples/namespaces/python/sqlalchemy/__init__.py b/examples/namespaces/python/sqlalchemy/__init__.py new file mode 100644 index 00000000..f7188d56 --- /dev/null +++ b/examples/namespaces/python/sqlalchemy/__init__.py @@ -0,0 +1 @@ +# Code generated by sqlc-ydb. DO NOT EDIT. diff --git a/examples/namespaces/python/sqlalchemy/models.py b/examples/namespaces/python/sqlalchemy/models.py new file mode 100644 index 00000000..b9c432b5 --- /dev/null +++ b/examples/namespaces/python/sqlalchemy/models.py @@ -0,0 +1,21 @@ +# Code generated by sqlc-ydb. DO NOT EDIT. +from dataclasses import dataclass +from datetime import date, datetime, timedelta +from uuid import UUID +from typing import Optional + +@dataclass +class LocalSqlcNamespacesAUsers: + id: int + name: str + +@dataclass +class LocalSqlcNamespacesBUsers: + id: int + name: str + +@dataclass +class CompareUserNamesRow: + id: int + primary_name: str + secondary_name: Optional[str] diff --git a/examples/namespaces/python/sqlalchemy/queries.py b/examples/namespaces/python/sqlalchemy/queries.py new file mode 100644 index 00000000..bdbd3d2c --- /dev/null +++ b/examples/namespaces/python/sqlalchemy/queries.py @@ -0,0 +1,140 @@ +# Code generated by sqlc-ydb. DO NOT EDIT. +from __future__ import annotations +from typing import Optional +from . import models as _models +import ydb as _ydb +from sqlalchemy import text as _text +from sqlalchemy.engine import Connection + + +class Querier: + def __init__(self, connection: Connection): + self._connection = connection + + # -- name: UpsertPrimaryUser :exec + def upsert_primary_user(self, id: int, name: str) -> None: + parameters = { + "id": (id, _ydb.PrimitiveType.Uint64), + "name": (name, _ydb.PrimitiveType.Utf8), + } + result = self._connection.execute( + _text( + ("PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "DECLARE $id AS Uint64;\n" + "DECLARE $name AS Utf8;\n" + "UPSERT INTO users (id, name) VALUES (:id, :name);") + ), + parameters, + ) + result.close() + return None + + # -- name: UpsertSecondaryUser :exec + def upsert_secondary_user(self, id: int, name: str) -> None: + parameters = { + "id": (id, _ydb.PrimitiveType.Uint64), + "name": (name, _ydb.PrimitiveType.Utf8), + } + result = self._connection.execute( + _text( + ("PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + "DECLARE $id AS Uint64;\n" + "DECLARE $name AS Utf8;\n" + "UPSERT INTO users (id, name) VALUES (:id, :name);") + ), + parameters, + ) + result.close() + return None + + # -- name: GetPrimaryUser :one + def get_primary_user(self, id: int) -> Optional[_models.LocalSqlcNamespacesAUsers]: + parameters = { + "id": (id, _ydb.PrimitiveType.Uint64), + } + result = self._connection.execute( + _text( + ("PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = :id;") + ), + parameters, + ) + try: + row = result.fetchone() + finally: + result.close() + if row is None: + return None + return _models.LocalSqlcNamespacesAUsers( + id=row._mapping["id"], + name=row._mapping["name"], + ) + + # -- name: GetSecondaryUser :one + def get_secondary_user(self, id: int) -> Optional[_models.LocalSqlcNamespacesBUsers]: + parameters = { + "id": (id, _ydb.PrimitiveType.Uint64), + } + result = self._connection.execute( + _text( + ("PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + "DECLARE $id AS Uint64;\n" + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = :id;") + ), + parameters, + ) + try: + row = result.fetchone() + finally: + result.close() + if row is None: + return None + return _models.LocalSqlcNamespacesBUsers( + id=row._mapping["id"], + name=row._mapping["name"], + ) + + # -- name: FindPrimaryUsersByName :many + def find_primary_users_by_name(self, name: str) -> list[_models.LocalSqlcNamespacesAUsers]: + parameters = { + "name": (name, _ydb.PrimitiveType.Utf8), + } + result = self._connection.execute( + _text( + ("PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "SELECT u.`id` AS `id`, `u`.`name` AS `name` FROM users VIEW by_name AS u WHERE u.name = :name ORDER BY u.id;") + ), + parameters, + ) + try: + rows = result.fetchall() + finally: + result.close() + return [_models.LocalSqlcNamespacesAUsers( + id=row._mapping["id"], + name=row._mapping["name"], + ) for row in rows] + + # -- name: CompareUserNames :many + def compare_user_names(self) -> list[_models.CompareUserNamesRow]: + parameters = { + } + result = self._connection.execute( + _text( + ("PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + "SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name\n" + "FROM users AS a\n" + "LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id\n" + "ORDER BY a.id;") + ), + parameters, + ) + try: + rows = result.fetchall() + finally: + result.close() + return [_models.CompareUserNamesRow( + id=row._mapping["id"], + primary_name=row._mapping["primary_name"], + secondary_name=row._mapping["secondary_name"], + ) for row in rows] diff --git a/examples/namespaces/queries.sql b/examples/namespaces/queries.sql new file mode 100644 index 00000000..bfc594c3 --- /dev/null +++ b/examples/namespaces/queries.sql @@ -0,0 +1,31 @@ +-- name: UpsertPrimaryUser :exec +PRAGMA TablePathPrefix("/local/sqlc_namespaces/a"); +DECLARE $id AS Uint64; +DECLARE $name AS Utf8; +UPSERT INTO users (id, name) VALUES ($id, $name); + +-- name: UpsertSecondaryUser :exec +PRAGMA TablePathPrefix("/local/sqlc_namespaces/b"); +DECLARE $id AS Uint64; +DECLARE $name AS Utf8; +UPSERT INTO users (id, name) VALUES ($id, $name); + +-- name: GetPrimaryUser :one +PRAGMA TablePathPrefix("/local/sqlc_namespaces/a"); +SELECT users.* FROM users WHERE id = $id; + +-- name: GetSecondaryUser :one +PRAGMA TablePathPrefix("/local/sqlc_namespaces/b"); +DECLARE $id AS Uint64; +SELECT users.* FROM users WHERE id = $id; + +-- name: FindPrimaryUsersByName :many +PRAGMA TablePathPrefix("/local/sqlc_namespaces/a"); +SELECT u.* FROM users VIEW by_name AS u WHERE u.name = $name ORDER BY u.id; + +-- name: CompareUserNames :many +PRAGMA TablePathPrefix("/local/sqlc_namespaces/a"); +SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name +FROM users AS a +LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id +ORDER BY a.id; diff --git a/examples/namespaces/rust/native/lib.rs b/examples/namespaces/rust/native/lib.rs new file mode 100644 index 00000000..4752f093 --- /dev/null +++ b/examples/namespaces/rust/native/lib.rs @@ -0,0 +1,3 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +pub mod models; +pub mod queries; diff --git a/examples/namespaces/rust/native/models.rs b/examples/namespaces/rust/native/models.rs new file mode 100644 index 00000000..3fcfbb74 --- /dev/null +++ b/examples/namespaces/rust/native/models.rs @@ -0,0 +1,26 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct GetPrimaryUserRow { + pub id: u64, + pub name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct GetSecondaryUserRow { + pub id: u64, + pub name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct FindPrimaryUsersByNameRow { + pub id: u64, + pub name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct CompareUserNamesRow { + pub id: u64, + pub primary_name: String, + pub secondary_name: Option, +} diff --git a/examples/namespaces/rust/native/queries.rs b/examples/namespaces/rust/native/queries.rs new file mode 100644 index 00000000..ef29d1ef --- /dev/null +++ b/examples/namespaces/rust/native/queries.rs @@ -0,0 +1,125 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. + +use super::models::*; + +pub struct Queries<'a, E: ydb::QueryExecutor> { + client: &'a mut E, +} + +#[bon::bon] +impl<'a, E: ydb::QueryExecutor> Queries<'a, E> { + pub fn new(client: &'a mut E) -> Self { + Self { client } + } + + // -- name: UpsertPrimaryUser :exec + #[builder(on(String, into))] + pub async fn upsert_primary_user(&mut self, id: u64, name: String) -> ydb::YdbResult<()> { + self.client + .exec(concat!( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n", + "DECLARE $id AS Uint64;\n", + "DECLARE $name AS Utf8;\n", + "UPSERT INTO users (id, name) VALUES ($id, $name);", + )) + .param("$id", id) + .param("$name", name) + .await + } + + // -- name: UpsertSecondaryUser :exec + #[builder(on(String, into))] + pub async fn upsert_secondary_user(&mut self, id: u64, name: String) -> ydb::YdbResult<()> { + self.client + .exec(concat!( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n", + "DECLARE $id AS Uint64;\n", + "DECLARE $name AS Utf8;\n", + "UPSERT INTO users (id, name) VALUES ($id, $name);", + )) + .param("$id", id) + .param("$name", name) + .await + } + + // -- name: GetPrimaryUser :one + #[builder(on(String, into))] + pub async fn primary_user(&mut self, id: u64) -> ydb::YdbResult { + let mut row = self + .client + .query_row(concat!( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n", + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;", + )) + .param("$id", id) + .await?; + Ok(GetPrimaryUserRow { + id: row.remove_field(0)?.try_into()?, + name: row.remove_field(1)?.try_into()?, + }) + } + + // -- name: GetSecondaryUser :one + #[builder(on(String, into))] + pub async fn secondary_user(&mut self, id: u64) -> ydb::YdbResult { + let mut row = self + .client + .query_row(concat!( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n", + "DECLARE $id AS Uint64;\n", + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;", + )) + .param("$id", id) + .await?; + Ok(GetSecondaryUserRow { + id: row.remove_field(0)?.try_into()?, + name: row.remove_field(1)?.try_into()?, + }) + } + + // -- name: FindPrimaryUsersByName :many + #[builder(on(String, into))] + pub async fn find_primary_users_by_name( + &mut self, + name: String, + ) -> ydb::YdbResult> { + self.client + .query_result_set(concat!( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n", + "SELECT u.`id` AS `id`, `u`.`name` AS `name` FROM users VIEW by_name AS u WHERE u.name = $name ORDER BY u.id;", + )) + .param("$name", name) + .await? + .rows() + .map(|mut row| { + Ok(FindPrimaryUsersByNameRow { + id: row.remove_field(0)?.try_into()?, + name: row.remove_field(1)?.try_into()?, + }) + }) + .collect() + } + + // -- name: CompareUserNames :many + #[builder(on(String, into))] + pub async fn compare_user_names(&mut self) -> ydb::YdbResult> { + self.client + .query_result_set(concat!( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n", + "SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name\n", + "FROM users AS a\n", + "LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id\n", + "ORDER BY a.id;", + )) + .await? + .rows() + .map(|mut row| { + Ok(CompareUserNamesRow { + id: row.remove_field(0)?.try_into()?, + primary_name: row.remove_field(1)?.try_into()?, + secondary_name: row.remove_field(2)?.try_into()?, + }) + }) + .collect() + } +} diff --git a/examples/namespaces/schema/a.sql b/examples/namespaces/schema/a.sql new file mode 100644 index 00000000..9067edde --- /dev/null +++ b/examples/namespaces/schema/a.sql @@ -0,0 +1,8 @@ +PRAGMA TablePathPrefix("/local/sqlc_namespaces/a"); + +CREATE TABLE users ( + id Uint64 NOT NULL, + name Utf8 NOT NULL, + PRIMARY KEY (id), + INDEX by_name GLOBAL SYNC ON (name) +); diff --git a/examples/namespaces/schema/b.sql b/examples/namespaces/schema/b.sql new file mode 100644 index 00000000..92d2ca75 --- /dev/null +++ b/examples/namespaces/schema/b.sql @@ -0,0 +1,7 @@ +PRAGMA TablePathPrefix("/local/sqlc_namespaces/b"); + +CREATE TABLE users ( + id Uint64 NOT NULL, + name Utf8 NOT NULL, + PRIMARY KEY (id) +); diff --git a/examples/namespaces/sqlc.yaml b/examples/namespaces/sqlc.yaml new file mode 100644 index 00000000..972daa82 --- /dev/null +++ b/examples/namespaces/sqlc.yaml @@ -0,0 +1,107 @@ +version: "2" +sql: + - name: native + engine: ydb + schema: schema + queries: queries.sql + gen: + kotlin: + package: namespaces.nativeapi + out: kotlin/nativeapi + runtime: ydb + php: + out: php/native + namespace: Namespaces\Native + runtime: ydb + typescript: + out: typescript/native + runtime: ydb + rust: + out: rust/native + runtime: ydb + go: + package: namespaces + out: go/native + sql_package: ydb + emit_json_tags: true + emit_interface: true + emit_empty_slices: true + python: + out: python/native + runtime: ydb + csharp: + namespace: Namespaces.AdoNet + out: csharp/adonet + cpp: + namespace: namespaces::native + out: cpp/native + runtime: ydb + java: + package: namespaces.nativeapi + out: java/native/src/main/java/namespaces/nativeapi + runtime: ydb + - name: portable + engine: ydb + schema: schema + queries: queries.sql + gen: + kotlin: + package: namespaces.jdbc + out: kotlin/jdbc + runtime: jdbc + go: + package: namespaces + out: go/database/sql + sql_package: database/sql + python: + out: python/dbapi + runtime: dbapi + java: + package: namespaces.jdbc + out: java/jdbc/src/main/java/namespaces/jdbc + runtime: jdbc + - name: exposed + engine: ydb + schema: schema + queries: queries.sql + gen: + kotlin: + package: namespaces.exposed + out: kotlin/exposed + runtime: exposed + - name: sqlalchemy + engine: ydb + schema: schema + queries: queries.sql + gen: + python: + out: python/sqlalchemy + runtime: sqlalchemy + emit_sync_querier: true + - name: userver + engine: ydb + schema: schema + queries: queries.sql + gen: + cpp: + namespace: namespaces::userver + out: cpp/userver + runtime: userver + - name: dapper + engine: ydb + schema: schema + queries: queries.sql + gen: + csharp: + namespace: Namespaces.Dapper + out: csharp/dapper + runtime: dapper + - name: jooq + engine: ydb + schema: schema + queries: queries.sql + gen: + java: + package: namespaces.jooq + out: java/jooq + runtime: jooq diff --git a/examples/namespaces/typescript/native/queries.ts b/examples/namespaces/typescript/native/queries.ts new file mode 100644 index 00000000..7cb483ae --- /dev/null +++ b/examples/namespaces/typescript/native/queries.ts @@ -0,0 +1,137 @@ +// Code generated by sqlc-ydb. DO NOT EDIT. +import type { Query, SQL } from "@ydbjs/query"; +import { Uint64, Utf8 } from "@ydbjs/value/primitive"; + +export type ConfigureQuery = (query: Query) => void; + +export type UpsertPrimaryUserParams = { + readonly id: bigint; + readonly name: string; +}; + +export type UpsertSecondaryUserParams = { + readonly id: bigint; + readonly name: string; +}; + +export type GetPrimaryUserRow = { + readonly id: bigint; + readonly name: string; +}; + +export type GetSecondaryUserRow = { + readonly id: bigint; + readonly name: string; +}; + +export type FindPrimaryUsersByNameRow = { + readonly id: bigint; + readonly name: string; +}; + +export type CompareUserNamesRow = { + readonly id: bigint; + readonly primary_name: string; + readonly secondary_name: string | null; +}; + +export class Queries { + readonly #sql: SQL; + + constructor(sql: SQL) { + if (typeof sql !== "function") throw new TypeError("Queries requires a YDB SQL function"); + this.#sql = sql; + } + + // -- name: UpsertPrimaryUser :exec + async upsertPrimaryUser(args: UpsertPrimaryUserParams, configure?: ConfigureQuery): Promise { + const stmt = this.#sql( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "DECLARE $id AS Uint64;\n" + + "DECLARE $name AS Utf8;\n" + + "UPSERT INTO users (id, name) VALUES ($id, $name);" + ); + // Keep explicit DECLARE statements; the SDK otherwise prepends duplicates. + Object.defineProperty(stmt, "text", { value: stmt.text, writable: false }); + stmt + .parameter("id", new Uint64(args.id)) + .parameter("name", new Utf8(args.name)); + configure?.(stmt); + await stmt; + } + + // -- name: UpsertSecondaryUser :exec + async upsertSecondaryUser(args: UpsertSecondaryUserParams, configure?: ConfigureQuery): Promise { + const stmt = this.#sql( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + + "DECLARE $id AS Uint64;\n" + + "DECLARE $name AS Utf8;\n" + + "UPSERT INTO users (id, name) VALUES ($id, $name);" + ); + // Keep explicit DECLARE statements; the SDK otherwise prepends duplicates. + Object.defineProperty(stmt, "text", { value: stmt.text, writable: false }); + stmt + .parameter("id", new Uint64(args.id)) + .parameter("name", new Utf8(args.name)); + configure?.(stmt); + await stmt; + } + + // -- name: GetPrimaryUser :one + async getPrimaryUser(id: bigint, configure?: ConfigureQuery): Promise { + const stmt = this.#sql<[GetPrimaryUserRow]>( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;" + ) + .parameter("id", new Uint64(id)); + configure?.(stmt); + const [rows] = await stmt; + + return rows[0] ?? null; + } + + // -- name: GetSecondaryUser :one + async getSecondaryUser(id: bigint, configure?: ConfigureQuery): Promise { + const stmt = this.#sql<[GetSecondaryUserRow]>( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/b\");\n" + + "DECLARE $id AS Uint64;\n" + + "SELECT users.`id` AS `id`, `users`.`name` AS `name` FROM users WHERE id = $id;" + ); + // Keep explicit DECLARE statements; the SDK otherwise prepends duplicates. + Object.defineProperty(stmt, "text", { value: stmt.text, writable: false }); + stmt + .parameter("id", new Uint64(id)); + configure?.(stmt); + const [rows] = await stmt; + + return rows[0] ?? null; + } + + // -- name: FindPrimaryUsersByName :many + async findPrimaryUsersByName(name: string, configure?: ConfigureQuery): Promise { + const stmt = this.#sql<[FindPrimaryUsersByNameRow]>( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "SELECT u.`id` AS `id`, `u`.`name` AS `name` FROM users VIEW by_name AS u WHERE u.name = $name ORDER BY u.id;" + ) + .parameter("name", new Utf8(name)); + configure?.(stmt); + const [rows] = await stmt; + + return rows; + } + + // -- name: CompareUserNames :many + async compareUserNames(configure?: ConfigureQuery): Promise { + const stmt = this.#sql<[CompareUserNamesRow]>( + "PRAGMA TablePathPrefix(\"/local/sqlc_namespaces/a\");\n" + + "SELECT a.id AS id, a.name AS primary_name, b.name AS secondary_name\n" + + "FROM users AS a\n" + + "LEFT JOIN `/local/sqlc_namespaces/b/users` AS b ON a.id = b.id\n" + + "ORDER BY a.id;" + ); + configure?.(stmt); + const [rows] = await stmt; + + return rows; + } +} diff --git a/examples/namespaces/typescript/package.json b/examples/namespaces/typescript/package.json new file mode 100644 index 00000000..e986b24b --- /dev/null +++ b/examples/namespaces/typescript/package.json @@ -0,0 +1,4 @@ +{ + "private": true, + "type": "module" +} diff --git a/internal/analyzer/analyzer.go b/internal/analyzer/analyzer.go index 418a651f..36bb4fca 100644 --- a/internal/analyzer/analyzer.go +++ b/internal/analyzer/analyzer.go @@ -149,14 +149,15 @@ func parseYQL(file, text string, lineOffset int) (parsedYQL, []model.Diagnostic) } type queryBlock struct { - name string - command model.Command - file string - line int - text string - functions *builtins.Registry - parsed *parsedYQL - wildcards *wildcardRewrites + name string + command model.Command + file string + line int + text string + functions *builtins.Registry + parsed *parsedYQL + wildcards *wildcardRewrites + tablePathPrefix string } func queryBlocks(source model.Source) ([]queryBlock, []model.Diagnostic) { diff --git a/internal/analyzer/analyzer_test.go b/internal/analyzer/analyzer_test.go index 95a90b2a..ef34aeac 100644 --- a/internal/analyzer/analyzer_test.go +++ b/internal/analyzer/analyzer_test.go @@ -557,9 +557,9 @@ func TestAnalyzeRejectsDuplicateQueryNamesAcrossFiles(t *testing.T) { func TestAnalyzeRejectsMixedUnsupportedQueryStatement(t *testing.T) { _, err := Analyze( []model.Source{{Name: "schema.sql", Text: `CREATE TABLE authors (id Uint64 NOT NULL, PRIMARY KEY (id));`}}, - []model.Source{{Name: "query.sql", Text: "-- name: GetAuthor :one\nPRAGMA TablePathPrefix('/Root');\nSELECT id FROM authors;"}}, + []model.Source{{Name: "query.sql", Text: "-- name: GetAuthor :one\nPRAGMA AnsiInForEmptyOrNullableItemsCollections;\nSELECT id FROM authors;"}}, ) - if err == nil || !strings.Contains(err.Error(), "unsupported statement in named query") { + if err == nil || !strings.Contains(err.Error(), "unsupported PRAGMA") { t.Fatalf("error = %v", err) } } diff --git a/internal/analyzer/catalog.go b/internal/analyzer/catalog.go index e716f7e9..ef42999e 100644 --- a/internal/analyzer/catalog.go +++ b/internal/analyzer/catalog.go @@ -25,22 +25,30 @@ func buildCatalog(sources []model.Source) (model.Catalog, []model.Diagnostic) { diagnostics = append(diagnostics, model.Diagnostic{Position: model.Position{File: source.Name, Line: 1, Column: 1}, Message: "unsupported schema query form"}) continue } + prefix, prefixDiagnostics := tablePathPrefix(source.Name, 0, parsed.tree) + diagnostics = append(diagnostics, prefixDiagnostics...) + if len(prefixDiagnostics) != 0 { + continue + } for _, statement := range statementList.AllSql_stmt() { core := statement.Sql_stmt_core() if statement.EXPLAIN() != nil || core == nil { diagnostics = append(diagnostics, diagnosticAt(source.Name, 0, statement, fmt.Sprintf("unsupported schema statement %q; supported statements are CREATE TABLE, ALTER TABLE, and DROP TABLE", statement.GetText()))) continue } + if core.Pragma_stmt() != nil { + continue + } if create := core.Create_table_stmt(); create != nil { - diagnostics = append(diagnostics, applyCreateTable(&catalog, source.Name, create)...) + diagnostics = append(diagnostics, applyCreateTable(&catalog, source.Name, prefix, create)...) continue } if alter := core.Alter_table_stmt(); alter != nil { - diagnostics = append(diagnostics, applyAlterTable(&catalog, source.Name, alter)...) + diagnostics = append(diagnostics, applyAlterTable(&catalog, source.Name, prefix, alter)...) continue } if drop := core.Drop_table_stmt(); drop != nil { - diagnostics = append(diagnostics, applyDropTable(&catalog, source.Name, drop)...) + diagnostics = append(diagnostics, applyDropTable(&catalog, source.Name, prefix, drop)...) continue } diagnostics = append(diagnostics, diagnosticAt(source.Name, 0, statement, fmt.Sprintf("unsupported schema statement %q; supported statements are CREATE TABLE, ALTER TABLE, and DROP TABLE", statement.GetText()))) @@ -49,11 +57,11 @@ func buildCatalog(sources []model.Source) (model.Catalog, []model.Diagnostic) { return catalog, diagnostics } -func applyCreateTable(catalog *model.Catalog, file string, create parser.ICreate_table_stmtContext) []model.Diagnostic { +func applyCreateTable(catalog *model.Catalog, file, prefix string, create parser.ICreate_table_stmtContext) []model.Diagnostic { if diagnostic := validateCreateTableShape(file, create); diagnostic != nil { return []model.Diagnostic{*diagnostic} } - name := simpleTableName(create.Simple_table_ref()) + name := resolveTablePath(prefix, simpleTableName(create.Simple_table_ref())) if name != "" { if _, exists := catalogTableIndex(*catalog, name); exists { if create.IF() != nil && create.NOT() != nil && create.EXISTS() != nil { @@ -64,7 +72,7 @@ func applyCreateTable(catalog *model.Catalog, file string, create parser.ICreate return []model.Diagnostic{diagnosticAt(file, 0, create, fmt.Sprintf("table %q already exists", name))} } } - table, diagnostics := catalogTable(file, create) + table, diagnostics := catalogTable(file, prefix, create) if len(diagnostics) != 0 { return diagnostics } @@ -72,11 +80,11 @@ func applyCreateTable(catalog *model.Catalog, file string, create parser.ICreate return nil } -func applyDropTable(catalog *model.Catalog, file string, drop parser.IDrop_table_stmtContext) []model.Diagnostic { +func applyDropTable(catalog *model.Catalog, file, prefix string, drop parser.IDrop_table_stmtContext) []model.Diagnostic { if drop.TABLE() == nil || drop.EXTERNAL() != nil || drop.TABLESTORE() != nil { return []model.Diagnostic{diagnosticAt(file, 0, drop, "only ordinary DROP TABLE is supported")} } - name := simpleTableName(drop.Simple_table_ref()) + name := resolveTablePath(prefix, simpleTableName(drop.Simple_table_ref())) if name == "" { return []model.Diagnostic{diagnosticAt(file, 0, drop, "DROP TABLE has no resolvable table name")} } @@ -91,8 +99,8 @@ func applyDropTable(catalog *model.Catalog, file string, drop parser.IDrop_table return nil } -func applyAlterTable(catalog *model.Catalog, file string, alter parser.IAlter_table_stmtContext) []model.Diagnostic { - name := simpleTableName(alter.Simple_table_ref()) +func applyAlterTable(catalog *model.Catalog, file, prefix string, alter parser.IAlter_table_stmtContext) []model.Diagnostic { + name := resolveTablePath(prefix, simpleTableName(alter.Simple_table_ref())) if name == "" { return []model.Diagnostic{diagnosticAt(file, 0, alter, "ALTER TABLE has no resolvable table name")} } @@ -111,7 +119,7 @@ func applyAlterTable(catalog *model.Catalog, file string, alter parser.IAlter_ta } } for _, action := range actions { - diagnostics = append(diagnostics, applyAlterTableAction(*catalog, index, &working, file, action)...) + diagnostics = append(diagnostics, applyAlterTableAction(*catalog, index, &working, file, prefix, action)...) if len(diagnostics) != 0 { return diagnostics } @@ -120,7 +128,7 @@ func applyAlterTable(catalog *model.Catalog, file string, alter parser.IAlter_ta return nil } -func applyAlterTableAction(catalog model.Catalog, tableIndex int, table *model.Table, file string, action parser.IAlter_table_actionContext) []model.Diagnostic { +func applyAlterTableAction(catalog model.Catalog, tableIndex int, table *model.Table, file, prefix string, action parser.IAlter_table_actionContext) []model.Diagnostic { if add := action.Alter_table_add_index(); add != nil { index, err := parseTableIndex(add.Table_index()) if err == nil { @@ -178,7 +186,7 @@ func applyAlterTableAction(catalog model.Catalog, tableIndex int, table *model.T return nil } if rename := action.Alter_table_rename_to(); rename != nil { - newName := identifier(rename.An_id_table().GetText()) + newName := resolveTablePath(prefix, identifier(rename.An_id_table().GetText())) if otherIndex, exists := catalogTableIndex(catalog, newName); exists && otherIndex != tableIndex { return []model.Diagnostic{diagnosticAt(file, 0, rename, fmt.Sprintf("table %q already exists", newName))} } @@ -224,13 +232,13 @@ func validateCreateTableShape(file string, create parser.ICreate_table_stmtConte return nil } -func catalogTable(file string, create parser.ICreate_table_stmtContext) (model.Table, []model.Diagnostic) { +func catalogTable(file, prefix string, create parser.ICreate_table_stmtContext) (model.Table, []model.Diagnostic) { var diagnostics []model.Diagnostic ref := create.Simple_table_ref() if ref == nil || ref.Simple_table_ref_core() == nil { return model.Table{}, []model.Diagnostic{diagnosticAt(file, 0, create, "CREATE TABLE has no resolvable table name")} } - table := model.Table{Name: identifier(ref.Simple_table_ref_core().GetText())} + table := model.Table{Name: resolveTablePath(prefix, simpleTableName(ref))} columnNames := map[string]bool{} primaryKeyNames := map[string]bool{} primaryKeyDeclarations := 0 diff --git a/internal/analyzer/database.go b/internal/analyzer/database.go index 9b0cc288..44ba9341 100644 --- a/internal/analyzer/database.go +++ b/internal/analyzer/database.go @@ -88,12 +88,18 @@ func databaseTableReferences(blocks []queryBlock) ([]databaseTableReference, []m continue } block.parsed = &parsed + prefix, prefixDiagnostics := tablePathPrefix(block.file, block.line-1, parsed.tree) + diagnostics = append(diagnostics, prefixDiagnostics...) + if len(prefixDiagnostics) != 0 { + continue + } tree := collectQueryTree(parsed.tree) diagnostics = append(diagnostics, validateQueryStatements(*block, tree)...) diagnostics = append(diagnostics, unsupportedSQLCMacroDiagnostics(*block, parsed.tokens)...) _, _, declarationDiagnostics := declarations(*block, tree) diagnostics = append(diagnostics, declarationDiagnostics...) add := func(name string, node antlr.ParserRuleContext) { + name = resolveTablePath(prefix, name) if name != "" && !seen[name] { seen[name] = true references = append(references, databaseTableReference{name: name, position: diagnosticAt(block.file, block.line-1, node, "").Position}) diff --git a/internal/analyzer/member.go b/internal/analyzer/member.go index 30641d6b..afdbf5cf 100644 --- a/internal/analyzer/member.go +++ b/internal/analyzer/member.go @@ -59,7 +59,7 @@ func resolveMemberAccess(root antlr.ParserRuleContext, scope expressionScope) (m base := identifier(casual.Id_expr().GetText()) qualified := false for _, rel := range scope.relations { - if rel.alias == base || rel.table.Name == base { + if rel.alias == base { qualified = true break } diff --git a/internal/analyzer/select_semantic.go b/internal/analyzer/select_semantic.go index 7ffd72e7..fd452d43 100644 --- a/internal/analyzer/select_semantic.go +++ b/internal/analyzer/select_semantic.go @@ -209,7 +209,7 @@ func resolvedColumnKey(relations []relation, ref columnRef) (string, error) { var key string matches := 0 for _, rel := range relations { - if ref.qualifier != "" && ref.qualifier != rel.alias && ref.qualifier != rel.table.Name { + if ref.qualifier != "" && ref.qualifier != rel.alias { continue } if tableColumn(rel.table, ref.name) != nil { diff --git a/internal/analyzer/semantic.go b/internal/analyzer/semantic.go index 49e3c5e5..4b36ac92 100644 --- a/internal/analyzer/semantic.go +++ b/internal/analyzer/semantic.go @@ -68,7 +68,11 @@ func analyzeQuery(catalog model.Catalog, block queryBlock) (model.AnalyzedQuery, if len(diagnostics) != 0 { return query, diagnostics } - query.Syntax = &model.QuerySyntax{Root: parsed.tree, Columns: map[int]model.ColumnBinding{}} + block.tablePathPrefix, diagnostics = tablePathPrefix(block.file, block.line-1, parsed.tree) + if len(diagnostics) != 0 { + return query, diagnostics + } + query.Syntax = &model.QuerySyntax{Root: parsed.tree, Columns: map[int]model.ColumnBinding{}, Tables: resolvedTableReferences(parsed.tree, block.tablePathPrefix), TablePathPrefix: block.tablePathPrefix} tree := collectQueryTree(parsed.tree) if diagnostics = unsupportedSQLCMacroDiagnostics(block, parsed.tokens); len(diagnostics) != 0 { return query, diagnostics @@ -132,17 +136,17 @@ func analyzeQuery(catalog model.Catalog, block queryBlock) (model.AnalyzedQuery, case len(tree.insert) == 1: target, diagnostics = targetTable(catalog, block, intoTableName(tree.insert[0]), tree.insert[0], diagnostics) if target != nil { - relations = []relation{{table: target, alias: target.Name}} + relations = []relation{{table: target, alias: intoTableName(tree.insert[0])}} } case len(tree.updates) == 1: target, diagnostics = targetTable(catalog, block, simpleTableName(tree.updates[0].Simple_table_ref()), tree.updates[0], diagnostics) if target != nil { - relations = []relation{{table: target, alias: target.Name}} + relations = []relation{{table: target, alias: simpleTableName(tree.updates[0].Simple_table_ref())}} } case len(tree.deletes) == 1: target, diagnostics = targetTable(catalog, block, simpleTableName(tree.deletes[0].Simple_table_ref()), tree.deletes[0], diagnostics) if target != nil { - relations = []relation{{table: target, alias: target.Name}} + relations = []relation{{table: target, alias: simpleTableName(tree.deletes[0].Simple_table_ref())}} } } @@ -317,7 +321,7 @@ func validateQueryStatements(block queryBlock, tree queryTree) []model.Diagnosti continue } switch { - case core.Declare_stmt() != nil: + case core.Declare_stmt() != nil, core.Pragma_stmt() != nil: case core.Named_nodes_stmt() != nil: case core.Select_stmt() != nil, core.Into_table_stmt() != nil, core.Update_stmt() != nil, core.Delete_stmt() != nil: mainStatements++ @@ -457,7 +461,7 @@ func selectRelations(catalog model.Catalog, block queryBlock, selectCore *parser } } else { key := tableRef.Table_key() - name := tableKeyName(key) + name := resolveTablePath(block.tablePathPrefix, tableKeyName(key)) table = findTable(catalog, name) if table == nil { diagnostics = append(diagnostics, diagnosticAt(block.file, block.line-1, source, fmt.Sprintf("unknown table %q", name))) @@ -476,6 +480,9 @@ func selectRelations(catalog model.Catalog, block queryBlock, selectCore *parser } } alias := table.Name + if tableRef.Table_key() != nil { + alias = tableKeyName(tableRef.Table_key()) + } if named.An_id() != nil { alias = identifier(named.An_id().GetText()) } @@ -535,7 +542,7 @@ func selectProjectionMode(block queryBlock, selectCore *parser.Select_coreContex matched := false var expressions []string for _, rel := range relations { - if prefix != "" && prefix != rel.alias && prefix != rel.table.Name { + if prefix != "" && prefix != rel.alias { continue } matched = true @@ -741,7 +748,7 @@ func validateColumnReferences(block queryBlock, root antlr.Tree, relations []rel func resolveColumn(relations []relation, ref columnRef) (model.Column, error) { var matches []model.Column for _, rel := range relations { - if ref.qualifier != "" && ref.qualifier != rel.alias && ref.qualifier != rel.table.Name { + if ref.qualifier != "" && ref.qualifier != rel.alias { continue } for _, column := range rel.table.Columns { @@ -869,7 +876,7 @@ func inferUpdate(block queryBlock, statement *parser.Update_stmtContext, table * diagnostics = append(diagnostics, diagnosticAt(block.file, block.line-1, clause, fmt.Sprintf("unknown column %q", name))) continue } - if err := validateDMLValue(clause.Expr(), *column, expressionScope{relations: []relation{{table: table, alias: table.Name}}, bindings: bindings, functions: block.functions}, inferred); err != nil { + if err := validateDMLValue(clause.Expr(), *column, expressionScope{relations: []relation{{table: table, alias: simpleTableName(statement.Simple_table_ref())}}, bindings: bindings, functions: block.functions}, inferred); err != nil { diagnostics = append(diagnostics, diagnosticAt(block.file, block.line-1, clause.Expr(), err.Error())) } } @@ -975,6 +982,7 @@ func returningProjection(block queryBlock, returning parser.IReturning_columns_l } func targetTable(catalog model.Catalog, block queryBlock, name string, ctx antlr.ParserRuleContext, diagnostics []model.Diagnostic) (*model.Table, []model.Diagnostic) { + name = resolveTablePath(block.tablePathPrefix, name) table := findTable(catalog, name) if table == nil { diagnostics = append(diagnostics, diagnosticAt(block.file, block.line-1, ctx, fmt.Sprintf("unknown table %q", name))) @@ -1024,7 +1032,7 @@ func recordColumnBindings(syntax *model.QuerySyntax, root antlr.Tree, relations } for _, ref := range columnRefs(root) { for _, relation := range relations { - if ref.qualifier != "" && !strings.EqualFold(ref.qualifier, relation.alias) { + if ref.qualifier != "" && ref.qualifier != relation.alias { continue } if column := tableColumn(relation.table, ref.name); column != nil { diff --git a/internal/analyzer/table_path_prefix_test.go b/internal/analyzer/table_path_prefix_test.go new file mode 100644 index 00000000..ab4cf227 --- /dev/null +++ b/internal/analyzer/table_path_prefix_test.go @@ -0,0 +1,407 @@ +package analyzer + +import ( + "context" + "fmt" + "reflect" + "slices" + "strings" + "testing" + + "github.com/ydb-platform/sqlc-ydb/internal/model" +) + +func TestTablePathPrefixResolvesDistinctNamespaces(t *testing.T) { + schema := []model.Source{{Name: "schema.sql", Text: "CREATE TABLE `/local/one/records` (id Uint32 NOT NULL, PRIMARY KEY(id)); CREATE TABLE `/local/two/records` (id Uint64 NOT NULL, PRIMARY KEY(id));"}} + for _, tc := range []struct{ prefix, kind string }{{"/local/one", "Uint32"}, {"/local/two", "Uint64"}} { + t.Run(tc.prefix, func(t *testing.T) { + sql := "-- name: Read :many\nPRAGMA TablePathPrefix('" + tc.prefix + "');\nSELECT records.id FROM records WHERE records.id = $id;" + result, err := Analyze(schema, []model.Source{{Name: "query.sql", Text: sql}}) + if err != nil { + t.Fatal(err) + } + query := result.Queries[0] + want := []model.Parameter{{Name: "id", Type: model.Type{Kind: tc.kind}}} + if query.SQL != sql || !reflect.DeepEqual(query.Parameters, want) { + t.Fatalf("query = %#v", query) + } + if !reflect.DeepEqual(query.Syntax.Relations, []model.TableBinding{{Table: tc.prefix + "/records", Alias: "records"}}) { + t.Fatalf("relations = %#v", query.Syntax.Relations) + } + column := query.ResultSets[0].Columns[0] + if column.Type.Kind != tc.kind || column.Table != tc.prefix+"/records" { + t.Fatalf("column = %#v", column) + } + }) + } +} + +func TestTablePathPrefixSchemaSourceScope(t *testing.T) { + schema := []model.Source{ + {Name: "one.sql", Text: "PRAGMA TablePathPrefix('/local/one'); CREATE TABLE records (id Uint32 NOT NULL, PRIMARY KEY(id));"}, + {Name: "two.sql", Text: "PRAGMA TablePathPrefix = '/local/two'; CREATE TABLE records (id Uint64 NOT NULL, PRIMARY KEY(id));"}, + {Name: "root.sql", Text: "CREATE TABLE records (id Utf8 NOT NULL, PRIMARY KEY(id));"}, + } + result, err := Analyze(schema, nil) + if err != nil { + t.Fatal(err) + } + var names []string + for _, table := range result.Catalog.Tables { + names = append(names, table.Name) + } + if !reflect.DeepEqual(names, []string{"/local/one/records", "/local/two/records", "records"}) { + t.Fatalf("catalog names = %v", names) + } +} + +func TestTablePathPrefixStaticFormsAndPaths(t *testing.T) { + for _, tc := range []struct{ pragma, source, path string }{ + {`PRAGMA TablePathPrefix('/local/one');`, "records", "/local/one/records"}, + {`pragma tablepathprefix = "/local/one";`, "records", "/local/one/records"}, + {`PRAGMA TablePathPrefix('/local/one'); PRAGMA TablePathPrefix('/local/one');`, "records", "/local/one/records"}, + {`PRAGMA TablePathPrefix('/local/one/./nested/../');`, "`./records`", "/local/one/records"}, + {`PRAGMA TablePathPrefix('/local/one');`, "`../two//records`", "/local/two/records"}, + {`PRAGMA TablePathPrefix('/local/one');`, "`/local/two/records`", "/local/two/records"}, + {`PRAGMA TablePathPrefix('/');`, "`local/two/records`", "/local/two/records"}, + {`PRAGMA TablePathPrefix('\x2flocal\u002fone');`, "records", "/local/one/records"}, + {`PRAGMA TablePathPrefix('\057local/one');`, "records", "/local/one/records"}, + {`PRAGMA TablePathPrefix('/local/имя');`, "`rec``ords`", "/local/имя/rec`ords"}, + {`PRAGMA TablePathPrefix('/local/quo\"te');`, "records", "/local/quo\"te/records"}, + {`PRAGMA TablePathPrefix("/local/quo\'te");`, "records", "/local/quo'te/records"}, + } { + t.Run(tc.pragma+tc.source, func(t *testing.T) { + schema := []model.Source{{Name: "schema.sql", Text: "CREATE TABLE " + quotedYQLIdentifier(tc.path) + " (id Uint32 NOT NULL, PRIMARY KEY(id));"}} + sql := "-- name: Read :many\n" + tc.pragma + "\n-- records and /local/one stay unchanged\nSELECT id FROM " + tc.source + ";" + result, err := Analyze(schema, []model.Source{{Name: "query.sql", Text: sql}}) + if err != nil { + t.Fatal(err) + } + query := result.Queries[0] + if query.SQL != sql || query.ResultSets[0].Columns[0].Table != tc.path { + t.Fatalf("query = %#v", query) + } + if len(query.Syntax.Tables) != 1 { + t.Fatalf("table mappings = %v", query.Syntax.Tables) + } + for _, name := range query.Syntax.Tables { + if name != tc.path { + t.Fatalf("physical path = %q", name) + } + } + }) + } +} + +func TestTablePathPrefixQueryScope(t *testing.T) { + schema := []model.Source{{Name: "schema.sql", Text: "CREATE TABLE `/local/one/records` (id Uint32 NOT NULL, PRIMARY KEY(id)); CREATE TABLE records (id Utf8 NOT NULL, PRIMARY KEY(id));"}} + queries := []model.Source{{Name: "query.sql", Text: "-- name: First :many\nPRAGMA TablePathPrefix('/local/one'); SELECT id FROM records;\n-- name: Second :many\nSELECT id FROM records;"}} + result, err := Analyze(schema, queries) + if err != nil { + t.Fatal(err) + } + if result.Queries[0].ResultSets[0].Columns[0].Type.Kind != "Uint32" || result.Queries[1].ResultSets[0].Columns[0].Type.Kind != "Utf8" || result.Queries[1].Syntax.TablePathPrefix != "" { + t.Fatalf("queries = %#v", result.Queries) + } +} + +func TestTablePathPrefixMigrations(t *testing.T) { + schema := []model.Source{{Name: "schema.sql", Text: `PRAGMA TablePathPrefix('/local/one'); +CREATE TABLE records (id Uint32 NOT NULL, PRIMARY KEY(id)); +CREATE TABLE IF NOT EXISTS records (ignored Uint32, PRIMARY KEY(ignored)); +ALTER TABLE records ADD COLUMN label Utf8; +ALTER TABLE records RENAME TO renamed; +ALTER TABLE renamed RENAME TO ` + "`/local/two/final`" + `; +CREATE TABLE disposable (id Uint32 NOT NULL, PRIMARY KEY(id)); +DROP TABLE disposable; +DROP TABLE IF EXISTS disposable; +ALTER TABLE ` + "`/local/two/final`" + ` ADD INDEX by_label GLOBAL SYNC ON(label);`}} + result, err := Analyze(schema, nil) + if err != nil { + t.Fatal(err) + } + if len(result.Catalog.Tables) != 1 { + t.Fatalf("tables = %#v", result.Catalog.Tables) + } + table := result.Catalog.Tables[0] + if table.Name != "/local/two/final" || len(table.Columns) != 2 || len(table.Indexes) != 1 { + t.Fatalf("table = %#v", table) + } + for _, column := range table.Columns { + if column.Table != table.Name { + t.Fatalf("column ownership = %#v", column) + } + } +} + +func TestTablePathPrefixDMLAndJoins(t *testing.T) { + schema := []model.Source{{Name: "schema.sql", Text: "PRAGMA TablePathPrefix('/local/one'); CREATE TABLE records (id Uint32 NOT NULL, label Utf8, INDEX by_label GLOBAL SYNC ON(label), PRIMARY KEY(id)); CREATE TABLE `/local/two/records` (id Uint32 NOT NULL, label Utf8, PRIMARY KEY(id));"}} + for _, sql := range []string{ + "INSERT INTO records (id, label) VALUES ($id, $label);", + "UPSERT INTO records (id, label) SELECT id, label FROM `/local/two/records`;", + "UPSERT INTO records SELECT * FROM `/local/two/records`;", + "UPDATE records SET label = $label WHERE records.id = $id;", + "UPDATE records SET id = records.id + 1u WHERE records.id = $id;", + "DELETE FROM records WHERE records.id = $id;", + "UPDATE records ON SELECT id, label FROM `/local/two/records`;", + "DELETE FROM records ON SELECT id FROM `/local/two/records`;", + } { + t.Run(sql, func(t *testing.T) { + result, err := Analyze(schema, []model.Source{{Name: "query.sql", Text: "-- name: Write :exec\nPRAGMA TablePathPrefix('/local/one'); " + sql}}) + if err != nil { + t.Fatal(err) + } + query := result.Queries[0] + var paths []string + for _, name := range query.Syntax.Tables { + paths = append(paths, name) + } + if !slices.Contains(paths, "/local/one/records") { + t.Fatalf("target mapping = %v", paths) + } + if strings.Contains(sql, "SELECT") && !slices.Contains(paths, "/local/two/records") { + t.Fatalf("source mapping = %v", paths) + } + }) + } + for _, projection := range []string{"r.*", "r.id, s.label"} { + sql := "-- name: Joined :many\nPRAGMA TablePathPrefix('/local/one'); SELECT " + projection + " FROM records VIEW by_label AS r JOIN `/local/two/records` AS s ON r.id = s.id;" + result, err := Analyze(schema, []model.Source{{Name: "query.sql", Text: sql}}) + if err != nil { + t.Fatal(err) + } + query := result.Queries[0] + want := []model.TableBinding{{Table: "/local/one/records", Alias: "r"}, {Table: "/local/two/records", Alias: "s"}} + if !reflect.DeepEqual(query.Syntax.Relations, want) { + t.Fatalf("relations = %#v", query.Syntax.Relations) + } + columns := query.ResultSets[0].Columns + if projection == "r.*" && (columns[0].ResultName() != "id" || columns[1].ResultName() != "label") { + t.Fatalf("wildcard keys = %#v", columns) + } + if projection != "r.*" && (columns[0].ResultName() != "r.id" || columns[1].ResultName() != "s.label") { + t.Fatalf("qualified keys = %#v", columns) + } + } +} + +func TestTablePathPrefixUsesAuthoredQualifier(t *testing.T) { + schema := []model.Source{{Name: "schema.sql", Text: "CREATE TABLE `/local/one/nested/records` (id Uint32 NOT NULL, PRIMARY KEY(id));"}} + for _, qualifier := range []string{"`nested/records`", "records", "`/local/one/nested/records`"} { + sql := "-- name: Read :many\nPRAGMA TablePathPrefix('/local/one'); SELECT " + qualifier + ".id FROM `nested/records`;" + _, err := Analyze(schema, []model.Source{{Name: "query.sql", Text: sql}}) + if qualifier == "`nested/records`" { + if err != nil { + t.Fatal(err) + } + } else if err == nil || !strings.Contains(err.Error(), "unknown column") { + t.Fatalf("qualifier %s: %v", qualifier, err) + } + } +} + +func TestTablePathPrefixRejectsUnsupportedPragmas(t *testing.T) { + for _, tc := range []struct{ sql, message string }{ + {`PRAGMA TablePathPrefix('');`, "nonempty absolute path"}, + {`PRAGMA TablePathPrefix('local/one');`, "nonempty absolute path"}, + {`PRAGMA TablePathPrefix($prefix);`, "static quoted absolute path"}, + {`PRAGMA TablePathPrefix = default;`, "reset forms are unsupported"}, + {`PRAGMA TablePathPrefix;`, "static quoted absolute path"}, + {`PRAGMA TablePathPrefix('/one', '/two');`, "one static quoted absolute path"}, + {`PRAGMA TablePathPrefix('/one'); PRAGMA TablePathPrefix('/two');`, "conflicting PRAGMA"}, + {`PRAGMA TablePathPrefix('/local' || '/one');`, "input"}, + {`PRAGMA TablePathPrefix('/local' u);`, "input"}, + {`PRAGMA TablePathPrefix("/local"u);`, "ordinary quoted string"}, + {`PRAGMA TablePathPrefix('/local\n/one');`, "control character"}, + {`PRAGMA TablePathPrefix('/local\q/one');`, "unsupported escape"}, + {`PRAGMA ydb.TablePathPrefix('/local/one');`, "only static TablePathPrefix"}, + {`PRAGMA AnsiInForEmptyOrNullableItemsCollections;`, "only static TablePathPrefix"}, + {`$x = 1; PRAGMA TablePathPrefix('/local/one');`, "must precede"}, + } { + t.Run(tc.sql, func(t *testing.T) { + _, err := Analyze(nil, []model.Source{{Name: "query.sql", Text: "-- name: Read :many\n" + tc.sql + "SELECT id FROM records;"}}) + if err == nil || !strings.Contains(err.Error(), tc.message) { + t.Fatalf("error = %v; want %q", err, tc.message) + } + _, err = Analyze([]model.Source{{Name: "schema.sql", Text: tc.sql + "CREATE TABLE records (id Uint32 NOT NULL, PRIMARY KEY(id));"}}, nil) + if err == nil || !strings.Contains(err.Error(), tc.message) { + t.Fatalf("schema error = %v; want %q", err, tc.message) + } + }) + } + for _, statement := range []string{"SELECT id FROM records;", "CREATE TABLE records (id Uint32 NOT NULL, PRIMARY KEY(id));"} { + sql := statement + " PRAGMA TablePathPrefix('/local/one');" + parsed, ds := parseYQL("query.sql", sql, 0) + if len(ds) != 0 { + t.Fatal(ds) + } + _, ds = tablePathPrefix("query.sql", 0, parsed.tree) + if len(ds) != 1 || !strings.Contains(ds[0].Message, "must precede") { + t.Fatalf("late pragma diagnostics = %v", ds) + } + } +} + +func TestTablePathPrefixDatabaseDiscoveryAndDrift(t *testing.T) { + for _, withSchema := range []bool{false, true} { + t.Run(fmt.Sprint(withSchema), func(t *testing.T) { + database := &fakeAnalysisDatabase{tables: map[string]model.Table{"/local/one/records": databaseTestTable("name"), "/local/two/records": databaseTestTable("name")}} + queries := []model.Source{{Name: "query.sql", Text: "-- name: First :many\nPRAGMA TablePathPrefix('/local/one'); SELECT id FROM records;\n-- name: Second :exec\nPRAGMA TablePathPrefix('/local/two'); UPSERT INTO records SELECT id, name FROM `/local/one/records`;"}} + var schema []model.Source + if withSchema { + for _, prefix := range []string{"/local/one", "/local/two"} { + schema = append(schema, model.Source{Name: prefix + ".sql", Text: "PRAGMA TablePathPrefix('" + prefix + "'); CREATE TABLE records (id Uint64 NOT NULL, name Utf8 NOT NULL, PRIMARY KEY(id));"}) + } + } + result, err := AnalyzeWithDatabase(context.Background(), schema, queries, Options{}, database) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(database.described, []string{"/local/one/records", "/local/two/records"}) || len(result.Catalog.Tables) != 2 { + t.Fatalf("described=%v tables=%#v", database.described, result.Catalog.Tables) + } + if len(database.validated) != 2 || !strings.Contains(database.validated[1], "PRAGMA TablePathPrefix('/local/two')") { + t.Fatalf("validated=%v", database.validated) + } + if withSchema { + table := database.tables["/local/two/records"] + table.Columns[0].Type = model.Type{Kind: "Uint32"} + database.tables["/local/two/records"] = table + _, err = AnalyzeWithDatabase(context.Background(), schema, queries, Options{}, database) + if err == nil || !strings.Contains(err.Error(), `database schema drift for table "/local/two/records"`) { + t.Fatalf("drift = %v", err) + } + } + }) + } +} + +func TestTablePathPrefixPreservesTextAndDMLReturning(t *testing.T) { + schema := []model.Source{{Name: "schema.sql", Text: "CREATE TABLE `/local/one/records` (id Uint32 NOT NULL, PRIMARY KEY(id));"}} + sql := "-- name: Read :many\r\nPRAGMA /* records */ TablePathPrefix = '/local/one';\r\nDECLARE $id AS Uint32;\r\nSELECT records.id, 'records /local/one PRAGMA' AS marker FROM records WHERE records.id = $id;" + result, err := Analyze(schema, []model.Source{{Name: "query.sql", Text: sql}}) + if err != nil { + t.Fatal(err) + } + if result.Queries[0].SQL != sql { + t.Fatalf("SQL changed: %q", result.Queries[0].SQL) + } + for _, statement := range []string{"INSERT INTO records (id) VALUES ($id)", "UPSERT INTO records (id) VALUES ($id)", "UPDATE records SET id = records.id + 1u WHERE records.id = $id", "DELETE FROM records WHERE records.id = $id"} { + sql = "-- name: Write :many\nPRAGMA TablePathPrefix('/local/one'); " + statement + " RETURNING *;" + result, err = Analyze(schema, []model.Source{{Name: "query.sql", Text: sql}}) + if err != nil { + t.Fatal(err) + } + query := result.Queries[0] + if !strings.Contains(query.SQL, "RETURNING `id`") || !strings.Contains(query.SQL, "PRAGMA TablePathPrefix('/local/one')") || query.ResultSets[0].Columns[0].Table != "/local/one/records" { + t.Fatalf("query = %#v", query) + } + } +} + +func TestTablePathPrefixDatabaseRejectsUnsupportedBeforeDiscovery(t *testing.T) { + database := &fakeAnalysisDatabase{} + _, err := AnalyzeWithDatabase(context.Background(), nil, []model.Source{{Name: "query.sql", Text: "-- name: Read :many\nPRAGMA TablePathPrefix($prefix); SELECT id FROM records;"}}, Options{}, database) + if err == nil || !strings.Contains(err.Error(), "static quoted absolute path") || len(database.described) != 0 { + t.Fatalf("error=%v described=%v", err, database.described) + } +} + +func TestTablePathPrefixDoesNotEnableAnsiDialect(t *testing.T) { + _, err := Analyze(nil, []model.Source{{Name: "query.sql", Text: "-- name: Read :many\nPRAGMA ANSI 1;"}}) + if err == nil || !strings.Contains(err.Error(), "exactly one supported SELECT") { + t.Fatalf("error = %v", err) + } +} + +func TestTableAliasIsTheOnlyQualifier(t *testing.T) { + for _, prefix := range []string{"", "/local/one"} { + t.Run(prefix, func(t *testing.T) { + pragma := "" + if prefix != "" { + pragma = "PRAGMA TablePathPrefix('" + prefix + "'); " + } + schema := []model.Source{{Name: "schema.sql", Text: pragma + "CREATE TABLE records (id Uint32 NOT NULL, meta Struct NOT NULL, PRIMARY KEY(id));"}} + for _, tc := range []struct{ expression, error string }{ + {"records.id", "unknown column"}, + {"records.*", "unknown table or alias"}, + {"records.meta.value AS value", "unknown column"}, + {"R.id", "unknown column"}, + {"R.*", "unknown table or alias"}, + {"r.id", ""}, + {"r.*", ""}, + {"r.meta.value AS value", ""}, + } { + _, err := Analyze(schema, []model.Source{{Name: "query.sql", Text: "-- name: Read :many\n" + pragma + "SELECT " + tc.expression + " FROM records AS r;"}}) + if tc.error == "" { + if err != nil { + t.Fatal(err) + } + } else if err == nil || !strings.Contains(err.Error(), tc.error) { + t.Fatalf("expression %q: error=%v want=%s", tc.expression, err, tc.error) + } + } + }) + } +} + +func TestColumnBindingsPreserveAliasCase(t *testing.T) { + for _, prefix := range []string{"", "/local/one"} { + t.Run(prefix, func(t *testing.T) { + pragma := "" + if prefix != "" { + pragma = "PRAGMA TablePathPrefix('" + prefix + "'); " + } + schema := []model.Source{{Name: "schema.sql", Text: pragma + "CREATE TABLE records (id Uint32 NOT NULL, PRIMARY KEY(id)); CREATE TABLE other_records (id Uint64 NOT NULL, PRIMARY KEY(id));"}} + sql := "-- name: Read :many\n" + pragma + "SELECT r.id AS lower_id, R.id AS upper_id FROM records AS r JOIN other_records AS R ON r.id = R.id WHERE R.id = $id;" + result, err := Analyze(schema, []model.Source{{Name: "query.sql", Text: sql}}) + if err != nil { + t.Fatal(err) + } + query := result.Queries[0] + if query.ResultSets[0].Columns[0].Type.Kind != "Uint32" || query.ResultSets[0].Columns[1].Type.Kind != "Uint64" || !reflect.DeepEqual(query.Parameters, []model.Parameter{{Name: "id", Type: model.Type{Kind: "Uint64"}}}) { + t.Fatalf("query types = %#v, parameters = %#v", query.ResultSets, query.Parameters) + } + bindings := 0 + for _, ref := range columnRefs(query.Syntax.Root) { + wantTable, wantType := "records", "Uint32" + if ref.qualifier == "R" { + wantTable, wantType = "other_records", "Uint64" + } + if prefix != "" { + wantTable = prefix + "/" + wantTable + } + binding, ok := query.Syntax.Columns[ref.ctx.GetStart().GetTokenIndex()] + if !ok || binding.Alias != ref.qualifier || binding.Table != wantTable || binding.Column.Type.Kind != wantType { + t.Errorf("binding for %s = %#v, want alias %q, table %q, type %s", qualifiedName(ref), binding, ref.qualifier, wantTable, wantType) + } + bindings++ + } + if bindings != 5 { + t.Fatalf("checked %d column bindings, want 5", bindings) + } + }) + } +} + +func TestTablePathPrefixDeclarationAndAssignmentOrdering(t *testing.T) { + schema := []model.Source{{Name: "schema.sql", Text: "CREATE TABLE `/local/one/records` (id Uint32 NOT NULL, PRIMARY KEY(id));"}} + const declaration = "DECLARE $id AS Uint32; " + const pragma = "PRAGMA TablePathPrefix('/local/one'); " + const assignment = "$key = $id; " + for _, preamble := range []string{declaration + pragma, pragma + declaration, pragma + declaration + pragma} { + sql := "-- name: Read :many\n" + preamble + assignment + "SELECT id FROM records WHERE id = $key;" + result, err := Analyze(schema, []model.Source{{Name: "query.sql", Text: sql}}) + if err != nil { + t.Fatal(err) + } + query := result.Queries[0] + if query.SQL != sql || !reflect.DeepEqual(query.Parameters, []model.Parameter{{Name: "id", Type: model.Type{Kind: "Uint32"}}}) { + t.Fatalf("query = %#v", query) + } + } + _, err := Analyze(schema, []model.Source{{Name: "query.sql", Text: "-- name: Read :many\n" + declaration + assignment + pragma + "SELECT id FROM records WHERE id = $key;"}}) + if err == nil || !strings.Contains(err.Error(), "must precede local assignments ($name = ...)") || !strings.Contains(err.Error(), "DECLARE may precede the pragma") { + t.Fatalf("late pragma error = %v", err) + } +} diff --git a/internal/analyzer/table_paths.go b/internal/analyzer/table_paths.go new file mode 100644 index 00000000..23648f5c --- /dev/null +++ b/internal/analyzer/table_paths.go @@ -0,0 +1,112 @@ +package analyzer + +import ( + "fmt" + "path" + "strconv" + "strings" + "unicode" + + "github.com/antlr4-go/antlr/v4" + "github.com/ydb-platform/sqlc-ydb/internal/model" + parser "github.com/ydb-platform/yql-parsers/go" +) + +// A single leading absolute prefix is the supported compiler contract. +// Its scope is one schema source or one named query, never another file. +func tablePathPrefix(file string, lineOffset int, root parser.ISql_queryContext) (string, []model.Diagnostic) { + var prefix string + var diagnostics []model.Diagnostic + if root.Sql_stmt_list() == nil { + return prefix, diagnostics + } + started := false + for _, statement := range root.Sql_stmt_list().AllSql_stmt() { + core := statement.Sql_stmt_core() + pragma := core.Pragma_stmt() + if pragma == nil { + if core.Declare_stmt() == nil { + started = true + } + continue + } + message := "" + switch { + case pragma.Opt_id_prefix_or_type().GetText() != "" || !strings.EqualFold(identifier(pragma.An_id().GetText()), "TablePathPrefix"): + message = "unsupported PRAGMA; only static TablePathPrefix is supported" + case started: + message = "PRAGMA TablePathPrefix must precede local assignments ($name = ...) and data or schema statements; external parameter DECLARE may precede the pragma" + case len(pragma.AllPragma_value()) != 1 || pragma.Pragma_value(0).STRING_VALUE() == nil: + message = "PRAGMA TablePathPrefix requires one static quoted absolute path, for example '/database/folder'; dynamic values and reset forms are unsupported" + default: + value, err := tablePathPrefixLiteral(pragma.Pragma_value(0).STRING_VALUE().GetText()) + if err != nil { + message = err.Error() + } else if prefix != "" && prefix != value { + message = "conflicting PRAGMA TablePathPrefix values are unsupported; use one prefix per named query or schema file" + } else { + prefix = value + } + } + if message != "" { + diagnostics = append(diagnostics, diagnosticAt(file, lineOffset, pragma, message)) + } + } + return prefix, diagnostics +} + +func tablePathPrefixLiteral(literal string) (string, error) { + invalid := func() (string, error) { + return "", fmt.Errorf("PRAGMA TablePathPrefix requires a nonempty absolute path in an ordinary quoted string, for example '/database/folder'") + } + if len(literal) < 2 || (literal[0] != '\'' && literal[0] != '"') || literal[len(literal)-1] != literal[0] { + return invalid() + } + var value strings.Builder + for rest := literal[1 : len(literal)-1]; rest != ""; { + if len(rest) >= 2 && rest[0] == '\\' && (rest[1] == '\'' || rest[1] == '"') { + value.WriteByte(rest[1]) + rest = rest[2:] + continue + } + character, multibyte, tail, err := strconv.UnquoteChar(rest, literal[0]) + if err != nil || unicode.IsControl(character) { + return "", fmt.Errorf("unsupported escape or control character in PRAGMA TablePathPrefix; use a quoted literal path") + } + if multibyte { + value.WriteRune(character) + } else { + value.WriteByte(byte(character)) + } + rest = tail + } + if !path.IsAbs(value.String()) { + return invalid() + } + return path.Clean(value.String()), nil +} + +func resolveTablePath(prefix, name string) string { + if path.IsAbs(name) { + return path.Clean(name) + } + if prefix == "" || name == "" { + return name + } + return path.Join(prefix, name) +} + +func resolvedTableReferences(root antlr.Tree, prefix string) map[int]string { + tables := map[int]string{} + descendants(root, func(node antlr.Tree) { + switch ref := node.(type) { + case *parser.Table_keyContext: + tables[ref.Id_table_or_type().GetStart().GetTokenIndex()] = resolveTablePath(prefix, tableKeyName(ref)) + case *parser.Simple_table_refContext: + if core := ref.Simple_table_ref_core(); core != nil { + tables[core.GetStart().GetTokenIndex()] = resolveTablePath(prefix, simpleTableName(ref)) + } + } + }) + return tables +} diff --git a/internal/codegen/java/generator.go b/internal/codegen/java/generator.go index 060604e5..504b3b28 100644 --- a/internal/codegen/java/generator.go +++ b/internal/codegen/java/generator.go @@ -88,6 +88,16 @@ func name(s string, upper bool) (string, error) { return n, nil } +// Table paths retain their namespace in generated types; other identifiers do +// not admit path separators. +func tableName(path string) (string, error) { + n, err := name(strings.ReplaceAll(path, "/", "_"), true) + if err != nil { + return "", fmt.Errorf("cannot represent %q as a Java identifier", path) + } + return n, nil +} + // quoted also escapes backslashes preceding u: Java Unicode escapes are processed // before tokenization. Doubling every input backslash keeps them literal. func quoted(s string) string { @@ -199,7 +209,7 @@ func Generate(a *model.AnalysisResult, o Options) ([]model.File, error) { return nil } for _, table := range a.Catalog.Tables { - n, err := name(table.Name, true) + n, err := tableName(table.Name) if err != nil { return nil, err } diff --git a/internal/codegen/java/jooq.go b/internal/codegen/java/jooq.go index a884df96..d1bc9646 100644 --- a/internal/codegen/java/jooq.go +++ b/internal/codegen/java/jooq.go @@ -38,6 +38,10 @@ func jooqConstant(s string) (string, error) { return result, nil } +func jooqTableConstant(path string) (string, error) { + return jooqConstant(strings.ReplaceAll(path, "/", "_")) +} + func generateJooq(a *model.AnalysisResult, o Options) ([]model.File, error) { header := "// Code generated by sqlc-ydb. DO NOT EDIT.\npackage " + o.Package + ";\n\n" files := []model.File{} @@ -45,12 +49,12 @@ func generateJooq(a *model.AnalysisResult, o Options) ([]model.File, error) { tables.WriteString(header + "import org.jooq.Record;\nimport org.jooq.Table;\nimport org.jooq.TableField;\nimport org.jooq.impl.TableImpl;\nimport tech.ydb.jooq.YdbTypes;\n\nimport static org.jooq.impl.DSL.name;\n\npublic final class Tables {\n private Tables() {}\n") tableNames := map[string]bool{} for _, t := range a.Catalog.Tables { - cn, err := name(t.Name, true) + cn, err := tableName(t.Name) if err != nil { return nil, err } cn += "Table" - constant, err := jooqConstant(t.Name) + constant, err := jooqTableConstant(t.Name) if err != nil { return nil, err } @@ -223,19 +227,25 @@ func generateJooq(a *model.AnalysisResult, o Options) ([]model.File, error) { b.WriteString(" }\n") continue } + isSelect := len(jooqNodes[*parser.Select_coreContext](q.Syntax.Root)) != 0 for _, rel := range q.Syntax.Relations { - tn, e := jooqConstant(rel.Table) + tn, e := jooqTableConstant(rel.Table) if e != nil { return nil, e } alias := tn - if rel.Alias != rel.Table { - alias, e = name(rel.Alias, false) + if isSelect && rel.Alias != rel.Table { + alias, e = name(strings.ReplaceAll(rel.Alias, "/", "_"), false) if e != nil { return nil, e } if seen[alias] { - return nil, fmt.Errorf("%s: Java alias collision: %s", q.Name, alias) + for authored, generated := range r.aliases { + if generated == alias { + return nil, fmt.Errorf("%s: Java alias collision: %s (from %q and %q)", q.Name, alias, authored, rel.Alias) + } + } + return nil, fmt.Errorf("%s: Java alias collision: %s (from %q)", q.Name, alias, rel.Alias) } seen[alias] = true fmt.Fprintf(&b, " var %s = %s.as(%s);\n", alias, tn, quoted(rel.Alias)) @@ -246,7 +256,27 @@ func generateJooq(a *model.AnalysisResult, o Options) ([]model.File, error) { if r.err != nil { return nil, fmt.Errorf("%s: %w", q.Name, r.err) } - if q.Command == model.Exec { + if q.Syntax.TablePathPrefix != "" { + // Keep the pragma with the typed statement: a relative table mapping + // must inherit the same prefix as the declared JDBC execution path. + pragmas := jooqNodes[*parser.Pragma_stmtContext](q.Syntax.Root) + text := string([]rune(q.SQL)[pragmas[0].GetStart().GetStart() : pragmas[len(pragmas)-1].GetStop().GetStop()+1]) + fmt.Fprintf(&b, " var stmt = %s;\n\n", body) + if q.Command == model.Exec { + fmt.Fprintf(&b, " dsl.query(\"{0};\\n{1}\", sql(%s), stmt).execute();\n", sqlLiteral(text)) + } else { + var fields []string + for _, c := range q.ResultSets[0].Columns { + _, dataType, _ := jooqType(c.Type) + fields = append(fields, "field(name("+quoted(c.ResultName())+"), "+dataType+")") + } + fetch := "fetchOptional" + if q.Command == model.Many { + fetch = "fetch" + } + fmt.Fprintf(&b, " return dsl.resultQuery(\"{0};\\n{1}\", sql(%s), stmt)\n .coerce(%s)\n .%s(mapping(%s::new));\n", sqlLiteral(text), strings.Join(fields, ", "), fetch, row) + } + } else if q.Command == model.Exec { b.WriteString(" " + body + "\n .execute();\n") } else { fetch := "fetchOptional" @@ -432,6 +462,9 @@ func (r *jooqRenderer) statement() string { root := r.query.Syntax.Root for _, stmt := range jooqNodes[*parser.Sql_stmtContext](root) { core := stmt.Sql_stmt_core() + if core.Pragma_stmt() != nil && r.query.Syntax.TablePathPrefix != "" { + continue + } isDML := core.Into_table_stmt() != nil || core.Update_stmt() != nil || core.Delete_stmt() != nil if isDML && len(jooqNodes[*parser.Select_coreContext](core)) > 0 { r.err = fmt.Errorf("SELECT-backed DML is unsupported by the jOOQ DSL; use runtime: jdbc or ydb") @@ -519,7 +552,7 @@ func (r *jooqRenderer) tableSource(source parser.IFlatten_sourceContext, rel mod if view == nil { return r.aliases[rel.Alias] } - table, err := jooqConstant(rel.Table) + table, err := jooqTableConstant(rel.Table) if err != nil { r.err = err return "" diff --git a/internal/codegen/java/jooq_declared.go b/internal/codegen/java/jooq_declared.go index 99b113bc..87397e9c 100644 --- a/internal/codegen/java/jooq_declared.go +++ b/internal/codegen/java/jooq_declared.go @@ -27,7 +27,11 @@ func jooqDeclaredSQL(q model.AnalyzedQuery, sql string) (string, error) { return nil } original := jooqID(ctx.GetText()) - constant, err := jooqConstant(original) + table, ok := q.Syntax.Tables[ctx.GetStart().GetTokenIndex()] + if !ok { + return fmt.Errorf("%s: missing resolved jOOQ table %q", q.Name, original) + } + constant, err := jooqTableConstant(table) if err != nil { return err } @@ -58,22 +62,28 @@ func jooqDeclaredSQL(q model.AnalyzedQuery, sql string) (string, error) { } } } - targets := map[string]bool{} + targets := map[model.TableBinding]bool{} for _, ref := range jooqNodes[*parser.Simple_table_ref_coreContext](q.Syntax.Root) { if err := add(ref, false); err != nil { return "", err } - targets[jooqID(ref.GetText())] = true + targets[model.TableBinding{Table: q.Syntax.Tables[ref.GetStart().GetTokenIndex()], Alias: jooqID(ref.GetText())}] = true } // DML targets have no SELECT alias to retain the original qualifier after mapping. terminals := jooqNodes[antlr.TerminalNode](q.Syntax.Root) +columns: for i, ref := range terminals { token := ref.GetSymbol() binding, ok := q.Syntax.Columns[token.GetTokenIndex()] - if !ok || !targets[binding.Table] || binding.Alias != binding.Table || i+1 == len(terminals) || terminals[i+1].GetText() != "." || seen[token.GetStart()] { + if !ok || !targets[binding.TableBinding] || i+1 == len(terminals) || terminals[i+1].GetText() != "." || seen[token.GetStart()] { continue } - constant, err := jooqConstant(binding.Table) + for parent := ref.GetParent(); parent != nil; parent = parent.GetParent() { + if _, ok := parent.(*parser.Select_coreContext); ok { + continue columns + } + } + constant, err := jooqTableConstant(binding.Table) if err != nil { return "", err } diff --git a/internal/codegen/java/jooq_prefix_test.go b/internal/codegen/java/jooq_prefix_test.go new file mode 100644 index 00000000..5535b472 --- /dev/null +++ b/internal/codegen/java/jooq_prefix_test.go @@ -0,0 +1,251 @@ +package java + +import ( + "encoding/base64" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/ydb-platform/sqlc-ydb/internal/analyzer" + "github.com/ydb-platform/sqlc-ydb/internal/codegen/jdbc" + "github.com/ydb-platform/sqlc-ydb/internal/model" +) + +const jooqPrefixSchema = "CREATE TABLE `/local/a/users` (id Uint64 NOT NULL, name Utf8 NOT NULL, INDEX by_name GLOBAL SYNC ON(name), PRIMARY KEY(id));\nCREATE TABLE `/local/b/users` (id Uint64 NOT NULL, name Utf8 NOT NULL, PRIMARY KEY(id));" +const jooqPrefixPragma = "PRAGMA /* namespace 🚀 {0} */ TablePathPrefix = '/local/a'" + +func TestJooqPrefixDSL(t *testing.T) { + queries := `-- name: Read :many +` + jooqPrefixPragma + `; +-- repeated static prefix stays attached to the statement +PRAGMA TablePathPrefix('/local/a'); +SELECT users.id FROM users VIEW by_name WHERE users.name = $name; +-- name: JoinUsers :many +` + jooqPrefixPragma + `; +SELECT a.id AS id FROM users AS a JOIN ` + "`/local/b/users`" + ` AS b ON a.id = b.id WHERE a.name = $name; +-- name: Write :exec +` + jooqPrefixPragma + `; +UPSERT INTO users (id, name) VALUES ($id, $name); +-- name: Remove :one +` + jooqPrefixPragma + `; +DELETE FROM users WHERE users.id = $id RETURNING id; +-- name: ReadPath :many +` + jooqPrefixPragma + `; +SELECT ` + "`../a/users`.id FROM `../a/users` WHERE `../a/users`.name = $name;" + ` +-- name: QuestionSingle :many +PRAGMA TablePathPrefix('/db/?w'); +SELECT u.id FROM ` + "`/local/a/users`" + ` AS u WHERE u.name = $name; +-- name: QuestionDouble :many +PRAGMA TablePathPrefix("/db/?w"); +SELECT u.id FROM ` + "`/local/a/users`" + ` AS u WHERE u.name = $name; +` + a, err := analyzer.Analyze([]model.Source{{Name: "schema.sql", Text: jooqPrefixSchema}}, []model.Source{{Name: "queries.sql", Text: queries}}) + if err != nil { + t.Fatal(err) + } + files, err := Generate(a, Options{Package: "prefix", Runtime: "jooq"}) + if err != nil { + t.Fatal(err) + } + code := string(files[len(files)-1].Content) + for _, want := range []string{jooqPrefixPragma, `dsl.resultQuery("{0};\n{1}"`, `dsl.query("{0};\n{1}"`, "LOCAL_A_USERS", "LOCAL_B_USERS"} { + if !strings.Contains(code, want) { + t.Fatalf("missing %q in %s", want, code) + } + } + if strings.Contains(code, "DECLARE") { + t.Fatal("inferred parameters must retain DSL bindings", code) + } + t.Run("published dialect", func(t *testing.T) { runJooqPrefixSDK(t, files) }) +} + +func TestJooqAliasCollisionNamesAuthoredAlias(t *testing.T) { + for _, aliases := range [][2]string{{"a/b", "a_b"}, {"a_b", "a/b"}} { + t.Run(aliases[0]+" then "+aliases[1], func(t *testing.T) { + query := fmt.Sprintf("-- name: Colliding :many\nSELECT `%s`.id FROM `/local/a/users` AS `%s` JOIN `/local/b/users` AS `%s` ON `%s`.id = `%s`.id;", aliases[0], aliases[0], aliases[1], aliases[0], aliases[1]) + analysis, err := analyzer.Analyze([]model.Source{{Name: "schema.sql", Text: jooqPrefixSchema}}, []model.Source{{Name: "query.sql", Text: query}}) + if err != nil { + t.Fatal(err) + } + files, err := Generate(analysis, Options{Package: "prefix", Runtime: "jooq"}) + want := fmt.Sprintf("Colliding: Java alias collision: aB (from %q and %q)", aliases[0], aliases[1]) + if files != nil || err == nil || err.Error() != want { + t.Fatalf("files %v, error %v; want %q", files, err, want) + } + }) + } +} + +func TestJooqAliasCollisionWithParametersAndLocals(t *testing.T) { + for _, tc := range []struct{ alias, parameter, identifier string }{ + {"a/b", "a_b", "aB"}, + {"stmt", "id", "stmt"}, + } { + t.Run(tc.alias, func(t *testing.T) { + query := fmt.Sprintf("-- name: Colliding :many\nSELECT `%s`.id FROM `/local/a/users` AS unaffected JOIN `/local/b/users` AS `%s` ON unaffected.id = `%s`.id WHERE `%s`.id = $%s;", tc.alias, tc.alias, tc.alias, tc.alias, tc.parameter) + analysis, err := analyzer.Analyze([]model.Source{{Name: "schema.sql", Text: jooqPrefixSchema}}, []model.Source{{Name: "query.sql", Text: query}}) + if err != nil { + t.Fatal(err) + } + files, err := Generate(analysis, Options{Package: "prefix", Runtime: "jooq"}) + want := fmt.Sprintf("Colliding: Java alias collision: %s (from %q)", tc.identifier, tc.alias) + if files != nil || err == nil || err.Error() != want { + t.Fatalf("files %v, error %v; want %q", files, err, want) + } + }) + } +} + +// Generate must reject incomplete semantic input rather than mapping an authored +// relative table name as though it were the resolved physical namespace. +func TestJooqPrefixRejectsMissingTableResolution(t *testing.T) { + for _, tc := range []struct{ name, command, statement string }{ + {"select", ":many", "SELECT users.id FROM users WHERE users.id = $id;"}, + {"delete", ":exec", "DELETE FROM users WHERE users.id = $id;"}, + } { + t.Run(tc.name, func(t *testing.T) { + query := "-- name: Incomplete " + tc.command + "\n" + jooqPrefixPragma + ";\nDECLARE $id AS Uint64;\n" + tc.statement + analysis, err := analyzer.Analyze([]model.Source{{Name: "schema.sql", Text: jooqPrefixSchema}}, []model.Source{{Name: "query.sql", Text: query}}) + if err != nil { + t.Fatal(err) + } + // The analyzer normally supplies this metadata. Exercise the generator's + // explicit invalid-input contract while retaining the real parsed query. + analysis.Queries[0].Syntax.Tables = nil + files, err := Generate(analysis, Options{Package: "prefix", Runtime: "jooq"}) + if files != nil || err == nil || err.Error() != `Incomplete: missing resolved jOOQ table "users"` { + t.Fatalf("incomplete table resolution produced files %v, error %v", files, err) + } + }) + } +} + +func TestJooqPrefixDeclaredSQLBytesThroughJava(t *testing.T) { + header := "-- name: Read :many\n" + jooqPrefixPragma + ";\nDECLARE $id AS Uint64;\n-- literals and comments are retained 🪄\n" + var program strings.Builder + program.WriteString("public class Main { static final String LOCAL_A_USERS = \"`/local/a/mapped_users`\", LOCAL_B_USERS = \"`/local/b/mapped_users`\"; static final Main dsl = new Main(); String render(String table) { return table; } public static void main(String[] args) {\n") + for i, tc := range []struct{ sql, want string }{ + {"SELECT users.id FROM users WHERE users.id = $id;", "SELECT users.id FROM `/local/a/mapped_users` AS `users` WHERE users.id = $id;"}, + {"SELECT users.id FROM users /* index 🐘 */ VIEW by_name WHERE users.id = $id;", "SELECT users.id FROM `/local/a/mapped_users` /* index 🐘 */ VIEW by_name AS `users` WHERE users.id = $id;"}, + {"SELECT a.id AS id FROM users AS a JOIN `/local/b/users` AS b ON a.id = b.id WHERE a.id = $id;", "SELECT a.id AS id FROM `/local/a/mapped_users` AS a JOIN `/local/b/mapped_users` AS b ON a.id = b.id WHERE a.id = $id;"}, + {"DELETE FROM users WHERE users.id = $id RETURNING id;", "DELETE FROM `/local/a/mapped_users` WHERE `/local/a/mapped_users`.id = $id RETURNING id;"}, + {"SELECT users.id, 'users /local/a/users'u AS label FROM users WHERE users.id = $id;", "SELECT users.id, 'users /local/a/users'u AS label FROM `/local/a/mapped_users` AS `users` WHERE users.id = $id;"}, + {"SELECT `../a/users`.id FROM `../a/users` WHERE `../a/users`.id = $id;", "SELECT `../a/users`.id FROM `/local/a/mapped_users` AS `../a/users` WHERE `../a/users`.id = $id;"}, + {"UPSERT INTO users SELECT users.id AS id, users.name AS name FROM users WHERE users.id = $id;", "UPSERT INTO `/local/a/mapped_users` SELECT users.id AS id, users.name AS name FROM `/local/a/mapped_users` AS `users` WHERE users.id = $id;"}, + } { + annotation := header + if strings.HasPrefix(tc.sql, "UPSERT") { + annotation = strings.Replace(header, ":many", ":exec", 1) + } + a, err := analyzer.Analyze([]model.Source{{Name: "schema.sql", Text: jooqPrefixSchema}}, []model.Source{{Name: "q.sql", Text: annotation + tc.sql}}) + if err != nil { + t.Fatal(err) + } + q := a.Queries[0] + sql, _ := jdbc.SQL(q) + expression, err := jooqDeclaredSQL(q, sql) + if err != nil { + t.Fatal(err) + } + want := strings.TrimPrefix(header, "-- name: Read :many\n") + tc.want + fmt.Fprintf(&program, "if (!java.util.Base64.getEncoder().encodeToString((%s).getBytes(java.nio.charset.StandardCharsets.UTF_8)).equals(%q)) throw new AssertionError(\"case %d\");\n", expression, base64.StdEncoding.EncodeToString([]byte(want)), i) + } + program.WriteString("}}") + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "Main.java"), []byte(program.String()), 0600); err != nil { + t.Fatal(err) + } + for _, args := range [][]string{{"javac", "--release", "17", "Main.java"}, {"java", "-cp", dir, "Main"}} { + cmd := exec.Command(args[0], args[1:]...) + cmd.Dir = dir + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("%s: %v\n%s\n%s", args[0], err, out, program.String()) + } + } +} + +func runJooqPrefixSDK(t *testing.T, files []model.File) { + t.Helper() + maven := os.Getenv("SQLC_YDB_TEST_MAVEN") + if maven == "" { + t.Skip("set SQLC_YDB_TEST_MAVEN to execute prefixed queries against the published dialect") + } + dir := t.TempDir() + classpath := filepath.Join(dir, "classpath") + cmd := exec.Command(maven, "-q", "dependency:build-classpath", "-Dmdep.outputFile="+classpath) + cmd.Dir = filepath.Join("..", "..", "..", "tests", "examples", "java", "jooq") + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("SDK classpath: %v\n%s", err, out) + } + cp, err := os.ReadFile(classpath) + if err != nil { + t.Fatal(err) + } + program := `package prefix; +import java.util.*; +import org.jooq.conf.*; +import org.jooq.tools.jdbc.*; +import org.jooq.types.ULong; +import tech.ydb.jooq.YDB; +import tech.ydb.table.values.PrimitiveValue; +public class Main { + public static void main(String[] args) throws Exception { + for (String target : List.of("mapped_users", "/local/mapped/users")) { + var statements = new ArrayList(); + try (var connection = new MockConnection(ctx -> { + String sql = ctx.sql(); + statements.add(sql); + String pragma = switch (statements.size()) { + case 6 -> "PRAGMA TablePathPrefix('/db/?w')"; + case 7 -> "PRAGMA TablePathPrefix(\"/db/?w\")"; + default -> "` + jooqPrefixPragma + `"; + }; + if (!sql.substring(0, sql.indexOf(';')).equals(pragma) || !sql.contains("` + "`" + `" + target + "` + "`" + `")) throw new AssertionError(sql); + Object[] want = switch (statements.size()) { + case 1, 2, 5, 6, 7 -> new Object[]{"Name"}; + case 3 -> new Object[]{PrimitiveValue.newUint64(-1L), "Name"}; + case 4 -> new Object[]{PrimitiveValue.newUint64(-1L)}; + default -> throw new AssertionError("extra execution " + sql); + }; + if (!Arrays.equals(ctx.bindings(), want)) throw new AssertionError(Arrays.toString(ctx.bindings()) + " type=" + ctx.bindings()[0].getClass() + " SQL=" + sql); + if (statements.size() == 3 && !sql.contains("into ` + "`" + `" + target + "` + "`" + ` (` + "`id`, `name`" + `)")) throw new AssertionError(sql); + if (statements.size() == 3) return new MockResult[]{new MockResult(1)}; + var dsl = YDB.using(); + var result = dsl.newResult(Tables.LOCAL_A_USERS.ID); + result.add(dsl.newRecord(Tables.LOCAL_A_USERS.ID).values(ULong.MAX)); + return new MockResult[]{new MockResult(1, result)}; + })) { + var settings = new Settings().withRenderMapping(new RenderMapping().withSchemata(new MappedSchema().withInput("").withTables( + new MappedTable().withInput("/local/a/users").withOutput(target), new MappedTable().withInput("/local/b/users").withOutput("/local/b/mapped")))); + var queries = new Queries(YDB.using(connection, settings)); + if (!queries.read("Name").get(0).id().equals(ULong.MAX)) throw new AssertionError("read"); + if (!queries.joinUsers("Name").get(0).id().equals(ULong.MAX)) throw new AssertionError("join"); + queries.write(ULong.MAX, "Name"); + if (!queries.remove(ULong.MAX).orElseThrow().id().equals(ULong.MAX)) throw new AssertionError("returning"); + if (!queries.readPath("Name").get(0).id().equals(ULong.MAX)) throw new AssertionError("relative path"); + if (!queries.questionSingle("Name").get(0).id().equals(ULong.MAX)) throw new AssertionError("single-quoted question mark"); + if (!queries.questionDouble("Name").get(0).id().equals(ULong.MAX)) throw new AssertionError("double-quoted question mark"); + if (statements.size() != 7 || !statements.get(0).contains("VIEW ` + "`by_name`" + `") || !statements.get(1).contains("` + "`/local/b/mapped`" + `")) throw new AssertionError(statements); + if (!statements.get(0).contains("-- repeated static prefix stays attached to the statement\nPRAGMA TablePathPrefix('/local/a');") || !statements.get(4).contains("` + "`../a/users`.`id`" + `")) throw new AssertionError(statements); + } + } + } +}` + files = append(files, model.File{Name: "Main.java", Content: []byte(program)}) + compile := []string{"-cp", strings.TrimSpace(string(cp)), "-d", dir} + for _, file := range files { + path := filepath.Join(dir, file.Name) + if err := os.WriteFile(path, file.Content, 0600); err != nil { + t.Fatal(err) + } + compile = append(compile, path) + } + for _, args := range [][]string{append([]string{"javac"}, compile...), {"java", "-cp", dir + string(os.PathListSeparator) + strings.TrimSpace(string(cp)), "prefix.Main"}} { + if out, err := exec.Command(args[0], args[1:]...).CombinedOutput(); err != nil { + t.Fatalf("%s: %v\n%s", args[0], err, out) + } + } +} diff --git a/internal/codegen/java/jooq_test.go b/internal/codegen/java/jooq_test.go index 03999ad8..6fe3cf8f 100644 --- a/internal/codegen/java/jooq_test.go +++ b/internal/codegen/java/jooq_test.go @@ -442,7 +442,7 @@ func TestJooqMappingHelpersRejectInvalidTableNames(t *testing.T) { t.Fatalf("invalid table produced SQL %q, error %v", sql, r.err) } text, _ := jdbc.SQL(q) - if sql, err := jooqDeclaredSQL(q, text); sql != "" || err == nil || err.Error() != "cannot represent \"a``b\" as a Java identifier" { + if sql, err := jooqDeclaredSQL(q, text); sql != "" || err == nil || err.Error() != "cannot represent \"a`b\" as a Java identifier" { t.Fatalf("invalid declared table produced SQL %q, error %v", sql, err) } } diff --git a/internal/codegen/java/table_paths_test.go b/internal/codegen/java/table_paths_test.go new file mode 100644 index 00000000..f49f2e26 --- /dev/null +++ b/internal/codegen/java/table_paths_test.go @@ -0,0 +1,43 @@ +package java + +import ( + "strings" + "testing" + + "github.com/ydb-platform/sqlc-ydb/internal/model" +) + +func TestJavaTablePathsKeepNamespaceInNames(t *testing.T) { + for _, runtime := range []string{"ydb", "jdbc", "jooq"} { + t.Run(runtime, func(t *testing.T) { + a := &model.AnalysisResult{Catalog: model.Catalog{Tables: []model.Table{ + {Name: "/local/tenant_a/users"}, {Name: "/local/tenant_b/users"}, + }}} + files, err := Generate(a, Options{Runtime: runtime}) + if err != nil { + t.Fatal(err) + } + if runtime == "jooq" { + for _, want := range []string{"LocalTenantAUsersTable", "LocalTenantBUsersTable", "LOCAL_TENANT_A_USERS", "LOCAL_TENANT_B_USERS", `"/local/tenant_a/users"`, `"/local/tenant_b/users"`} { + if !strings.Contains(string(files[0].Content), want) { + t.Fatalf("missing %s in %s", want, files[0].Content) + } + } + } else if files[0].Name != "LocalTenantAUsers.java" || files[1].Name != "LocalTenantBUsers.java" { + t.Fatalf("table namespaces lost: %v", files) + } + }) + } +} + +func TestJavaTablePathCollisions(t *testing.T) { + for _, runtime := range []string{"ydb", "jdbc", "jooq"} { + t.Run(runtime, func(t *testing.T) { + a := &model.AnalysisResult{Catalog: model.Catalog{Tables: []model.Table{{Name: "tenant/users"}, {Name: "tenant_users"}}}} + files, err := Generate(a, Options{Runtime: runtime}) + if files != nil || err == nil || !strings.Contains(err.Error(), "name collision") { + t.Fatalf("colliding table paths: files=%v, error=%v", files, err) + } + }) + } +} diff --git a/internal/codegen/kotlin/generator.go b/internal/codegen/kotlin/generator.go index 33932d1f..b4f49042 100644 --- a/internal/codegen/kotlin/generator.go +++ b/internal/codegen/kotlin/generator.go @@ -177,7 +177,7 @@ func Generate(a *model.AnalysisResult, o Options) ([]model.File, error) { return nil } for _, table := range a.Catalog.Tables { - n, err := name(table.Name, true) + n, err := name(strings.ReplaceAll(table.Name, "/", "_"), true) if err != nil { return nil, err } diff --git a/internal/codegen/kotlin/table_paths_test.go b/internal/codegen/kotlin/table_paths_test.go new file mode 100644 index 00000000..02077f5f --- /dev/null +++ b/internal/codegen/kotlin/table_paths_test.go @@ -0,0 +1,27 @@ +package kotlin + +import ( + "strings" + "testing" + + "github.com/ydb-platform/sqlc-ydb/internal/model" +) + +func TestKotlinTablePaths(t *testing.T) { + columns := []model.Column{{Name: "id", Type: model.Type{Kind: "Uint64"}}} + a := &model.AnalysisResult{Catalog: model.Catalog{Tables: []model.Table{{Name: "/local/tenant_a/users", Columns: columns}, {Name: "/local/tenant_b/users", Columns: columns}}}} + files, err := Generate(a, Options{}) + if err != nil { + t.Fatal(err) + } + for i, want := range []string{"data class LocalTenantAUsers", "data class LocalTenantBUsers"} { + if !strings.Contains(string(files[i].Content), want) { + t.Fatalf("missing %q in %s", want, files[i].Content) + } + } + a.Catalog.Tables = []model.Table{{Name: "tenant/users", Columns: columns}, {Name: "tenant_users", Columns: columns}} + files, err = Generate(a, Options{}) + if files != nil || err == nil || !strings.Contains(err.Error(), "name collision") { + t.Fatalf("colliding table paths: files=%v, error=%v", files, err) + } +} diff --git a/internal/endtoend/table_path_prefix_jooq_live_test.go b/internal/endtoend/table_path_prefix_jooq_live_test.go new file mode 100644 index 00000000..a1f2e2eb --- /dev/null +++ b/internal/endtoend/table_path_prefix_jooq_live_test.go @@ -0,0 +1,132 @@ +package endtoend + +import ( + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/ydb-platform/sqlc-ydb/internal/analyzer" + "github.com/ydb-platform/sqlc-ydb/internal/codegen/java" + "github.com/ydb-platform/sqlc-ydb/internal/model" +) + +func tablePathPrefixJooq(t *testing.T, schemas []model.Source, a, b string) { + t.Helper() + maven := os.Getenv("SQLC_YDB_TEST_MAVEN") + if maven == "" { + t.Skip("set SQLC_YDB_TEST_MAVEN for live generated jOOQ namespace checks") + } + queries := strings.NewReplacer("$PREFIX_A", a, "$PREFIX_B", b).Replace(tablePathPrefixJooqQueries) + analysis, err := analyzer.Analyze(schemas, []model.Source{{Name: "jooq.sql", Text: queries}}) + if err != nil { + t.Fatal(err) + } + files, err := java.Generate(analysis, java.Options{Package: "prefixlive", Runtime: "jooq"}) + if err != nil { + t.Fatal(err) + } + dir := t.TempDir() + classpath := filepath.Join(dir, "classpath") + cmd := exec.Command(maven, "-q", "dependency:build-classpath", "-Dmdep.outputFile="+classpath) + cmd.Dir = filepath.Join("..", "..", "tests", "examples", "java", "jooq") + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("jOOQ SDK classpath: %v\n%s", err, out) + } + cp, err := os.ReadFile(classpath) + if err != nil { + t.Fatal(err) + } + program := strings.NewReplacer("$TABLE_A", strconv.Quote(a+"/users"), "$MAPPED_A", strconv.Quote(a+"/mapped_users")).Replace(tablePathPrefixJooqProgram) + files = append(files, model.File{Name: "Main.java", Content: []byte(program)}) + compile := []string{"-cp", strings.TrimSpace(string(cp)), "-d", dir} + for _, file := range files { + filename := filepath.Join(dir, file.Name) + if err := os.WriteFile(filename, file.Content, 0600); err != nil { + t.Fatal(err) + } + compile = append(compile, filename) + } + for _, args := range [][]string{append([]string{"javac"}, compile...), {"java", "-cp", dir + string(os.PathListSeparator) + strings.TrimSpace(string(cp)), "prefixlive.Main"}} { + if out, err := exec.Command(args[0], args[1:]...).CombinedOutput(); err != nil { + t.Fatalf("prefixed jOOQ %s: %v\n%s", args[0], err, out) + } + } +} + +const tablePathPrefixJooqQueries = `-- name: UpsertInferred :exec +PRAGMA TablePathPrefix("$PREFIX_A"); +UPSERT INTO users (id,name) VALUES ($id,$name); + +-- name: ReadInferred :many +PRAGMA TablePathPrefix("$PREFIX_A"); +SELECT u.* FROM users VIEW by_name AS u WHERE u.name = $name ORDER BY u.id; + +-- name: UpsertDeclared :exec +PRAGMA TablePathPrefix("$PREFIX_A"); +DECLARE $id AS Uint64; +DECLARE $name AS Utf8; +UPSERT INTO users (id,name) VALUES ($id,$name); + +-- name: ReadDeclared :one +PRAGMA TablePathPrefix("$PREFIX_A"); +DECLARE $id AS Uint64; +SELECT users.* FROM users WHERE users.id = $id; + +-- name: CopyDeclared :exec +PRAGMA TablePathPrefix("$PREFIX_A"); +DECLARE $id AS Uint64; +UPSERT INTO users SELECT users.id, users.name, users.note FROM users WHERE users.id = $id; + +-- name: RemoveInferred :one +PRAGMA TablePathPrefix("$PREFIX_A"); +DELETE FROM users WHERE id = $id RETURNING id; + +-- name: ReadAbsoluteB :one +PRAGMA TablePathPrefix("$PREFIX_A"); +DECLARE $id AS Uint64; +SELECT u.* FROM ` + "`$PREFIX_B/users`" + ` AS u WHERE u.id = $id; +` + +const tablePathPrefixJooqProgram = `package prefixlive; +import java.sql.*; +import org.jooq.conf.*; +import org.jooq.types.ULong; +import tech.ydb.jooq.YDB; +public class Main { + public static void main(String[] args) throws Exception { + try (var connection = DriverManager.getConnection("jdbc:ydb:" + System.getenv("YDB_CONNECTION_STRING"))) { + int mode = 0; + for (String mapped : new String[]{"mapped_users", $MAPPED_A}) { + var settings = new Settings().withRenderMapping(new RenderMapping().withSchemata(new MappedSchema().withInput("") + .withTables(new MappedTable().withInput($TABLE_A).withOutput(mapped)))); + var queries = new Queries(YDB.using(connection, settings)); + ULong id = mode == 0 ? ULong.MAX : ULong.valueOf(100); + String label = "mapped inferred " + mode; + queries.upsertInferred(id, label); + var inferred = queries.readInferred(label); + if (inferred.size() != 1 || !inferred.get(0).id().equals(id) || inferred.get(0).note() != null) throw new AssertionError(inferred); + var declared = queries.readDeclared(id).orElseThrow(); + if (!declared.name().equals(label)) throw new AssertionError(declared); + ULong declaredId = ULong.valueOf(200 + mode); + String declaredLabel = "mapped declared " + mode; + queries.upsertDeclared(declaredId, declaredLabel); + var declaredViaDSL = queries.readInferred(declaredLabel); + if (declaredViaDSL.size() != 1 || !declaredViaDSL.get(0).id().equals(declaredId)) throw new AssertionError(declaredViaDSL); + queries.copyDeclared(declaredId); + if (!queries.readDeclared(declaredId).orElseThrow().name().equals(declaredLabel)) throw new AssertionError("self-select mapping"); + var deleted = queries.removeInferred(declaredId).orElseThrow(); + if (!deleted.id().equals(declaredId) || !queries.readInferred(declaredLabel).isEmpty()) throw new AssertionError(deleted); + var absolute = queries.readAbsoluteB(ULong.valueOf(1)).orElseThrow(); + if (!absolute.active() || !absolute.name().equals("B")) throw new AssertionError(absolute); + mode++; + } + try (var statement = connection.createStatement(); var rows = statement.executeQuery("SELECT id,name FROM " + '` + "`" + `' + $TABLE_A + '` + "`" + `' + " ORDER BY id")) { + if (!rows.next() || rows.getLong(1) != 1 || !rows.getString(2).equals("A") || rows.next()) throw new AssertionError("RenderMapping wrote to the original table"); + } + } + } +} +` diff --git a/internal/endtoend/table_path_prefix_live_test.go b/internal/endtoend/table_path_prefix_live_test.go new file mode 100644 index 00000000..46c664af --- /dev/null +++ b/internal/endtoend/table_path_prefix_live_test.go @@ -0,0 +1,214 @@ +package endtoend + +import ( + "bytes" + "context" + "fmt" + "os" + "path" + "path/filepath" + "reflect" + "slices" + "strconv" + "strings" + "testing" + "time" + + "github.com/ydb-platform/sqlc-ydb/internal/analyzer" + "github.com/ydb-platform/sqlc-ydb/internal/cli" + "github.com/ydb-platform/sqlc-ydb/internal/config" + "github.com/ydb-platform/sqlc-ydb/internal/database" + "github.com/ydb-platform/sqlc-ydb/internal/model" +) + +// Same-leaf tables have different schemas and rows, so resolving the wrong +// namespace cannot pass either metadata checks or generated runtime checks. +func TestLiveYDBTablePathPrefix(t *testing.T) { + dsn := os.Getenv("YDB_CONNECTION_STRING") + if dsn == "" { + t.Skip("set YDB_CONNECTION_STRING for TablePathPrefix validation") + } + dir := t.TempDir() + settings, err := (config.Database{URI: dsn, Timeout: "30s"}).Resolve(dir) + if err != nil { + t.Fatal(err) + } + root := path.Join(settings.Database, fmt.Sprintf("sqlc_prefixx%d", time.Now().UnixNano())) + a, b := root+"/a", root+"/b" + schemas := []model.Source{ + {Name: "a.sql", Text: "PRAGMA TablePathPrefix(" + strconv.Quote(a) + ");\nCREATE TABLE users (id Uint64 NOT NULL, name Utf8 NOT NULL, note Utf8, PRIMARY KEY(id), INDEX by_name GLOBAL SYNC ON(name));"}, + {Name: "b.sql", Text: "PRAGMA TablePathPrefix(" + strconv.Quote(b) + ");\nCREATE TABLE users (active Bool NOT NULL, id Uint64 NOT NULL, name Utf8 NOT NULL, PRIMARY KEY(id));"}, + } + runDatabasePython(t, dir, tablePathPrefixFixturePython, "create", root, schemas[0].Text, schemas[1].Text) + t.Cleanup(func() { runDatabasePython(t, dir, tablePathPrefixFixturePython, "drop", root) }) + querySQL := strings.NewReplacer("$PREFIX_A", a, "$PREFIX_B", b, "$ROOT", root).Replace(tablePathPrefixQueries) + queries := []model.Source{{Name: "queries.sql", Text: querySQL}} + offline, err := analyzer.Analyze(schemas, queries) + if err != nil { + t.Fatal(err) + } + client, err := database.New(settings) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := client.Close(); err != nil { + t.Error(err) + } + }) + for _, localSchema := range []bool{false, true} { + var local []model.Source + if localSchema { + local = schemas + } + connected, err := analyzer.AnalyzeWithDatabase(context.Background(), local, queries, analyzer.Options{}, client) + if err != nil { + t.Fatal(err) + } + var names []string + for _, table := range connected.Catalog.Tables { + names = append(names, table.Name) + } + slices.Sort(names) + if !slices.Equal(names, []string{a + "/users", b + "/users"}) { + t.Fatalf("discovered table identities = %v", names) + } + if len(connected.Queries) != len(offline.Queries) { + t.Fatalf("connected query count = %d", len(connected.Queries)) + } + for i, got := range connected.Queries { + want := offline.Queries[i] + if got.SQL != want.SQL || !reflect.DeepEqual(got.Parameters, want.Parameters) || !reflect.DeepEqual(got.ResultSets, want.ResultSets) { + t.Fatalf("local schema=%v: query %s lost offline/connected parity", localSchema, got.Name) + } + } + } + for _, change := range []struct { + file int + from, to string + }{{0, "note Utf8", "note Int32"}, {1, "active Bool", "active Utf8"}} { + changed := slices.Clone(schemas) + changed[change.file].Text = strings.Replace(changed[change.file].Text, change.from, change.to, 1) + _, err := analyzer.AnalyzeWithDatabase(context.Background(), changed, queries, analyzer.Options{}, client) + wantTable := []string{a, b}[change.file] + "/users" + if err == nil || !strings.Contains(err.Error(), "database schema drift") || !strings.Contains(err.Error(), wantTable) { + t.Fatalf("drift for %s = %v", wantTable, err) + } + } + + configuration := "version: '2'\nsql:\n" + for _, runtime := range []string{"ydb", "database/sql"} { + configuration += "- engine: ydb\n schema: [a.sql, b.sql]\n queries: queries.sql\n gen:\n go:\n package: records\n out: " + strings.ReplaceAll(runtime, "/", "_") + "\n sql_package: " + runtime + "\n" + } + for name, content := range map[string]string{ + "a.sql": schemas[0].Text, "b.sql": schemas[1].Text, "queries.sql": querySQL, "sqlc.yaml": configuration, + "go.mod": "module generated\n\ngo 1.26.0\n\nrequire github.com/ydb-platform/ydb-go-sdk/v3 v3.151.1\n", + } { + if err := os.WriteFile(filepath.Join(dir, name), []byte(content), 0600); err != nil { + t.Fatal(err) + } + } + var stdout, stderr bytes.Buffer + if code := cli.Run([]string{"generate", "-f", filepath.Join(dir, "sqlc.yaml")}, &stdout, &stderr); code != 0 { + t.Fatalf("generate namespace fixture: %s", stderr.String()) + } + for _, runtime := range []string{"ydb", "database/sql"} { + t.Run(runtime, func(t *testing.T) { + setup, imports := "q := New(driver.Query())", "" + if runtime == "database/sql" { + setup = "db := sql.OpenDB(ydb.MustConnector(driver)); defer db.Close(); q := New(db)" + imports = `"database/sql";` + } + source := `package records +import("context";"testing";"time";` + imports + `ydb "github.com/ydb-platform/ydb-go-sdk/v3") +func TestNamespaces(t *testing.T) { + ctx,cancel:=context.WithTimeout(context.Background(),60*time.Second);defer cancel() + driver,err:=ydb.Open(ctx,` + strconv.Quote(dsn) + `,ydb.WithAnonymousCredentials());if err!=nil {t.Fatal(err)};defer driver.Close(ctx) + ` + setup + ` + first,err:=q.ReadA(ctx,1);if err!=nil || first.Name!="A" || first.Note==nil || *first.Note!="original" {t.Fatalf("namespace A: %v %v",first,err)} + other,err:=q.ReadB(ctx,1);if err!=nil || other.Name!="B" || !other.Active {t.Fatalf("namespace B: %v %v",other,err)} + absolute,err:=q.ReadAbsoluteB(ctx,1);if err!=nil || absolute.Name!="B" || !absolute.Active {t.Fatalf("absolute bypass: %v %v",absolute,err)} + relative,err:=q.ReadRelativeA(ctx,1);if err!=nil || relative.Name!="A" {t.Fatalf("relative source: %v %v",relative,err)} + indexed,err:=q.FindA(ctx,"A");if err!=nil || len(indexed)!=1 || indexed[0].ID!=1 {t.Fatalf("prefixed VIEW: %v %v",indexed,err)} + if err:=q.InsertA(ctx,InsertAParams{ID:2,Name:"inserted"});err!=nil {t.Fatal(err)} + if err:=q.UpdateA(ctx,UpdateAParams{ID:2,Name:"updated"});err!=nil {t.Fatal(err)} + inserted,err:=q.ReadA(ctx,2);if err!=nil || inserted.Name!="updated" || inserted.Note!=nil {t.Fatalf("prefixed writes: %v %v",inserted,err)} + if err:=q.CopyFromB(ctx,"B");err!=nil {t.Fatal(err)} + copied,err:=q.ReadA(ctx,11);if err!=nil || copied.Name!="B" || copied.Note!=nil {t.Fatalf("cross-namespace UPSERT SELECT: %v %v",copied,err)} + for _,id:=range []uint64{2,11} {if err:=q.DeleteA(ctx,id);err!=nil {t.Fatal(err)}} + other,err=q.ReadB(ctx,1);if err!=nil || other.Name!="B" || !other.Active {t.Fatalf("other namespace changed: %v %v",other,err)} +} +` + compileTypedDMLPackage(t, dir, "./"+strings.ReplaceAll(runtime, "/", "_"), source, false) + }) + } + t.Run("jooq", func(t *testing.T) { tablePathPrefixJooq(t, schemas, a, b) }) +} + +const tablePathPrefixQueries = `-- name: ReadA :one +PRAGMA TablePathPrefix("$PREFIX_A"); +DECLARE $id AS Uint64; +SELECT users.* FROM users WHERE users.id = $id; + +-- name: ReadB :one +PRAGMA TablePathPrefix("$PREFIX_B"); +DECLARE $id AS Uint64; +SELECT users.* FROM users WHERE id = $id; + +-- name: ReadAbsoluteB :one +PRAGMA TablePathPrefix("$PREFIX_A"); +DECLARE $id AS Uint64; +SELECT u.* FROM ` + "`$PREFIX_B/users`" + ` AS u WHERE u.id = $id; + +-- name: ReadRelativeA :one +PRAGMA TablePathPrefix("$ROOT"); +DECLARE $id AS Uint64; +SELECT u.* FROM ` + "`a/users`" + ` AS u WHERE u.id = $id; + +-- name: FindA :many +PRAGMA TablePathPrefix("$PREFIX_A"); +DECLARE $name AS Utf8; +SELECT u.* FROM users VIEW by_name AS u WHERE u.name = $name ORDER BY u.id; + +-- name: InsertA :exec +PRAGMA TablePathPrefix("$PREFIX_A"); +DECLARE $id AS Uint64; +DECLARE $name AS Utf8; +INSERT INTO users (id,name) VALUES ($id,$name); + +-- name: UpdateA :exec +PRAGMA TablePathPrefix("$PREFIX_A"); +DECLARE $id AS Uint64; +DECLARE $name AS Utf8; +UPDATE users SET name = $name WHERE users.id = $id; + +-- name: CopyFromB :exec +PRAGMA TablePathPrefix("$PREFIX_A"); +DECLARE $name AS Utf8; +UPSERT INTO users SELECT id+10ul AS id, name FROM ` + "`$PREFIX_B/users`" + ` WHERE name = $name; + +-- name: DeleteA :exec +PRAGMA TablePathPrefix("$PREFIX_A"); +DECLARE $id AS Uint64; +DELETE FROM users WHERE users.id = $id; +` + +const tablePathPrefixFixturePython = databasePythonConnection + `import sys +mode, root = sys.argv[1:3] +with ydb.Driver(config) as driver: + driver.wait(20) + with ydb.QuerySessionPool(driver) as pool: + if mode == "create": + for directory in [root, root+"/a", root+"/b"]: + driver.scheme_client.make_directory(directory) + for schema in sys.argv[3:]: + pool.execute_with_retries(schema) + pool.execute_with_retries(sys.argv[3].replace("CREATE TABLE users", "CREATE TABLE mapped_users")) + pool.execute_with_retries('UPSERT INTO '+chr(96)+root+'/a/users'+chr(96)+' (id,name,note) VALUES (1ul,"A"u,"original"u);') + pool.execute_with_retries('UPSERT INTO '+chr(96)+root+'/b/users'+chr(96)+' (active,id,name) VALUES (true,1ul,"B"u);') + else: + for table in [root+"/a/mapped_users", root+"/a/users", root+"/b/users"]: + pool.execute_with_retries("DROP TABLE IF EXISTS "+chr(96)+table+chr(96)+";") + for directory in [root+"/b", root+"/a", root]: + driver.scheme_client.remove_directory(directory) +` diff --git a/internal/model/model.go b/internal/model/model.go index 445432cd..4a252ba5 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -165,9 +165,11 @@ const ( // QuerySyntax retains the original ANTLR contexts with resolved column bindings. // DSL renderers consume these contexts without reparsing SQL or constructing an AST. type QuerySyntax struct { - Root antlr.ParserRuleContext - Columns map[int]ColumnBinding - Relations []TableBinding + Root antlr.ParserRuleContext + Columns map[int]ColumnBinding + Relations []TableBinding + Tables map[int]string // physical table token index to resolved catalog path + TablePathPrefix string // validated static absolute prefix, or empty when absent } type TableBinding struct{ Table, Alias string } type ColumnBinding struct { diff --git a/tests/examples/csharp/GeneratedProfiles.csproj b/tests/examples/csharp/GeneratedProfiles.csproj index 43158f02..55a65933 100644 --- a/tests/examples/csharp/GeneratedProfiles.csproj +++ b/tests/examples/csharp/GeneratedProfiles.csproj @@ -11,6 +11,7 @@ + diff --git a/tests/examples/go/namespaces/namespaces_test.go b/tests/examples/go/namespaces/namespaces_test.go new file mode 100644 index 00000000..9d515dae --- /dev/null +++ b/tests/examples/go/namespaces/namespaces_test.go @@ -0,0 +1,93 @@ +package namespaces_test + +import ( + "context" + "testing" + "time" + + "example.com/sqlc-ydb-example-tests/internal/testdb" + sq "example.com/sqlc-ydb-examples/namespaces/go/database/sql" + native "example.com/sqlc-ydb-examples/namespaces/go/native" + "github.com/ydb-platform/ydb-go-sdk/v3" +) + +func TestNamespaces(t *testing.T) { + db := testdb.Open(t) + var created []string + t.Cleanup(func() { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + for i := len(created) - 1; i >= 0; i-- { + if err := db.Driver.Scheme().RemoveDirectory(ctx, created[i]); err != nil { + t.Errorf("remove owned directory %s: %v", created[i], err) + } + } + }) + for _, directory := range []string{"/local/sqlc_namespaces", "/local/sqlc_namespaces/a", "/local/sqlc_namespaces/b"} { + // MakeDirectory also succeeds for an existing directory; inspect first + // so cleanup does not remove a directory owned by another caller. + if _, err := db.Driver.Scheme().DescribePath(db.Context, directory); err == nil { + continue + } else if !ydb.IsOperationErrorSchemeError(err) && !ydb.IsOperationErrorNotFoundError(err) { + t.Fatal(err) + } + if err := db.Driver.Scheme().MakeDirectory(db.Context, directory); err != nil { + if !ydb.IsOperationErrorAlreadyExistsError(err) { + t.Fatal(err) + } + } else { + created = append(created, directory) + } + } + // Apply each source separately: TablePathPrefix is request-global on YDB. + // Apply registers table cleanup only after CREATE succeeds, preserving any + // pre-existing tables if this example database is already in use. + db.Apply(t, "../../../../examples/namespaces/schema/a.sql", "DROP TABLE `/local/sqlc_namespaces/a/users`;") + db.Apply(t, "../../../../examples/namespaces/schema/b.sql", "DROP TABLE `/local/sqlc_namespaces/b/users`;") + ctx := db.Context + n, s := native.New(db.Native), sq.New(db.SQL) + if err := n.UpsertPrimaryUser(ctx, native.UpsertPrimaryUserParams{ID: 1, Name: "native primary"}); err != nil { + t.Fatal(err) + } + if err := n.UpsertSecondaryUser(ctx, native.UpsertSecondaryUserParams{ID: 1, Name: "native secondary"}); err != nil { + t.Fatal(err) + } + primary, err := s.GetPrimaryUser(ctx, 1) + if err != nil || primary.Name != "native primary" { + t.Fatalf("SQL primary: %v %v", primary, err) + } + secondary, err := s.GetSecondaryUser(ctx, 1) + if err != nil || secondary.Name != "native secondary" { + t.Fatalf("SQL secondary: %v %v", secondary, err) + } + if err := s.UpsertPrimaryUser(ctx, sq.UpsertPrimaryUserParams{ID: 2, Name: "SQL primary"}); err != nil { + t.Fatal(err) + } + if err := s.UpsertSecondaryUser(ctx, sq.UpsertSecondaryUserParams{ID: 2, Name: "SQL secondary"}); err != nil { + t.Fatal(err) + } + nativePrimary, err := n.GetPrimaryUser(ctx, 2) + if err != nil || nativePrimary.Name != "SQL primary" { + t.Fatalf("native primary: %v %v", nativePrimary, err) + } + nativeSecondary, err := n.GetSecondaryUser(ctx, 2) + if err != nil || nativeSecondary.Name != "SQL secondary" { + t.Fatalf("native secondary: %v %v", nativeSecondary, err) + } + nativeIndex, err := n.FindPrimaryUsersByName(ctx, "SQL primary") + if err != nil || len(nativeIndex) != 1 || nativeIndex[0].ID != 2 { + t.Fatalf("native index: %v %v", nativeIndex, err) + } + sqlIndex, err := s.FindPrimaryUsersByName(ctx, "native primary") + if err != nil || len(sqlIndex) != 1 || sqlIndex[0].ID != 1 { + t.Fatalf("SQL index: %v %v", sqlIndex, err) + } + nativeJoin, err := n.CompareUserNames(ctx) + if err != nil || len(nativeJoin) != 2 || nativeJoin[0].PrimaryName != "native primary" || nativeJoin[0].SecondaryName == nil || *nativeJoin[0].SecondaryName != "native secondary" { + t.Fatalf("native absolute-path join: %v %v", nativeJoin, err) + } + sqlJoin, err := s.CompareUserNames(ctx) + if err != nil || len(sqlJoin) != 2 || sqlJoin[1].PrimaryName != "SQL primary" || sqlJoin[1].SecondaryName == nil || *sqlJoin[1].SecondaryName != "SQL secondary" { + t.Fatalf("SQL absolute-path join: %v %v", sqlJoin, err) + } +} diff --git a/tests/examples/java/batch/pom.xml b/tests/examples/java/batch/pom.xml index 850b5b8d..37567b34 100644 --- a/tests/examples/java/batch/pom.xml +++ b/tests/examples/java/batch/pom.xml @@ -11,6 +11,8 @@ batch-sourcesgenerate-sourcesadd-source ${project.basedir}/../../../../examples/batch/java/native ${project.basedir}/../../../../examples/batch/java/jdbc + ${project.basedir}/../../../../examples/namespaces/java/native/src/main/java + ${project.basedir}/../../../../examples/namespaces/java/jdbc/src/main/java diff --git a/tests/examples/java/jooq/pom.xml b/tests/examples/java/jooq/pom.xml index cec5a9ac..8ece0e0b 100644 --- a/tests/examples/java/jooq/pom.xml +++ b/tests/examples/java/jooq/pom.xml @@ -26,6 +26,7 @@ ${project.basedir}/../../../../examples/booktest/java/jooq ${project.basedir}/../../../../examples/jets/java/jooq ${project.basedir}/../../../../examples/ondeck/java/jooq + ${project.basedir}/../../../../examples/namespaces/java/jooq diff --git a/tests/examples/java/jooq/src/test/java/GeneratedQueriesTest.java b/tests/examples/java/jooq/src/test/java/GeneratedQueriesTest.java index b054b1e2..00aca0e3 100644 --- a/tests/examples/java/jooq/src/test/java/GeneratedQueriesTest.java +++ b/tests/examples/java/jooq/src/test/java/GeneratedQueriesTest.java @@ -24,7 +24,7 @@ void everyExampleRendersAndBindsThroughThePublishedDialect() throws Exception { return new MockResult[] {new MockResult(0, DSL.using(YDB.DIALECT).newResult())}; })) { var dsl = YDB.using(withNamedBinding(connection)); - for (String family : List.of("authors", "batch", "booktest", "jets", "ondeck")) { + for (String family : List.of("authors", "batch", "booktest", "jets", "ondeck", "namespaces")) { Class type = Class.forName(family + ".jooq.Queries"); Object queries = type.getConstructor(tech.ydb.jooq.YdbDSLContext.class).newInstance(dsl); for (Method method : type.getDeclaredMethods()) { @@ -65,7 +65,7 @@ void everyExampleRendersAndBindsThroughThePublishedDialect() throws Exception { } } } - assertEquals(46, statements.size()); + assertEquals(52, statements.size()); } @Test void declaredQueryReadsDialectCarriers() throws Exception { diff --git a/tests/examples/java/jooq/src/test/java/LiveTest.java b/tests/examples/java/jooq/src/test/java/LiveTest.java index 94678e86..a5e8fe75 100644 --- a/tests/examples/java/jooq/src/test/java/LiveTest.java +++ b/tests/examples/java/jooq/src/test/java/LiveTest.java @@ -167,6 +167,40 @@ void aggregatesLimitAndUpdateReturning() throws Exception { } } + @Test + void namespacesKeepIndependentUsers() throws Exception { + String database = java.net.URI.create(System.getenv("YDB_CONNECTION_STRING")).getPath(); + String prefix = database + "/sqlc_jooq_namespaces_" + UUID.randomUUID().toString().replace("-", ""); + String primary = prefix + "_a", secondary = prefix + "_b"; + List created = new ArrayList<>(); + try (Connection connection = DriverManager.getConnection("jdbc:ydb:" + System.getenv("YDB_CONNECTION_STRING"))) { + var settings = new Settings().withRenderMapping(new RenderMapping().withSchemata( + new MappedSchema().withInput("").withTables( + new MappedTable().withInput("/local/sqlc_namespaces/a/users").withOutput(primary), + new MappedTable().withInput("/local/sqlc_namespaces/b/users").withOutput(secondary)))); + var queries = new namespaces.jooq.Queries(YDB.using(connection, settings)); + try { + create(connection, created, primary, "id Uint64 NOT NULL, name Utf8 NOT NULL, PRIMARY KEY(id), INDEX by_name GLOBAL SYNC ON(name)"); + create(connection, created, secondary, "id Uint64 NOT NULL, name Utf8 NOT NULL, PRIMARY KEY(id)"); + queries.upsertPrimaryUser(ULong.MAX, "Primary"); + queries.upsertSecondaryUser(ULong.MAX, "Secondary"); + queries.upsertPrimaryUser(ULong.valueOf(1), "Only primary"); + assertEquals("Primary", queries.getPrimaryUser(ULong.MAX).orElseThrow().name()); + assertEquals("Secondary", queries.getSecondaryUser(ULong.MAX).orElseThrow().name()); + assertEquals(ULong.MAX, queries.findPrimaryUsersByName("Primary").get(0).id()); + var joined = queries.compareUserNames(); + assertEquals(2, joined.size()); + assertNull(joined.get(0).secondaryName()); + assertEquals("Primary", joined.get(1).primaryName()); + assertEquals("Secondary", joined.get(1).secondaryName()); + } finally { + for (String table : created.reversed()) { + try (var statement = connection.createStatement()) { statement.execute("DROP TABLE `" + table + "`"); } + } + } + } + } + private static final class Fixture implements AutoCloseable { final String prefix = "sqlc_jooq_" + UUID.randomUUID().toString().replace("-", "") + "_"; final Connection connection; diff --git a/tests/examples/kotlin/pom.xml b/tests/examples/kotlin/pom.xml index c22608a8..48fd543c 100644 --- a/tests/examples/kotlin/pom.xml +++ b/tests/examples/kotlin/pom.xml @@ -66,7 +66,13 @@ generate-sources add-source - ${project.basedir}/../../../examples/batch/kotlin/jdbc${project.basedir}/../../../examples/batch/kotlin/exposed + + ${project.basedir}/../../../examples/batch/kotlin/jdbc + ${project.basedir}/../../../examples/batch/kotlin/exposed + ${project.basedir}/../../../examples/namespaces/kotlin/nativeapi + ${project.basedir}/../../../examples/namespaces/kotlin/jdbc + ${project.basedir}/../../../examples/namespaces/kotlin/exposed + diff --git a/tests/examples/php/check-generated.php b/tests/examples/php/check-generated.php index 1efd4de4..8c9d7e71 100644 --- a/tests/examples/php/check-generated.php +++ b/tests/examples/php/check-generated.php @@ -27,6 +27,7 @@ function check(bool $condition, string $message): void 'Booktest\\Native', 'Jets\\Native', 'Ondeck\\Native', + 'Namespaces\\Native', ]; foreach ($namespaces as $namespace) { @@ -452,4 +453,4 @@ public function clients(): array check($checked, 'batch: configure callback was not called'); } -echo "Imported and checked generated PHP for all five examples against YDB PHP SDK 1.16.1.\n"; +echo "Imported and checked generated PHP for all six examples against YDB PHP SDK 1.16.1.\n"; diff --git a/tests/examples/php/composer.json b/tests/examples/php/composer.json index f44a5573..e44c674b 100644 --- a/tests/examples/php/composer.json +++ b/tests/examples/php/composer.json @@ -17,7 +17,8 @@ "../../../examples/batch/php/native", "../../../examples/booktest/php/native", "../../../examples/jets/php/native", - "../../../examples/ondeck/php/native" + "../../../examples/ondeck/php/native", + "../../../examples/namespaces/php/native" ] }, "scripts": { diff --git a/tests/examples/rust/src/lib.rs b/tests/examples/rust/src/lib.rs index 06e333af..bb3f4471 100644 --- a/tests/examples/rust/src/lib.rs +++ b/tests/examples/rust/src/lib.rs @@ -12,3 +12,6 @@ pub mod jets; #[path = "../../../../examples/ondeck/rust/native/lib.rs"] pub mod ondeck; + +#[path = "../../../../examples/namespaces/rust/native/lib.rs"] +pub mod namespaces; diff --git a/tests/examples/typescript/check-generated.mjs b/tests/examples/typescript/check-generated.mjs index e14789f5..6cd591aa 100644 --- a/tests/examples/typescript/check-generated.mjs +++ b/tests/examples/typescript/check-generated.mjs @@ -6,6 +6,7 @@ const modules = await Promise.all([ import("./.typescript-build/booktest/typescript/native/queries.js"), import("./.typescript-build/jets/typescript/native/queries.js"), import("./.typescript-build/ondeck/typescript/native/queries.js"), + import("./.typescript-build/namespaces/typescript/native/queries.js"), ]); for (const generated of modules) { @@ -88,4 +89,4 @@ for (const books of [[], [batchInput], [batchInput, { ...batchInput, bookId: 3n } } -console.log("Imported generated TypeScript for all five examples against the pinned YDB SDK."); +console.log("Imported generated TypeScript for all six examples against the pinned YDB SDK.");