From c534c75b9604f469f0573e37846718accbd51d2e Mon Sep 17 00:00:00 2001 From: Jeffrey Aven Date: Thu, 2 Jul 2026 14:58:17 +1000 Subject: [PATCH 1/3] fix: symtab registers snake display column names; robot coverage for AWS e2e casing + walker behaviours Companion to any-sdk feature/aws-e2e-casing-walker-fixes (requires the matching any-sdk bump; go.mod update follows once the tag is cut). Core fix: - statement_analyzer: symbol table now registers the tabulated display column names alongside the raw property keys, so a WHERE clause can reference a snake alias when config.snake_case_aliases is on (was "could not locate symbol vpc_id"). Without aliasing the tabulation re-registers the same names - a no-op. Robot coverage (native_test fixture provider + flask mock), all shaken out by live AWS provider lifecycle testing: - DESCRIBE EXTENDED renders snake aliases (ToDescriptionMap parity). - SELECT * projects VALUES through snake-aliased star expansion (GetAllColumns parity; previously values projected as column names via SQLite's string-literal fallback for unknown quoted identifiers). - A snake WHERE key satisfies a REQUIRED PascalCase wire parameter during method routing and reaches the wire re-keyed (new echo_strict resource). - request.base '{}' fallback body is sent verbatim when no body fields exist (aws-json no-input pattern; new echo_post resource). - schema_driven_xml: member-name properties with xml: name overrides project values (the AWS provider Display shape, new volumes_alias resource); nested wire elements JSON-stringify under string columns; CreateVpc-style singleton unwrap projects (INSERT ... RETURNING); an empty 200 body yields zero rows; a snake column WHERE post-filters via the symtab fix. Co-Authored-By: Claude Fable 5 --- .../primitivegenerator/statement_analyzer.go | 20 +++ .../flask/native_test/app.py | 54 +++++++ .../v0.1.0/services/casing.yaml | 62 +++++++ .../v0.1.0/services/xml_ec2.yaml | 152 ++++++++++++++++++ test/robot/functional/stackql_casing.robot | 61 +++++++ .../robot/functional/stackql_xml_walker.robot | 75 +++++++++ 6 files changed, 424 insertions(+) diff --git a/internal/stackql/primitivegenerator/statement_analyzer.go b/internal/stackql/primitivegenerator/statement_analyzer.go index f144a611..28a93169 100644 --- a/internal/stackql/primitivegenerator/statement_analyzer.go +++ b/internal/stackql/primitivegenerator/statement_analyzer.go @@ -1022,6 +1022,26 @@ func (pb *standardPrimitiveGenerator) expandTable( uid := fmt.Sprintf("%s.%s", tbl.GetUniqueID(), colName) pb.PrimitiveComposer.SetSymbol(uid, colEntry) //nolint:errcheck // TODO: review } + // Register the display column names alongside the raw property keys. + // Under config.snake_case_aliases the tabulation's display names are the + // snake aliases (any-sdk owns that translation), so WHERE / projection + // symbols resolve for either form; without aliasing this re-registers + // the same names and is a no-op. + if tabulation := responseSchema.Tabulate(false, ""); tabulation != nil { + for _, col := range tabulation.GetColumns() { + colSchema := col.GetSchema() + if colSchema == nil { + continue + } + colEntry := symtab.NewSymTabEntry( + pb.PrimitiveComposer.GetDRMConfig().GetRelationalType(colSchema.GetType()), + colSchema, + "", + ) + uid := fmt.Sprintf("%s.%s", tbl.GetUniqueID(), col.GetName()) + pb.PrimitiveComposer.SetSymbol(uid, colEntry) //nolint:errcheck // duplicate registration is benign + } + } return nil } diff --git a/test/python/stackql_test_tooling/flask/native_test/app.py b/test/python/stackql_test_tooling/flask/native_test/app.py index 81c56b0d..e6f0cb9f 100644 --- a/test/python/stackql_test_tooling/flask/native_test/app.py +++ b/test/python/stackql_test_tooling/flask/native_test/app.py @@ -42,6 +42,20 @@ def casing_echo_post(): } ) + @app.get("/casing/echo_strict") + def casing_echo_strict_get(): + # Same echo semantics as /casing/echo; the spec-side difference is that + # VpcId is a REQUIRED wire parameter, so reaching this endpoint at all + # proves the router satisfied the requirement (via the snake alias when + # the SQL used vpc_id). + return jsonify( + { + "VpcId": request.args.get("VpcId"), + "SubnetId": request.args.get("SubnetId"), + "echoed_query": request.query_string.decode("utf-8"), + } + ) + # ---- OData push-down target -------------------------------------------- @app.get("/odata/people") @@ -127,6 +141,46 @@ def xml_ec2_volumes(): ) return Response(body, mimetype="text/xml") + @app.get("/xml/ec2/volumes_alias") + def xml_ec2_volumes_alias(): + # Wire elements use EC2 locationName casing (volumeId, attachmentSet); + # the spec's schema keys are the botocore member names (VolumeId, + # Attachments) with xml: name overrides - the AWS provider shape. The + # nested content exercises JSON stringification of a + # complex value under a string-typed column. + body = ( + "" + "req-ec2-alias-1" + "" + "vol-a18available" + "i-1" + "/dev/sda1" + "vol-a216in-use" + "" + "" + "" + ) + return Response(body, mimetype="text/xml") + + @app.get("/xml/ec2/vpc") + def xml_ec2_vpc(): + # CreateVpc-style singleton: the row lives under a named wrapper member + # one level below the response root (walker singleton-unwrap regime). + body = ( + "" + "req-ec2-vpc-1" + "vpc-fixture-1" + "10.99.0.0/16pending" + "" + ) + return Response(body, mimetype="text/xml") + + @app.get("/xml/ec2/volumes_empty_body") + def xml_ec2_volumes_empty_body(): + # S3 CreateBucket-style: 200 with an empty body. The walker must yield + # zero rows rather than an mxj EOF error. + return Response("", mimetype="text/xml") + @app.get("/xml/query/stacks") def xml_query_stacks(): # `id`/`region` are single-word (value projects); `stackName` is multi-word diff --git a/test/registry/src/stackql_native_test/v0.1.0/services/casing.yaml b/test/registry/src/stackql_native_test/v0.1.0/services/casing.yaml index 60a0e55e..ff0e0412 100644 --- a/test/registry/src/stackql_native_test/v0.1.0/services/casing.yaml +++ b/test/registry/src/stackql_native_test/v0.1.0/services/casing.yaml @@ -79,6 +79,48 @@ components: - $ref: '#/components/x-stackQL-resources/echo/methods/insert' delete: [] update: [] + echo_strict: + id: stackql_native_test.casing.echo_strict + name: echo_strict + title: Echo with a REQUIRED PascalCase wire parameter (routing reverse-casing) + methods: + select: + operation: + $ref: '#/paths/~1casing~1echo_strict/get' + request: + nativeCasing: pascal + response: + mediaType: application/json + openAPIDocKey: '200' + objectKey: $ + sqlVerbs: + select: + - $ref: '#/components/x-stackQL-resources/echo_strict/methods/select' + insert: [] + delete: [] + update: [] + echo_post: + id: stackql_native_test.casing.echo_post + name: echo_post + title: POST echo exercising the request.base fallback body (aws-json '{}' pattern) + methods: + select: + operation: + $ref: '#/paths/~1casing~1echo/post' + request: + mediaType: application/json + base: '{}' + nativeCasing: pascal + response: + mediaType: application/json + openAPIDocKey: '200' + objectKey: $ + sqlVerbs: + select: + - $ref: '#/components/x-stackQL-resources/echo_post/methods/select' + insert: [] + delete: [] + update: [] paths: /casing/echo: get: @@ -113,3 +155,23 @@ paths: servers: - url: http://localhost:1070 summary: Echo received request body + /casing/echo_strict: + get: + operationId: casing/echo_strict/get + parameters: + - in: query + name: VpcId + required: true + schema: + type: string + - $ref: '#/components/parameters/SubnetId' + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/echo_response' + servers: + - url: http://localhost:1070 + summary: Echo received query parameters, VpcId required diff --git a/test/registry/src/stackql_native_test/v0.1.0/services/xml_ec2.yaml b/test/registry/src/stackql_native_test/v0.1.0/services/xml_ec2.yaml index 7db43034..caf91b4b 100644 --- a/test/registry/src/stackql_native_test/v0.1.0/services/xml_ec2.yaml +++ b/test/registry/src/stackql_native_test/v0.1.0/services/xml_ec2.yaml @@ -28,6 +28,50 @@ components: type: array items: $ref: '#/components/schemas/volume' + volume_alias: + type: object + properties: + VolumeId: + type: string + xml: + name: volumeId + Size: + type: integer + xml: + name: size + Attachments: + type: string + xml: + name: attachmentSet + state: + type: string + volumes_alias_response: + type: object + properties: + volumeSet: + type: array + items: + $ref: '#/components/schemas/volume_alias' + vpc_row: + type: object + properties: + VpcId: + type: string + xml: + name: vpcId + CidrBlock: + type: string + xml: + name: cidrBlock + state: + type: string + vpc_response: + type: object + properties: + vpcRows: + type: array + items: + $ref: '#/components/schemas/vpc_row' x-stackQL-resources: volumes: id: stackql_native_test.xml_ec2.volumes @@ -52,6 +96,75 @@ components: delete: [] insert: [] update: [] + volumes_alias: + id: stackql_native_test.xml_ec2.volumes_alias + name: volumes_alias + title: EC2 volumes whose schema uses member-name properties with xml name overrides (the AWS provider shape) + methods: + select: + operation: + $ref: '#/paths/~1xml~1ec2~1volumes_alias/get' + response: + mediaType: application/json + openAPIDocKey: '200' + overrideMediaType: application/json + objectKey: volumeSet + schema_override: + $ref: '#/components/schemas/volumes_alias_response' + transform: + type: schema_driven_xml_v0.1.0 + sqlVerbs: + select: + - $ref: '#/components/x-stackQL-resources/volumes_alias/methods/select' + delete: [] + insert: [] + update: [] + vpc_singleton: + id: stackql_native_test.xml_ec2.vpc_singleton + name: vpc_singleton + title: Singleton-unwrap regime (CreateVpc-style {vpc:{...}} wrapper) + methods: + select: + operation: + $ref: '#/paths/~1xml~1ec2~1vpc/get' + response: + mediaType: application/json + openAPIDocKey: '200' + overrideMediaType: application/json + objectKey: vpcRows + schema_override: + $ref: '#/components/schemas/vpc_response' + transform: + type: schema_driven_xml_v0.1.0 + sqlVerbs: + select: + - $ref: '#/components/x-stackQL-resources/vpc_singleton/methods/select' + delete: [] + insert: [] + update: [] + volumes_empty_body: + id: stackql_native_test.xml_ec2.volumes_empty_body + name: volumes_empty_body + title: Empty 200 response body (S3 CreateBucket-style) yields zero rows + methods: + select: + operation: + $ref: '#/paths/~1xml~1ec2~1volumes_empty_body/get' + response: + mediaType: application/json + openAPIDocKey: '200' + overrideMediaType: application/json + objectKey: volumeSet + schema_override: + $ref: '#/components/schemas/volumes_response' + transform: + type: schema_driven_xml_v0.1.0 + sqlVerbs: + select: + - $ref: '#/components/x-stackQL-resources/volumes_empty_body/methods/select' + delete: [] + insert: [] + update: [] paths: /xml/ec2/volumes: get: @@ -66,3 +179,42 @@ paths: servers: - url: http://localhost:1070 summary: List EC2 volumes as XML + /xml/ec2/volumes_alias: + get: + operationId: xml/ec2/volumes_alias/get + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/volumes_alias_response' + servers: + - url: http://localhost:1070 + summary: List EC2 volumes as XML with locationName-style wire elements + /xml/ec2/vpc: + get: + operationId: xml/ec2/vpc/get + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/vpc_response' + servers: + - url: http://localhost:1070 + summary: Singleton vpc wrapped one level below the response root + /xml/ec2/volumes_empty_body: + get: + operationId: xml/ec2/volumes_empty_body/get + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/volumes_response' + servers: + - url: http://localhost:1070 + summary: Returns 200 with an empty body diff --git a/test/robot/functional/stackql_casing.robot b/test/robot/functional/stackql_casing.robot index fa306c06..e5789df5 100644 --- a/test/robot/functional/stackql_casing.robot +++ b/test/robot/functional/stackql_casing.robot @@ -90,3 +90,64 @@ Multiple Pascal Case Wire Parameters Transmitted ... ${SQL_BACKEND_CFG_STR_CANONICAL} ... select echoed_query from stackql_native_test.casing.echo where VpcId \= 'abc123' and SubnetId \= 'sub-9'; ... SubnetId\=sub-9 + +Describe Extended Shows Snake Case Aliases + [Documentation] DESCRIBE surfaces the same snake aliases as SELECT: wire VpcId + ... renders as vpc_id (any-sdk ToDescriptionMap parity fix). + Should StackQL Exec Inline Contain + ... ${STACKQL_EXE} + ... ${OKTA_SECRET_STR} + ... ${GITHUB_SECRET_STR} + ... ${K8S_SECRET_STR} + ... ${REGISTRY_NO_VERIFY_CFG_STR} + ... ${AUTH_CFG_STR} + ... ${SQL_BACKEND_CFG_STR_CANONICAL} + ... describe extended stackql_native_test.casing.echo; + ... vpc_id + +Select Star Projects Snake Aliased Values + [Documentation] SELECT * expands to snake-aliased columns (any-sdk GetAllColumns + ... parity fix). Before the fix the wire-cased identifiers resolved as string + ... literals on a case-sensitive backend and every value projected as its own + ... column name; the assertion checks the echoed VALUE, not the header. + Should StackQL Exec Inline Contain + ... ${STACKQL_EXE} + ... ${OKTA_SECRET_STR} + ... ${GITHUB_SECRET_STR} + ... ${K8S_SECRET_STR} + ... ${REGISTRY_NO_VERIFY_CFG_STR} + ... ${AUTH_CFG_STR} + ... ${SQL_BACKEND_CFG_STR_CANONICAL} + ... select * from stackql_native_test.casing.echo where VpcId \= 'star-val-1'; + ... star-val-1 + +Snake Case Where Key Satisfies Required Wire Parameter + [Documentation] Method routing accepts a snake key for a REQUIRED wire param + ... (any-sdk parameterMatch reverse-casing fix): echo_strict requires VpcId and + ... the SQL supplies vpc_id; the echoed wire query proves both routing and + ... request construction re-keyed it. + Should StackQL Exec Inline Contain + ... ${STACKQL_EXE} + ... ${OKTA_SECRET_STR} + ... ${GITHUB_SECRET_STR} + ... ${K8S_SECRET_STR} + ... ${REGISTRY_NO_VERIFY_CFG_STR} + ... ${AUTH_CFG_STR} + ... ${SQL_BACKEND_CFG_STR_CANONICAL} + ... select echoed_query from stackql_native_test.casing.echo_strict where vpc_id \= 'req-9'; + ... VpcId\=req-9 + +Base Fallback Body Sent When No Body Params + [Documentation] A method with request.base '{}' and no SQL-supplied body fields + ... sends the base bytes verbatim (the aws-json no-input pattern); the mock + ... echoes the received body. + Should StackQL Exec Inline Contain + ... ${STACKQL_EXE} + ... ${OKTA_SECRET_STR} + ... ${GITHUB_SECRET_STR} + ... ${K8S_SECRET_STR} + ... ${REGISTRY_NO_VERIFY_CFG_STR} + ... ${AUTH_CFG_STR} + ... ${SQL_BACKEND_CFG_STR_CANONICAL} + ... select echoed_body, ok from stackql_native_test.casing.echo_post; + ... {} diff --git a/test/robot/functional/stackql_xml_walker.robot b/test/robot/functional/stackql_xml_walker.robot index b670d148..a36e8809 100644 --- a/test/robot/functional/stackql_xml_walker.robot +++ b/test/robot/functional/stackql_xml_walker.robot @@ -101,3 +101,78 @@ Schema Driven Xml Rest Xml Singleton Yields One Row ... ${SQL_BACKEND_CFG_STR_CANONICAL} ... select id, name from stackql_native_test.xml_restxml.hostedzone; ... example.com + +Xml Name Override Projects Row Values + [Documentation] Schema properties carry botocore member names (VolumeId) with + ... xml: name overrides for the wire elements (volumeId) - the AWS provider + ... shape. The walker extracts by the override and the value projects under + ... the snake alias of the member name. + Should StackQL Exec Inline Contain + ... ${STACKQL_EXE} + ... ${OKTA_SECRET_STR} + ... ${GITHUB_SECRET_STR} + ... ${K8S_SECRET_STR} + ... ${REGISTRY_NO_VERIFY_CFG_STR} + ... ${AUTH_CFG_STR} + ... ${SQL_BACKEND_CFG_STR_CANONICAL} + ... select volume_id, size from stackql_native_test.xml_ec2.volumes_alias order by volume_id; + ... vol-a1 + +Xml Name Override Complex Value Is Json + [Documentation] A nested wire element () landing under a + ... string-typed column serialises as JSON (never Go map notation), so users + ... can JSON_EXTRACT it. + Should StackQL Exec Inline Contain + ... ${STACKQL_EXE} + ... ${OKTA_SECRET_STR} + ... ${GITHUB_SECRET_STR} + ... ${K8S_SECRET_STR} + ... ${REGISTRY_NO_VERIFY_CFG_STR} + ... ${AUTH_CFG_STR} + ... ${SQL_BACKEND_CFG_STR_CANONICAL} + ... select attachments from stackql_native_test.xml_ec2.volumes_alias where volume_id \= 'vol-a1'; + ... "instanceId" + +Xml Where On Snake Column Post Filters + [Documentation] A WHERE key that is a response column (not a request parameter) + ... post-filters the materialised rows; the snake alias must resolve in the + ... symbol table (stackql symtab display-name registration fix). + Should StackQL Exec Inline Contain + ... ${STACKQL_EXE} + ... ${OKTA_SECRET_STR} + ... ${GITHUB_SECRET_STR} + ... ${K8S_SECRET_STR} + ... ${REGISTRY_NO_VERIFY_CFG_STR} + ... ${AUTH_CFG_STR} + ... ${SQL_BACKEND_CFG_STR_CANONICAL} + ... select state from stackql_native_test.xml_ec2.volumes where cidr_block \= '10.0.1.0/24'; + ... in-use + +Xml Singleton Unwrap Projects Row + [Documentation] CreateVpc-style response nests the row under a named wrapper + ... ({vpc: {...}}); the walker unwraps one level when the payload root does not + ... carry the row fields (this is what makes INSERT ... RETURNING project). + Should StackQL Exec Inline Contain + ... ${STACKQL_EXE} + ... ${OKTA_SECRET_STR} + ... ${GITHUB_SECRET_STR} + ... ${K8S_SECRET_STR} + ... ${REGISTRY_NO_VERIFY_CFG_STR} + ... ${AUTH_CFG_STR} + ... ${SQL_BACKEND_CFG_STR_CANONICAL} + ... select vpc_id, cidr_block from stackql_native_test.xml_ec2.vpc_singleton; + ... vpc-fixture-1 + +Xml Empty Response Body Yields Zero Rows + [Documentation] A 200 with an empty body (S3 CreateBucket-style) yields zero + ... rows, not an mxj EOF transform error. + Should StackQL Exec Inline Contain + ... ${STACKQL_EXE} + ... ${OKTA_SECRET_STR} + ... ${GITHUB_SECRET_STR} + ... ${K8S_SECRET_STR} + ... ${REGISTRY_NO_VERIFY_CFG_STR} + ... ${AUTH_CFG_STR} + ... ${SQL_BACKEND_CFG_STR_CANONICAL} + ... select count(*) as num from stackql_native_test.xml_ec2.volumes_empty_body; + ... 0 From 346bc71c119c694181cc8ab87e506f9a7f7e1ec1 Mon Sep 17 00:00:00 2001 From: Jeffrey Aven Date: Fri, 3 Jul 2026 10:12:53 +1000 Subject: [PATCH 2/3] build: bump any-sdk to v0.5.3-alpha11 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 00750960..eca9f76d 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/spf13/cobra v1.4.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.10.1 - github.com/stackql/any-sdk v0.5.3-alpha10 + github.com/stackql/any-sdk v0.5.3-alpha11 github.com/stackql/go-suffix-map v0.0.1-alpha01 github.com/stackql/psql-wire v0.1.2-beta01 github.com/stackql/stackql-parser v0.0.16-alpha02 diff --git a/go.sum b/go.sum index 55a84b5f..6f5b9346 100644 --- a/go.sum +++ b/go.sum @@ -467,8 +467,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk= github.com/spf13/viper v1.10.1/go.mod h1:IGlFPqhNAPKRxohIzWpI5QEy4kuI7tcl5WvR+8qy1rU= -github.com/stackql/any-sdk v0.5.3-alpha10 h1:FlumRS0hICuUqztUsvRCdHljSLA+aTqrU0Y4Z7DF8WQ= -github.com/stackql/any-sdk v0.5.3-alpha10/go.mod h1:yGAr3eEtY6UpFMq8fF5BTvZhdqYGpyS90LP6KY5AKZs= +github.com/stackql/any-sdk v0.5.3-alpha11 h1:peFWQndLyduUOEUXTTnrVWcdiw7wT4D+IulTG8MCL4A= +github.com/stackql/any-sdk v0.5.3-alpha11/go.mod h1:yGAr3eEtY6UpFMq8fF5BTvZhdqYGpyS90LP6KY5AKZs= github.com/stackql/go-suffix-map v0.0.1-alpha01 h1:TDUDS8bySu41Oo9p0eniUeCm43mnRM6zFEd6j6VUaz8= github.com/stackql/go-suffix-map v0.0.1-alpha01/go.mod h1:QAi+SKukOyf4dBtWy8UMy+hsXXV+yyEE4vmBkji2V7g= github.com/stackql/psql-wire v0.1.2-beta01 h1:+0xeLLam3nbI1iv84pWJYGYw5VO5S/+eICRbdu6+dj8= From a2ef8bc519bd382edc5125e2159abedfe152e1d2 Mon Sep 17 00:00:00 2001 From: Jeffrey Aven Date: Fri, 3 Jul 2026 11:06:26 +1000 Subject: [PATCH 3/3] fix(test-tooling): resolve sqlite3 CLI defensively for export tests The Windows CI runner installs sqlite with `choco install --force sqlite` and points SQLITE_EXE at the historical layout (C:\ProgramData\chocolatey\lib\SQLite\tools\sqlite3.exe). Newer versions of the chocolatey package nest the binary one level deeper, breaking the hardcoded path with FileNotFoundError in the four sqlite export tests (unrelated to this branch's changes; would fail on main identically). Resolution order: configured value when it exists (file or PATH-resolvable command name), then `sqlite3` on PATH, then a recursive search of the chocolatey SQLite lib tree (newest hit wins), falling back to the configured value so a genuine miss still surfaces the original error. Verified: resolver unit cases (nested choco layout, existing path kept, miss falls through) on Windows; Stackql Export robot suite 8/8 locally. Co-Authored-By: Claude Fable 5 --- .../stackql_test_tooling/StackQLInterfaces.py | 3 +- .../stackql_test_tooling/sqlite_exe_locate.py | 40 +++++++++++++++++++ .../stackql_test_tooling/stackql_context.py | 3 +- 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 test/python/stackql_test_tooling/sqlite_exe_locate.py diff --git a/test/python/stackql_test_tooling/StackQLInterfaces.py b/test/python/stackql_test_tooling/StackQLInterfaces.py index cda73809..5aa0d9db 100644 --- a/test/python/stackql_test_tooling/StackQLInterfaces.py +++ b/test/python/stackql_test_tooling/StackQLInterfaces.py @@ -17,6 +17,7 @@ from .psycopg_client import PsycoPGClient from .psycopg2_client import PsycoPG2Client from .sqlalchemy_client import SQLAlchemyClient +from .sqlite_exe_locate import resolve_sqlite_exe SQL_BACKEND_CANONICAL_SQLITE_EMBEDDED :str = 'sqlite_embedded' SQL_BACKEND_POSTGRES_TCP :str = 'postgres_tcp' @@ -25,7 +26,7 @@ _TEST_APP_CACHE_ROOT = os.path.join("test", ".stackql") PSQL_EXE :str = os.environ.get('PSQL_EXE', 'psql') -SQLITE_EXE :str = os.environ.get('SQLITE_EXE', 'sqlite3') +SQLITE_EXE :str = resolve_sqlite_exe(os.environ.get('SQLITE_EXE', 'sqlite3')) @library(scope='SUITE', version='0.1.0', doc_format='reST') diff --git a/test/python/stackql_test_tooling/sqlite_exe_locate.py b/test/python/stackql_test_tooling/sqlite_exe_locate.py new file mode 100644 index 00000000..0fef0d5c --- /dev/null +++ b/test/python/stackql_test_tooling/sqlite_exe_locate.py @@ -0,0 +1,40 @@ +"""Defensive resolution of the sqlite3 CLI executable for export tests. + +The Windows CI runner installs sqlite with `choco install --force sqlite` and +points SQLITE_EXE at the historical layout +(C:\\ProgramData\\chocolatey\\lib\\SQLite\\tools\\sqlite3.exe). Newer versions +of the chocolatey package nest the binary one level deeper (a +sqlite-tools-win-* folder), which broke the hardcoded path with +FileNotFoundError. Resolve in order: + +1. The configured value, when it is an existing file or a bare command name + resolvable on PATH. +2. `sqlite3` on PATH. +3. A recursive search of the chocolatey SQLite lib tree (newest hit wins). + +Falls back to the configured value so a genuine miss still surfaces the +original, informative error at call time. +""" + +import glob +import os +import shutil + +_CHOCO_SQLITE_TOOLS_GLOB = r"C:\ProgramData\chocolatey\lib\SQLite\tools\**\sqlite3.exe" + + +def resolve_sqlite_exe(configured: str) -> str: + if not configured: + configured = "sqlite3" + if os.path.isfile(configured): + return configured + if os.path.sep not in configured and shutil.which(configured): + return configured + path_hit = shutil.which("sqlite3") + if path_hit: + return path_hit + if os.name == "nt": + hits = sorted(glob.glob(_CHOCO_SQLITE_TOOLS_GLOB, recursive=True)) + if hits: + return hits[-1] + return configured diff --git a/test/python/stackql_test_tooling/stackql_context.py b/test/python/stackql_test_tooling/stackql_context.py index 925329fe..4ec354a2 100644 --- a/test/python/stackql_test_tooling/stackql_context.py +++ b/test/python/stackql_test_tooling/stackql_context.py @@ -10,6 +10,7 @@ ## cannot use relative imports due to robot usage pattern from typed_python_responses import SELECT_AWS_CLOUD_CONTROL_EVENTS_MINIMAL_EXPECTED from registry_cfg import RegistryCfg +from sqlite_exe_locate import resolve_sqlite_exe _exe_name = 'stackql' _mcp_client_exe_name = 'stackql_mcp_client' @@ -597,7 +598,7 @@ def get_object_count_dict(count :int) -> dict: PG_SRV_PORT_DOCKER_UNENCRYPTED = 5577 PSQL_EXE :str = os.environ.get('PSQL_EXE', 'psql') - SQLITE_EXE :str = os.environ.get('SQLITE_EXE', 'sqlite3') + SQLITE_EXE :str = resolve_sqlite_exe(os.environ.get('SQLITE_EXE', 'sqlite3')) PSQL_CLIENT_HOST :str = "127.0.0.1"