Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
931 changes: 931 additions & 0 deletions tools/docs-validation/state.json

Large diffs are not rendered by default.

23 changes: 22 additions & 1 deletion vendor/wheels/public/docs/reference/migration/addcolumn.txt
Original file line number Diff line number Diff line change
@@ -1 +1,22 @@
addColumn(table='members', columnType='string', columnName='status', limit=50);
// 1. Add a simple string column to an existing table
addColumn(table="members", columnType="string", columnName="status", limit=50);

// 2. Add a boolean column with a default value and no NULLs allowed
addColumn(
table="members",
columnType="boolean",
columnName="isActive",
default=1,
allowNull=false
);

// 3. Add a decimal column with precision and scale (e.g. for a price field)
addColumn(
table="products",
columnType="decimal",
columnName="price",
precision=10,
scale=2,
default=0,
allowNull=false
);
31 changes: 31 additions & 0 deletions vendor/wheels/public/docs/reference/migration/addforeignkey.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 1. Add a foreign key from orders.customerId to customers.id
addForeignKey(
table="orders",
referenceTable="customers",
column="customerId",
referenceColumn="id"
);

// 2. Add a foreign key from comments.postId to posts.id
addForeignKey(
table="comments",
referenceTable="posts",
column="postId",
referenceColumn="id"
);

// 3. Use addForeignKey in a migration's up() and remove it in down()
// In your migration CFC:
//
// public void function up() {
// addForeignKey(
// table="order_items",
// referenceTable="orders",
// column="orderId",
// referenceColumn="id"
// );
// }
//
// public void function down() {
// dropForeignKey(table="order_items", keyName="FK_order_items_orders");
// }
13 changes: 12 additions & 1 deletion vendor/wheels/public/docs/reference/migration/addindex.txt
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
addIndex(table='members', columnNames='username', unique=true);
// 1. Add a basic index on a single column
addIndex(table="users", columnNames="email");

// 2. Add a unique index to enforce uniqueness on a column
addIndex(table="members", columnNames="username", unique=true);

// 3. Add a composite index on multiple columns
addIndex(table="orders", columnNames="customerId,createdAt");

// 4. Add an index with a custom index name
// (defaults to tableName_firstColumnName, e.g. "posts_publishedAt")
addIndex(table="posts", columnNames="publishedAt", indexName="idx_posts_published");
38 changes: 31 additions & 7 deletions vendor/wheels/public/docs/reference/migration/addrecord.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
addRecord(table='people',
id = 1,
title = "Mr",
firstname = "Bruce",
lastname = "Wayne",
email = "bruce@wayneenterprises.com",
tel = "555-67869099",
// 1. Insert a simple seed record into a settings table
addRecord(
table = "settings",
name = "siteName",
value = "My Wheels App"
);

// 2. Insert a record with multiple columns (extra keyword arguments become column/value pairs)
addRecord(
table = "people",
id = 1,
title = "Mr",
firstName = "Bruce",
lastName = "Wayne",
email = "bruce@wayneenterprises.com",
phone = "555-678-9099"
);

// 3. Seed an admin user role during a migration's up() function
component extends="wheels.migrator.Migration" {
function up() {
addRecord(
table = "roles",
id = 1,
name = "admin",
active = true
);
}
function down() {
removeRecord(table = "roles", where = "id = 1");
}
}
17 changes: 17 additions & 0 deletions vendor/wheels/public/docs/reference/migration/addreference.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// 1. Add a foreign key from comments.postId to posts.id using a reference name
// Equivalent to: addForeignKey(table="comments", referenceTable="posts", column="postId", referenceColumn="id")
addReference(table="comments", referenceName="post");

// 2. Add a foreign key from order_items.orderId to orders.id
addReference(table="order_items", referenceName="order");

// 3. Use addReference in a migration's up() and undo it with dropReference() in down()
// In your migration CFC:
//
// public void function up() {
// addReference(table="comments", referenceName="post");
// }
//
// public void function down() {
// dropReference(table="comments", referenceName="post");
// }
44 changes: 44 additions & 0 deletions vendor/wheels/public/docs/reference/migration/announce.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// 1. Log a custom message during a migration's up() step
function up() {
transaction {
try {
createTable(name="articles", force=true) {
t.string(columnNames="title", null=false);
Comment on lines +5 to +6

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 In announce.txt, two examples pass null=false (line 6) and null=true (line 28) — but neither TableDefinition.string() nor addColumn() has a null parameter; the correct argument name is allowNull. CFML silently accepts unknown named arguments, so users copy/pasting these examples will get column definitions with the default nullability instead of the intended constraint, and every other example added in this PR consistently uses allowNull. Replace null=false with allowNull=false on line 6 and null=true with allowNull=true on line 28.

Extended reasoning...

What the bug is

The two examples in the new announce.txt file pass an unrecognized null keyword argument:

  • Line 6: t.string(columnNames="title", null=false);
  • Line 28: addColumn(table="users", columnType="string", columnName="apiKey", limit=64, null=true);

Both call sites are documenting how to constrain (or relax) NULLability on a column, but the actual parameter is named allowNull, not null.

Why this is wrong

I verified both function signatures directly:

  • vendor/wheels/migrator/TableDefinition.cfc:258 — public any function string(string columnNames, any limit, string default, boolean allowNull)
  • vendor/wheels/migrator/Migration.cfc:164-174 — public void function addColumn(required string table, required string columnType, required string columnName = "", string afterColumn = "", string referenceName = "", string default, boolean allowNull, numeric limit, numeric precision, numeric scale)

Neither function declares a null parameter. CFML does not error on unknown named arguments — it accepts them into the arguments struct and the called function simply never reads them. So the column ends up created with whatever the underlying default nullability is, not what the example reads as if it were specifying.

Step-by-step proof

  1. User copies example New master #2 from announce.txt: addColumn(table="users", columnType="string", columnName="apiKey", limit=64, null=true);
  2. CFML invokes addColumn with arguments struct: {table:"users", columnType:"string", columnName:"apiKey", limit:64, null:true}.
  3. addColumn (Migration.cfc:164) sets arguments.addColumns = true and forwards to changeColumn(argumentCollection=arguments).
  4. changeColumn reads named parameters by their declared names — allowNull, default, limit, etc. The null key in the struct is never read.
  5. The column is created with the database default for nullability (typically NULL allowed). The user reads "null=true" and assumes they explicitly opted in to NULLs, but in reality the argument was silently dropped — there was never any control flow that examined arguments.null.

Why other examples don't hit this

Every other example added in this PR — addcolumn.txt, biginteger.txt, boolean.txt, changecolumn.txt, createtable.txt, string.txt, text.txt, etc. — uses allowNull=…. The announce.txt examples are the only inconsistency.

Impact

Reference docs are copy-paste templates. A null=false in an example reads as "this column will be NOT NULL," but the produced DDL will have the database default. For a migration that is supposed to enforce a NOT NULL constraint on articles.title (example #1), this is a real correctness drift — silently passing tests in dev (where data happens to be present) but failing the moment a row is inserted without the column.

Fix

In vendor/wheels/public/docs/reference/migration/announce.txt:

  • Line 6: change null=false to allowNull=false
  • Line 28: change null=true to allowNull=true

t.text("body");
t.timestamps();
};
announce("Created articles table");
} catch (any e) {
local.exception = e;
}

if (StructKeyExists(local, "exception")) {
transaction action="rollback";
throw(errorCode="1", detail=local.exception.detail, message=local.exception.message, type="any");
} else {
transaction action="commit";
}
}
}

// 2. Announce multiple steps to provide granular feedback
function up() {
transaction {
try {
addColumn(table="users", columnType="string", columnName="apiKey", limit=64, null=true);
announce("Added apiKey column to users");

addIndex(table="users", columnNames="apiKey", unique=true);
announce("Added unique index on users.apiKey");
} catch (any e) {
local.exception = e;
}

if (StructKeyExists(local, "exception")) {
transaction action="rollback";
throw(errorCode="1", detail=local.exception.detail, message=local.exception.message, type="any");
} else {
transaction action="commit";
}
}
}
12 changes: 11 additions & 1 deletion vendor/wheels/public/docs/reference/migration/changecolumn.txt
Original file line number Diff line number Diff line change
@@ -1 +1,11 @@
changeColumn(table='members', columnType='string', columnName='status', limit=50);
// 1. Change a string column's length limit
changeColumn(table="members", columnName="status", columnType="string", limit=50);

// 2. Change a column type and set a default value
changeColumn(table="orders", columnName="quantity", columnType="integer", default=1);

// 3. Change a decimal column with precision and scale
changeColumn(table="products", columnName="price", columnType="decimal", precision=10, scale=2, allowNull=false);

// 4. Change a text column and explicitly allow NULL values
changeColumn(table="articles", columnName="summary", columnType="text", allowNull=true);
16 changes: 14 additions & 2 deletions vendor/wheels/public/docs/reference/migration/changetable.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
t = changeTable(name='employees');
t.string(columnNames="fullName", default="", allowNull=true, limit="255");
// 1. Add a new string column to an existing table
t = changeTable(name="employees");
t.string(columnNames="fullName", default="", allowNull=true, limit=255);
t.change();

// 2. Modify multiple columns at once (change type and nullability)
t = changeTable(name="products");
t.integer(columnNames="stock", default=0, allowNull=false);
t.boolean(columnNames="active", default=true, allowNull=false);
t.change();

// 3. Add a new column using addColumns=true so the migration fails gracefully if the column already exists
t = changeTable(name="orders");
t.datetime(columnNames="shippedAt", allowNull=true);
t.change(addColumns=true);
32 changes: 15 additions & 17 deletions vendor/wheels/public/docs/reference/migration/createtable.txt
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
// Example: create a users table
t = createTable(name='users');
t.string(columnNames='firstname,lastname', default='', allowNull=false, limit=50);
t.string(columnNames='email', default='', allowNull=false, limit=255);
t.string(columnNames='passwordHash', default='', allowNull=true, limit=500);
t.string(columnNames='passwordResetToken,verificationToken', default='', allowNull=true, limit=500);
t.boolean(columnNames='passwordChangeRequired,verified', default=false);
t.datetime(columnNames='passwordResetTokenAt,passwordResetAt,loggedinAt', default='', allowNull=true);
t.integer(columnNames='roleid', default=0, allowNull=false, limit=3);
// 1. Create a users table with standard columns and timestamps
t = createTable(name="users");
t.string(columnNames="firstName,lastName", default="", allowNull=false, limit=50);
t.string(columnNames="email", default="", allowNull=false, limit=255);
t.string(columnNames="passwordHash", default="", allowNull=true, limit=500);
t.boolean(columnNames="verified", default=false);
t.integer(columnNames="roleId", default=0, allowNull=false);
t.timestamps();
t.create();

// Example: Create a table with a different Primary Key
t = createTable(name='tokens', id=false);
t.primaryKey(name='id', allowNull=false, type="string", limit=35 );
// 2. Create a table with a custom string primary key (disabling the auto integer id)
t = createTable(name="tokens", id=false);
t.primaryKey(name="id", allowNull=false, type="string", limit=36);
t.string(columnNames="userId", allowNull=false, limit=36);
t.datetime(columnNames="expiresAt", allowNull=false);
t.integer(columnNames='requests', default=0, allowNull=false);
t.timestamps();
t.create();

// Example: Create a Join Table with composite primary keys
t = createTable(name='userkintins', id=false);
t.primaryKey(name="userid", allowNull=false, limit=11);
t.primaryKey(name='profileid', type="string", limit=11 );
// 3. Create a join table with composite primary keys and force=true to recreate if it already exists
t = createTable(name="userRoles", id=false, force=true);
t.primaryKey(name="userId", allowNull=false, limit=11);
t.primaryKey(name="roleId", allowNull=false, limit=11);
t.create();
21 changes: 21 additions & 0 deletions vendor/wheels/public/docs/reference/migration/createview.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// 1. Create a simple database view that joins users and roles
v = createView(name="userRoles");
v.selectStatement(sql="SELECT u.id, u.firstName, u.lastName, r.name AS roleName FROM users u INNER JOIN roles r ON u.roleId = r.id");
v.create();

// 2. Create a view for active (non-deleted) users using method chaining
createView(name="activeUsers")
.selectStatement(sql="SELECT id, firstName, lastName, email FROM users WHERE deletedAt IS NULL")
.create();

// 3. Full up/down migration using createView and dropView
component extends="wheels.migrator.Migration" {
function up() {
createView(name="publishedArticles")
.selectStatement(sql="SELECT id, title, body, authorId, publishedAt FROM articles WHERE publishedAt IS NOT NULL")
.create();
}
function down() {
dropView(name="publishedArticles");
}
}
86 changes: 82 additions & 4 deletions vendor/wheels/public/docs/reference/migration/down.txt
Original file line number Diff line number Diff line change
@@ -1,17 +1,95 @@
// 1. Reverse a table creation by dropping the table
// Called automatically when rolling back this migration
function down() {
transaction {
try {
// your code goes here
dropTable('myTable');
dropTable("employees");
} catch (any e) {
local.exception = e;
}

if (StructKeyExists(local, "exception")) {
if (structKeyExists(local, "exception")) {
transaction action="rollback";
throw(errorCode="1", detail=local.exception.detail, message=local.exception.message, type="any");
throw(
errorCode = "1",
detail = local.exception.detail,
message = local.exception.message,
type = "any"
);
} else {
transaction action="commit";
}
}
}

// 2. Reverse an addColumn() call by removing the column
function down() {
transaction {
try {
removeColumn(table="users", columnName="biography");
} catch (any e) {
local.exception = e;
}

if (structKeyExists(local, "exception")) {
transaction action="rollback";
throw(
errorCode = "1",
detail = local.exception.detail,
message = local.exception.message,
type = "any"
);
} else {
transaction action="commit";
}
}
}

// 3. Paired up() and down() inside a full migration component
component extends="[extends]" hint="Add status column to orders" {

function up() {
transaction {
try {
addColumn(table="orders", columnType="string", columnName="status", limit=50, default="pending");
} catch (any e) {
local.exception = e;
}

if (structKeyExists(local, "exception")) {
transaction action="rollback";
throw(
errorCode = "1",
detail = local.exception.detail,
message = local.exception.message,
type = "any"
);
} else {
transaction action="commit";
}
}
}

function down() {
transaction {
try {
removeColumn(table="orders", columnName="status");
} catch (any e) {
local.exception = e;
}

if (structKeyExists(local, "exception")) {
transaction action="rollback";
throw(
errorCode = "1",
detail = local.exception.detail,
message = local.exception.message,
type = "any"
);
} else {
transaction action="commit";
}
}
}

}
13 changes: 13 additions & 0 deletions vendor/wheels/public/docs/reference/migration/dropforeignkey.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// 1. Drop a foreign key constraint by its explicit name
// In the down() function, remove a foreign key added in up()
dropForeignKey(table="orders", keyName="FK_orders_customers");

// 2. Drop a foreign key that was created via addReference()
// addReference() creates keys named FK_<table>_<pluralizedReference>
// So addReference(table="comments", referenceName="post") creates "FK_comments_posts"
dropForeignKey(table="comments", keyName="FK_comments_posts");

// 3. Use inside a migration's down() to reverse an addForeignKey() call
// up() called: addForeignKey(table="profiles", referenceTable="users", column="userId", referenceColumn="id")
// down() reverses it:
dropForeignKey(table="profiles", keyName="FK_profiles_users");
18 changes: 18 additions & 0 deletions vendor/wheels/public/docs/reference/migration/dropreference.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 1. Drop the foreign key from comments.postId back to posts.id
// Removes the constraint named FK_comments_posts
dropReference(table="comments", referenceName="post");

// 2. Drop a foreign key from order_items back to orders
// Removes the constraint named FK_order_items_orders
dropReference(table="order_items", referenceName="order");

// 3. Use dropReference in the down() of a migration that added a reference in up()
// In your migration CFC:
//
// public void function up() {
// addReference(table="comments", referenceName="post");
// }
//
// public void function down() {
// dropReference(table="comments", referenceName="post");
// }
Loading