Skip to content

Commit ed9cf93

Browse files
fix(migrator): add addForeignKeyOptions to PostgreSQL adapter so FK migrations run (#2877)
The PostgreSQL migrator was the only adapter missing addForeignKeyOptions(sql, options), so any migration emitting an inline foreign-key constraint — e.g. wheels generate scaffold ... --belongsTo=author — crashed with "Component [wheels.databaseAdapters.PostgreSQL.PostgreSQLMigrator] has no function with name [addForeignKeyOptions]" when run against PG. The new implementation mirrors the MySQL signature; CockroachDB inherits the fix via its PostgreSQLMigrator subclass. Fixes #2876 Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
1 parent ffd4ac0 commit ed9cf93

3 files changed

Lines changed: 122 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ All historical references to "CFWheels" in this changelog have been preserved fo
2222

2323
### Fixed
2424

25+
- `wheels migrate latest` no longer crashes on PostgreSQL (and CockroachDB) when a migration emits an inline foreign-key constraint — e.g. anything `wheels generate scaffold ... --belongsTo=author` produces. `wheels.databaseAdapters.PostgreSQL.PostgreSQLMigrator` was missing the public `addForeignKeyOptions(sql, options)` method that every other adapter implements (`MySQLMigrator`, `SQLiteMigrator`, `MicrosoftSQLServerMigrator`, `OracleMigrator`); `Abstract.createTable()` builds the inline FK clause via `foreignKeys[i].toForeignKeySQL()` → `ForeignKeyDefinition.cfc` → `adapter.addForeignKeyOptions(...)`, so every PostgreSQL FK column threw `Component [wheels.databaseAdapters.PostgreSQL.PostgreSQLMigrator] has no function with name [addForeignKeyOptions]` and aborted the migration. The new implementation mirrors the MySQL signature (`FOREIGN KEY (col) REFERENCES tbl (refCol)`), which PostgreSQL accepts verbatim, and `CockroachDBMigrator` (which extends `PostgreSQLMigrator`) inherits the fix automatically. The reporter's "works on Windows" observation lined up with the `wheels new` SQLite default — only PostgreSQL/CockroachDB targets ever hit the missing method (#2876)
2526
- CLI services in `Module.cfc` now instantiate via the module-relative path (`new services.X()`) instead of the absolute FQN (`new cli.lucli.services.X()`), so `wheels new` and the other subcommands resolve their service classes when running from the installed distribution. The module tarball is built with `tar -C cli/lucli .`, which flattens the module root so services live at `<module-root>/services/` with no `cli/lucli/` tree and no `cli.lucli` mapping — the absolute form only resolved against the source-tree layout. That split is why the `fast-test` job (which runs from source, where both forms resolve) stayed green while the snapshot smoke test — which installs the built tarball and runs `wheels new` — failed on every `develop` push since #2861 with `could not find component or class with name [cli.lucli.services.ArgSpec]`. All 8 absolute references (7× `ArgSpec`, 1× the latent `TestRunner` call) are converted to the relative form the 17 sibling services already use; the `ArgSpec` docblock example is updated to match so it cannot re-seed the pattern (#2873)
2627
- Oracle `DROP TABLE` / `DROP VIEW` in the migrator now work on Oracle 19c/21c. `wheels.databaseAdapters.Oracle.OracleMigrator::dropTable()` emitted `DROP TABLE IF EXISTS <name> CASCADE CONSTRAINTS` and `dropView()` inherited `DROP VIEW IF EXISTS` from `Abstract`, but Oracle only added the `IF EXISTS` DDL modifier in 23c — on 19c/21c both are a hard parse error (ORA-00933). Because the `remove-table` migration template re-throws on error, `migrate down`, rollbacks, `force`-create, and migrator test re-runs failed outright on pre-23c Oracle. Both helpers now emit the version-agnostic Oracle PL/SQL idiom — `BEGIN EXECUTE IMMEDIATE 'DROP TABLE <name> CASCADE CONSTRAINTS'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;` — which runs the bare DROP and swallows ORA-00942 ("table or view does not exist"), preserving "drop if exists" semantics on every supported Oracle version with no version detection. `$execute` (`vendor/wheels/migrator/Base.cfc`) never splits on `;` and deliberately omits the trailing-semicolon append for Oracle, so the anonymous block reaches the driver intact. Framework-side counterpart to the demo-app test-populate fix in #2864 (#2869)
2728
- `application.wheels.protectedControllerMethods` is now populated at application start from the public method surface of `wheels.Global` plus the `wheels.controller.*` and `wheels.view.*` mixin components, so framework helpers like `env()`, `model()`, `findAll()`, `redirectTo()`, and `linkTo()` can no longer be invoked as controller actions from a URL. The list was previously initialized to an empty string (the orphaned `local.allowedGlobalMethods = "get,set,mapper"` line in `onapplicationstart.cfc` pointed to the intent but never wired it up), so `$callAction()`'s allow-list check was a no-op. Any unauthenticated `GET /<anyController>/env` request reached the global `env()` helper directly and raised `"The parameter [name] to function [env] is required but was not passed in."` as a 500; other helper names dispatched into unintended code paths. Derived from `getMetaData().functions` on each source component (excluding `$`-prefixed internal methods, which are already gated separately), so the list stays in sync with the framework's mixin surface automatically. Reaching one of these names now throws `Wheels.ActionNotAllowed` and falls through to the missing-action / 404 path, matching every other non-existent action. **Migration note:** applications that defined controller actions with the same name as a public framework helper (e.g. `env`, `model`, `redirectTo`) will need to rename those actions — they now return 404 rather than dispatching, since the protection gate at `processing.cfc:132` fires before the `StructKeyExists(this, action)` lookup that would otherwise reach a same-named user action (#2844)

vendor/wheels/databaseAdapters/PostgreSQL/PostgreSQLMigrator.cfc

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,24 @@ component extends="wheels.databaseAdapters.Abstract" {
2020
return "PostgreSQL";
2121
}
2222

23+
/**
24+
* generates sql fragment for inline foreign key constraint definition,
25+
* used by ForeignKeyDefinition.toForeignKeySQL() (which Abstract.createTable()
26+
* splices into the CREATE TABLE body). Without this, any `t.references()`
27+
* or scaffold `--belongsTo=` on PostgreSQL throws at migrate time. See #2876.
28+
*/
29+
public string function addForeignKeyOptions(required string sql, struct options = {}) {
30+
arguments.sql = arguments.sql & " FOREIGN KEY (" & arguments.options.column & ")";
31+
if (StructKeyExists(arguments.options, "referenceTable")) {
32+
if (StructKeyExists(arguments.options, "referenceColumn")) {
33+
arguments.sql = arguments.sql & " REFERENCES ";
34+
arguments.sql = arguments.sql & arguments.options.referenceTable;
35+
arguments.sql = arguments.sql & " (" & arguments.options.referenceColumn & ")";
36+
}
37+
}
38+
return arguments.sql;
39+
}
40+
2341
/**
2442
* generates sql for primary key options
2543
*/
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Regression coverage for #2876 — `wheels migrate latest` failed on Linux
3+
* whenever a migration ran `t.references()` against PostgreSQL with the
4+
* error:
5+
*
6+
* Component [wheels.databaseAdapters.PostgreSQL.PostgreSQLMigrator]
7+
* has no function with name [addForeignKeyOptions]
8+
*
9+
* `Abstract.createTable()` builds the inline FK clause via
10+
* `foreignKeys[i].toForeignKeySQL()` → `ForeignKeyDefinition.cfc` →
11+
* `adapter.addForeignKeyOptions(sql, options)`. Every other adapter
12+
* implements that method (MySQL, SQLite, MSSQL, Oracle); only PostgreSQL
13+
* was missing it, so any scaffold that produced an FK column blew up at
14+
* migrate time. CockroachDB extends PostgreSQLMigrator and inherited the
15+
* same gap.
16+
*
17+
* These specs run at the adapter unit layer — the adapter is instantiated
18+
* directly and `addForeignKeyOptions` is called with the same option
19+
* struct shape that `ForeignKeyDefinition::addForeignKeyOptions` builds
20+
* (`column`, `referenceTable`, `referenceColumn`). That keeps the
21+
* assertions adapter-independent of the currently-configured test
22+
* datasource — exactly the pattern referencesSpec.cfc uses for
23+
* TableDefinition-layer plumbing.
24+
*/
25+
component extends="wheels.WheelsTest" {
26+
27+
function beforeAll() {
28+
variables.pgAdapter = CreateObject("component", "wheels.databaseAdapters.PostgreSQL.PostgreSQLMigrator");
29+
variables.cockroachAdapter = CreateObject("component", "wheels.databaseAdapters.CockroachDB.CockroachDBMigrator");
30+
}
31+
32+
function run() {
33+
34+
describe("PostgreSQLMigrator.addForeignKeyOptions()", () => {
35+
36+
it("exists as a public method on the adapter", () => {
37+
var fns = getMetaData(variables.pgAdapter).functions;
38+
var found = false;
39+
for (var fn in fns) {
40+
if (fn.name == "addForeignKeyOptions") {
41+
found = true;
42+
break;
43+
}
44+
}
45+
expect(found).toBeTrue();
46+
});
47+
48+
it("appends FOREIGN KEY (col) REFERENCES table (refCol) to the constraint sql", () => {
49+
var sql = variables.pgAdapter.addForeignKeyOptions(
50+
sql = "CONSTRAINT FK_posts_users",
51+
options = {
52+
column: "userid",
53+
referenceTable: "users",
54+
referenceColumn: "id"
55+
}
56+
);
57+
expect(sql).toInclude("FOREIGN KEY");
58+
expect(sql).toInclude("userid");
59+
expect(sql).toInclude("REFERENCES");
60+
expect(sql).toInclude("users");
61+
expect(sql).toInclude("id");
62+
});
63+
64+
});
65+
66+
describe("CockroachDBMigrator inherits the PostgreSQL fix", () => {
67+
68+
it("exposes addForeignKeyOptions via PostgreSQLMigrator inheritance", () => {
69+
var sql = variables.cockroachAdapter.addForeignKeyOptions(
70+
sql = "CONSTRAINT FK_posts_users",
71+
options = {
72+
column: "userid",
73+
referenceTable: "users",
74+
referenceColumn: "id"
75+
}
76+
);
77+
expect(sql).toInclude("FOREIGN KEY");
78+
expect(sql).toInclude("REFERENCES");
79+
});
80+
81+
});
82+
83+
describe("ForeignKeyDefinition.toForeignKeySQL() integrates with PostgreSQLMigrator", () => {
84+
85+
it("does not throw when toForeignKeySQL() walks through the PG adapter", () => {
86+
var fk = CreateObject("component", "wheels.migrator.ForeignKeyDefinition").init(
87+
adapter = variables.pgAdapter,
88+
table = "posts",
89+
referenceTable = "users",
90+
column = "userid",
91+
referenceColumn = "id"
92+
);
93+
var sql = fk.toForeignKeySQL();
94+
expect(sql).toInclude("CONSTRAINT");
95+
expect(sql).toInclude("FOREIGN KEY");
96+
expect(sql).toInclude("REFERENCES");
97+
});
98+
99+
});
100+
101+
}
102+
103+
}

0 commit comments

Comments
 (0)