Skip to content

Commit dfdaf79

Browse files
authored
Merge pull request #1999 from wheels-dev/peter/cockroachdb-adapter-phase2
fix: CockroachDB adapter phase 2 — fix SQL generation and remove soft-fail
2 parents 6676f66 + f8c6acd commit dfdaf79

26 files changed

Lines changed: 534 additions & 22 deletions

.github/workflows/tests.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -386,8 +386,7 @@ jobs:
386386
fi
387387
388388
# Databases whose failures are logged but don't block CI.
389-
# CockroachDB populate.cfm fails to create tables (tracked for fix).
390-
SOFT_FAIL_DBS="cockroachdb"
389+
SOFT_FAIL_DBS=""
391390
392391
# Track per-database result
393392
if [ "$HTTP_CODE" = "200" ]; then
@@ -517,7 +516,7 @@ jobs:
517516
echo "| Database | Result |" >> $GITHUB_STEP_SUMMARY
518517
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
519518
520-
SOFT_FAIL_DBS="cockroachdb"
519+
SOFT_FAIL_DBS=""
521520
IFS=',' read -ra DBS <<< "${{ steps.db-list.outputs.databases }}"
522521
for db in "${DBS[@]}"; do
523522
RESULT_FILE="/tmp/test-results/${{ matrix.cfengine }}-${db}-result.txt"
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# CockroachDB Adapter Improvements (Phase 2)
2+
3+
**Issues**: #1972, #1974
4+
**Goal**: Fix CockroachDB test failures and remove from SOFT_FAIL_DBS in CI
5+
6+
## Context
7+
8+
CockroachDB is marked as soft-fail in CI (`SOFT_FAIL_DBS="cockroachdb"` in `.github/workflows/tests.yml`). Tests run but failures don't block the build. Current results on Lucee 6: **2217 pass, 31 fail, 38 error** out of 507 suites.
9+
10+
Phase 1 (already merged) established the CockroachDB adapter: `CockroachDBModel.cfc` (type mapping, RETURNING clause identity select) and `CockroachDBMigrator.cfc` (native sqlTypes, unique_rowid() PK generation), plus unit and integration tests.
11+
12+
Phase 2 fixes the remaining test failures so CockroachDB becomes a first-class CI target.
13+
14+
## Root Causes
15+
16+
### 1. Missing CockroachDB in adapter name checks (9 SQL errors + ~20 cascading failures)
17+
18+
The SQL generation layer in `model/update.cfc` and `model/sql.cfc` uses hardcoded adapter name lists like `ListFind('PostgreSQL,H2,Oracle,SQLite', adapterName())`. CockroachDB returns `"CockroachDB"` for `adapterName()` and matches **none** of these branches.
19+
20+
The critical failure: `update.cfc:108` — the `UPDATE table SET` prefix is never generated for CockroachDB, producing SQL like `"authorid" = ` (no table, no SET keyword). This causes 9 direct SQL syntax errors and cascading failures in tests that depend on UPDATE operations.
21+
22+
**Locations to fix** (add `CockroachDB` alongside `PostgreSQL`):
23+
24+
| File | Line | Current List |
25+
|------|------|-------------|
26+
| `vendor/wheels/model/update.cfc` | 108 | `PostgreSQL,H2,Oracle,SQLite` |
27+
| `vendor/wheels/model/sql.cfc` | 653 | `PostgreSQL,H2,MicrosoftSQLServer,Oracle,SQLite` |
28+
| `vendor/wheels/model/sql.cfc` | 664 | `PostgreSQL` |
29+
| `vendor/wheels/model/sql.cfc` | 701 | `PostgreSQL,H2` |
30+
31+
### 2. unique_rowid() generates large INT8 IDs (3-5 test failures)
32+
33+
CockroachDB's SERIAL uses `unique_rowid()` producing large non-sequential INT8 values (e.g., `1161774559250219009`). Core tests that hardcode `Expected [1]` or `Expected [5]` fail.
34+
35+
Affected tests:
36+
- `readSpec > findfirst > works` — expects id=1
37+
- `readSpec > findLastOne > works` — expects id=5
38+
- `crudSpec > order > is working with maxrows and calculated property` — ID in expected string
39+
- `sqlSpec > works with numeric operators` — expects cf_sql_integer (gets cf_sql_bigint)
40+
- `migrationSpec > is changing column` — expects limit 50 (gets 2147483647)
41+
42+
### 3. Transaction behavior differences (12 failures in transactionsSpec)
43+
44+
CockroachDB uses SERIALIZABLE isolation by default and has different SAVEPOINT semantics. Several transaction tests assume READ COMMITTED behavior or specific savepoint rollback mechanics.
45+
46+
## Design
47+
48+
### Part 1: Fix adapter name checks
49+
50+
Mechanical change: add `CockroachDB` to each `ListFind()` call that currently includes `PostgreSQL` in `update.cfc` and `sql.cfc`. CockroachDB uses the PostgreSQL wire protocol and should receive identical SQL generation treatment.
51+
52+
### Part 2: Adapter-specific test suites
53+
54+
**Strategy**: Guard core tests that have CockroachDB-incompatible assumptions, then provide equivalent coverage through CockroachDB-specific test specs.
55+
56+
**Guard pattern** (in core spec files):
57+
```cfm
58+
it("expects sequential IDs", () => {
59+
if ($isCockroachDB()) return; // covered by CockroachDBCrudSpec
60+
// ... test with hardcoded IDs
61+
});
62+
```
63+
64+
The `$isCockroachDB()` helper checks the current adapter name. This is the pattern already used in `CockroachDBIntegrationSpec.cfc`.
65+
66+
**New test files** (in `vendor/wheels/tests/specs/database/`):
67+
68+
1. **CockroachDBCrudSpec.cfc** — CRUD lifecycle with non-sequential ID assertions:
69+
- Create returns numeric key > 0
70+
- Sequential creates produce unique increasing keys
71+
- findFirst/findLast work without assuming specific ID values
72+
- findAll with maxrows works with large IDs
73+
74+
2. **CockroachDBTransactionSpec.cfc** — Transaction behavior under SERIALIZABLE isolation:
75+
- Basic transaction commit/rollback
76+
- invokeWithTransaction callback behavior
77+
- Nested transaction semantics
78+
- deleteAll/updateAll within transactions
79+
80+
3. **CockroachDBTypeSpec.cfc** — Type introspection for CockroachDB-specific types:
81+
- SERIAL columns report as cf_sql_bigint (INT8)
82+
- STRING type maps correctly
83+
- Column metadata returns expected limits
84+
85+
### Part 3: Remove from SOFT_FAIL_DBS
86+
87+
After all fixes verified locally on Lucee 6 + Adobe 2025:
88+
- Remove `cockroachdb` from `SOFT_FAIL_DBS` on lines 390 and 520 of `.github/workflows/tests.yml`
89+
90+
## Files to modify
91+
92+
| File | Change |
93+
|------|--------|
94+
| `vendor/wheels/model/update.cfc` | Add CockroachDB to adapter name list (line 108) |
95+
| `vendor/wheels/model/sql.cfc` | Add CockroachDB to adapter name lists (lines 653, 664, 701) |
96+
| `vendor/wheels/tests/specs/model/readSpec.cfc` | Guard CockroachDB-incompatible ID tests |
97+
| `vendor/wheels/tests/specs/model/crudSpec.cfc` | Guard CockroachDB-incompatible ID/order tests |
98+
| `vendor/wheels/tests/specs/model/sqlSpec.cfc` | Guard CockroachDB type expectation test |
99+
| `vendor/wheels/tests/specs/migrator/migrationSpec.cfc` | Guard CockroachDB column limit test |
100+
| `vendor/wheels/tests/specs/model/transactionsSpec.cfc` | Guard CockroachDB-incompatible transaction tests |
101+
| `vendor/wheels/tests/specs/database/CockroachDBCrudSpec.cfc` | **New** — CRUD with non-sequential IDs |
102+
| `vendor/wheels/tests/specs/database/CockroachDBTransactionSpec.cfc` | **New** — SERIALIZABLE transaction tests |
103+
| `vendor/wheels/tests/specs/database/CockroachDBTypeSpec.cfc` | **New** — Type introspection tests |
104+
| `.github/workflows/tests.yml` | Remove cockroachdb from SOFT_FAIL_DBS |
105+
106+
## Verification
107+
108+
1. Run Lucee 6 + CockroachDB locally: `curl "http://localhost:60006/wheels/core/tests?db=cockroachdb&format=json"`
109+
2. Run Adobe 2025 + CockroachDB locally: `curl "http://localhost:62025/wheels/core/tests?db=cockroachdb&format=json"`
110+
3. Confirm 0 fail, 0 error for CockroachDB across both engines
111+
4. Run Lucee 6 + H2 to verify no regressions: `curl "http://localhost:60006/wheels/core/tests?db=h2&format=json"`
112+
5. Push and verify CI passes with CockroachDB as a blocking database
113+
114+
## Open questions
115+
116+
- Some transaction failures may be fixable in the framework (not just test expectations) — investigate after fixing SQL generation to see which persist
117+
- IN clause failures (`works with IN operator with spaces`) may be a separate SQL generation or CockroachDB dialect issue — investigate after main fix
118+
- Guard pattern: use inline `adapterName() != "CockroachDB"` check (matches existing pattern in CockroachDBIntegrationSpec.cfc) rather than adding a new base class helper — keeps change minimal

vendor/wheels/databaseAdapters/Base.cfc

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,13 @@ component output=false extends="wheels.Global"{
105105
}
106106

107107
// Manual identity retrieval for Lucee / ACF
108+
// Pass the query result (if any) as returningIdentity — needed by adapters
109+
// that use RETURNING clauses (e.g. CockroachDB) to retrieve generated keys.
108110
wheels.id = $identitySelect(
109-
primaryKey = args.primaryKey,
110-
queryAttributes = args.queryAttributes,
111-
result = wheels.result
111+
primaryKey = args.primaryKey,
112+
queryAttributes = args.queryAttributes,
113+
result = wheels.result,
114+
returningIdentity = structKeyExists(local, args.debugName) ? local[args.debugName] : ""
112115
);
113116

114117
if (structKeyExists(wheels,"id") && isStruct(wheels.id) && !structIsEmpty(wheels.id)) {
@@ -168,7 +171,8 @@ component output=false extends="wheels.Global"{
168171
public any function $identitySelect(
169172
required struct queryAttributes,
170173
required struct result,
171-
required string primaryKey
174+
required string primaryKey,
175+
any returningIdentity = ""
172176
) {
173177
local.query = {};
174178
local.sql = Trim(arguments.result.sql);

vendor/wheels/databaseAdapters/CockroachDB/CockroachDBModel.cfc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ component extends="wheels.databaseAdapters.PostgreSQL.PostgreSQLModel" output=fa
1010
switch (arguments.type) {
1111
case "bool":
1212
case "boolean":
13-
local.rv = "cf_sql_boolean";
13+
local.rv = "cf_sql_bit";
1414
break;
1515
case "bit":
1616
case "varbit":

vendor/wheels/databaseAdapters/MicrosoftSQLServer/MicrosoftSQLServerModel.cfc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,8 @@ component extends="wheels.databaseAdapters.Base" output=false {
224224
public any function $identitySelect(
225225
required struct queryAttributes,
226226
required struct result,
227-
required string primaryKey
227+
required string primaryKey,
228+
any returningIdentity = ""
228229
) {
229230
var query = {};
230231
local.sql = Trim(arguments.result.sql);

vendor/wheels/databaseAdapters/Oracle/OracleModel.cfc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ component extends="wheels.databaseAdapters.Base" output=false {
9494
public any function $identitySelect(
9595
required struct queryAttributes,
9696
required struct result,
97-
required string primaryKey
97+
required string primaryKey,
98+
any returningIdentity = ""
9899
) {
99100
var query = {};
100101
local.sql = Trim(arguments.result.sql);

vendor/wheels/databaseAdapters/PostgreSQL/PostgreSQLModel.cfc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ component extends="wheels.databaseAdapters.Base" output=false {
124124
public any function $identitySelect(
125125
required struct queryAttributes,
126126
required struct result,
127-
required string primaryKey
127+
required string primaryKey,
128+
any returningIdentity = ""
128129
) {
129130
var query = {};
130131
local.sql = Trim(arguments.result.sql);

vendor/wheels/databaseAdapters/SQLite/SQLiteModel.cfc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ component extends="wheels.databaseAdapters.Base" output=false {
8282
public any function $identitySelect(
8383
required struct queryAttributes,
8484
required struct result,
85-
required string primaryKey
85+
required string primaryKey,
86+
any returningIdentity = ""
8687
) {
8788
var query = {};
8889
var local = {};

vendor/wheels/model/sql.cfc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ component {
650650
// Issue#1273: Added this section to allow included tables to be referenced in the query
651651
local.migration = CreateObject("component", "wheels.migrator.Migration").init();
652652
local.tempSql = "";
653-
if(arguments.include != "" && ListFind('PostgreSQL,H2,MicrosoftSQLServer,Oracle,SQLite', local.migration.adapter.adapterName()) && structKeyExists(arguments, "sql")){
653+
if(arguments.include != "" && ListFind('PostgreSQL,CockroachDB,H2,MicrosoftSQLServer,Oracle,SQLite', local.migration.adapter.adapterName()) && structKeyExists(arguments, "sql")){
654654
local.tempSql = arguments.sql;
655655
}
656656
local.whereClause = $whereClause(
@@ -661,7 +661,7 @@ component {
661661
useIndex = arguments.useIndex,
662662
sql = local.tempSql
663663
);
664-
if(arguments.include != "" && ListFind('PostgreSQL', local.migration.adapter.adapterName()) && structKeyExists(arguments, "sql")){
664+
if(arguments.include != "" && ListFind('PostgreSQL,CockroachDB', local.migration.adapter.adapterName()) && structKeyExists(arguments, "sql")){
665665
if(left(arguments.sql[1], 6) == 'UPDATE'){
666666
ArrayAppend(arguments.sql, "FROM #arguments.include#");
667667
}
@@ -698,7 +698,7 @@ component {
698698
// Issue#1273: Added this section to allow included tables to be referenced in the query
699699
local.joinclause = "";
700700
local.migration = CreateObject("component", "wheels.migrator.Migration").init();
701-
if(arguments.include != "" && ListFind('PostgreSQL,H2', local.migration.adapter.adapterName()) && left(arguments.sql[1], 6) == 'UPDATE'){
701+
if(arguments.include != "" && ListFind('PostgreSQL,CockroachDB,H2', local.migration.adapter.adapterName()) && left(arguments.sql[1], 6) == 'UPDATE'){
702702
for(local.i = 1; local.i<= arrayLen(local.classes); i++){
703703
if(structKeyExists(local.classes[local.i], "JOIN")){
704704
local.joinclause &= local.classes[local.i].JOIN.Split("ON")[2];

vendor/wheels/model/update.cfc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ component {
105105
ArrayAppend(arguments.sql, "UPDATE #$quotedTableName()# SET");
106106
}
107107
}
108-
else if (ListFind('PostgreSQL,H2,Oracle,SQLite', local.migration.adapter.adapterName())){
108+
else if (ListFind('PostgreSQL,CockroachDB,H2,Oracle,SQLite', local.migration.adapter.adapterName())){
109109
ArrayAppend(arguments.sql, "UPDATE #$quotedTableName()# SET");
110110
}
111111
local.pos = 0;

0 commit comments

Comments
 (0)