diff --git a/core/src/wheels/databaseAdapters/H2/H2Model.cfc b/core/src/wheels/databaseAdapters/H2/H2Model.cfc index ea4b169e1..7152d090f 100755 --- a/core/src/wheels/databaseAdapters/H2/H2Model.cfc +++ b/core/src/wheels/databaseAdapters/H2/H2Model.cfc @@ -180,5 +180,12 @@ component extends="wheels.databaseAdapters.Base" output=false { return local.columns; } + /** + * Define H2 reserved words. + */ + public string function $escapeReservedWords(required string word) { + // TODO + return arguments.word; + } } diff --git a/core/src/wheels/databaseAdapters/MicrosoftSQLServer/MicrosoftSQLServerModel.cfc b/core/src/wheels/databaseAdapters/MicrosoftSQLServer/MicrosoftSQLServerModel.cfc index 90f658b99..e10d6a611 100755 --- a/core/src/wheels/databaseAdapters/MicrosoftSQLServer/MicrosoftSQLServerModel.cfc +++ b/core/src/wheels/databaseAdapters/MicrosoftSQLServer/MicrosoftSQLServerModel.cfc @@ -235,16 +235,16 @@ component extends="wheels.databaseAdapters.Base" output=false { ); if (!ListFindNoCase(local.columnList, ListFirst(arguments.primaryKey))) { local.rv = {}; - + // Use @@IDENTITY instead of SCOPE_IDENTITY() for BoxLang compatibility // SCOPE_IDENTITY() returns empty values in BoxLang with SQL Server query = $query(sql = "SELECT @@IDENTITY AS lastId", argumentCollection = arguments.queryAttributes); - + // Fallback to SCOPE_IDENTITY() if @@IDENTITY fails (for other CFML engines) if (!len(query.lastId)) { query = $query(sql = "SELECT SCOPE_IDENTITY() AS lastId", argumentCollection = arguments.queryAttributes); } - + local.rv[$generatedKey()] = query.lastId; return local.rv; } @@ -258,5 +258,12 @@ component extends="wheels.databaseAdapters.Base" output=false { return "NEWID()"; } + /** + * Define MSSQL reserved words. + */ + public string function $escapeReservedWords(required string word) { + // TODO + return arguments.word; + } } diff --git a/core/src/wheels/databaseAdapters/MySQL/MySQLModel.cfc b/core/src/wheels/databaseAdapters/MySQL/MySQLModel.cfc index 19f418073..9f1d95da4 100755 --- a/core/src/wheels/databaseAdapters/MySQL/MySQLModel.cfc +++ b/core/src/wheels/databaseAdapters/MySQL/MySQLModel.cfc @@ -115,5 +115,279 @@ component extends="wheels.databaseAdapters.Base" output=false { return "() VALUES()"; } + /** + * Define MySQL reserved words. + */ + public string function $escapeReservedWords(required string word) { + local.reservedWords = { + "accessible" = true, + "add" = true, + "all" = true, + "alter" = true, + "analyze" = true, + "and" = true, + "as" = true, + "asc" = true, + "asensitive" = true, + "before" = true, + "between" = true, + "bigint" = true, + "binary" = true, + "blob" = true, + "both" = true, + "by" = true, + "call" = true, + "cascade" = true, + "case" = true, + "change" = true, + "char" = true, + "character" = true, + "check" = true, + "collate" = true, + "column" = true, + "condition" = true, + "constraint" = true, + "continue" = true, + "convert" = true, + "create" = true, + "cross" = true, + "cube" = true, + "cume_dist" = true, + "current_date" = true, + "current_time" = true, + "current_timestamp" = true, + "current_user" = true, + "cursor" = true, + "database" = true, + "databases" = true, + "day_hour" = true, + "day_microsecond" = true, + "day_minute" = true, + "day_second" = true, + "dec" = true, + "decimal" = true, + "declare" = true, + "default" = true, + "delayed" = true, + "delete" = true, + "dense_rank" = true, + "desc" = true, + "describe" = true, + "deterministic" = true, + "distinct" = true, + "distinctrow" = true, + "div" = true, + "double" = true, + "drop" = true, + "dual" = true, + "each" = true, + "else" = true, + "elseif" = true, + "empty" = true, + "enclosed" = true, + "escaped" = true, + "except" = true, + "exists" = true, + "exit" = true, + "explain" = true, + "false" = true, + "fetch" = true, + "first_value" = true, + "float" = true, + "float4" = true, + "float8" = true, + "for" = true, + "force" = true, + "foreign" = true, + "from" = true, + "fulltext" = true, + "function" = true, + "generated" = true, + "get" = true, + "grant" = true, + "group" = true, + "grouping" = true, + "groups" = true, + "having" = true, + "high_priority" = true, + "hour_microsecond" = true, + "hour_minute" = true, + "hour_second" = true, + "if" = true, + "ignore" = true, + "in" = true, + "index" = true, + "infile" = true, + "inner" = true, + "inout" = true, + "insensitive" = true, + "insert" = true, + "int" = true, + "int1" = true, + "int2" = true, + "int3" = true, + "int4" = true, + "int8" = true, + "integer" = true, + "interval" = true, + "into" = true, + "io_after_gtids" = true, + "io_before_gtids" = true, + "is" = true, + "iterate" = true, + "join" = true, + "json_table" = true, + "key" = true, + "keys" = true, + "kill" = true, + "lag" = true, + "last_value" = true, + "lead" = true, + "leading" = true, + "leave" = true, + "left" = true, + "like" = true, + "limit" = true, + "linear" = true, + "lines" = true, + "load" = true, + "localtime" = true, + "localtimestamp" = true, + "lock" = true, + "long" = true, + "longblob" = true, + "longtext" = true, + "loop" = true, + "low_priority" = true, + "master_bind" = true, + "master_ssl_verify_server_cert" = true, + "match" = true, + "maxvalue" = true, + "mediumblob" = true, + "mediumint" = true, + "mediumtext" = true, + "middleint" = true, + "minute_microsecond" = true, + "minute_second" = true, + "mod" = true, + "modifies" = true, + "natural" = true, + "not" = true, + "no_write_to_binlog" = true, + "nth_value" = true, + "ntile" = true, + "null" = true, + "numeric" = true, + "of" = true, + "on" = true, + "optimize" = true, + "optimizer_costs" = true, + "option" = true, + "optionally" = true, + "or" = true, + "order" = true, + "out" = true, + "outer" = true, + "outfile" = true, + "over" = true, + "partition" = true, + "percent_rank" = true, + "persist" = true, + "persist_only" = true, + "precision" = true, + "primary" = true, + "procedure" = true, + "purge" = true, + "range" = true, + "rank" = true, + "read" = true, + "reads" = true, + "read_write" = true, + "real" = true, + "recursive" = true, + "references" = true, + "regexp" = true, + "release" = true, + "rename" = true, + "repeat" = true, + "replace" = true, + "require" = true, + "resignal" = true, + "restrict" = true, + "return" = true, + "revoke" = true, + "right" = true, + "rlike" = true, + "row" = true, + "rows" = true, + "row_number" = true, + "schema" = true, + "schemas" = true, + "second_microsecond" = true, + "select" = true, + "sensitive" = true, + "separator" = true, + "set" = true, + "show" = true, + "signal" = true, + "smallint" = true, + "spatial" = true, + "specific" = true, + "sql" = true, + "sqlexception" = true, + "sqlstate" = true, + "sqlwarning" = true, + "sql_big_result" = true, + "sql_calc_found_rows" = true, + "sql_small_result" = true, + "ssl" = true, + "starting" = true, + "stored" = true, + "straight_join" = true, + "system" = true, + "table" = true, + "terminated" = true, + "then" = true, + "tinyblob" = true, + "tinyint" = true, + "tinytext" = true, + "to" = true, + "trailing" = true, + "trigger" = true, + "true" = true, + "undo" = true, + "union" = true, + "unique" = true, + "unlock" = true, + "unsigned" = true, + "update" = true, + "usage" = true, + "use" = true, + "using" = true, + "utc_date" = true, + "utc_time" = true, + "utc_timestamp" = true, + "values" = true, + "varbinary" = true, + "varchar" = true, + "varcharacter" = true, + "varying" = true, + "virtual" = true, + "when" = true, + "where" = true, + "while" = true, + "window" = true, + "with" = true, + "write" = true, + "xor" = true, + "year_month" = true, + "zerofill" = true + }; + + if (StructKeyExists(local.reservedWords, arguments.word)) { + return "`#local.rv#`"; + } + return arguments.word; + } } diff --git a/core/src/wheels/databaseAdapters/Oracle/OracleModel.cfc b/core/src/wheels/databaseAdapters/Oracle/OracleModel.cfc index 2659e7b10..0b16ea9ca 100755 --- a/core/src/wheels/databaseAdapters/Oracle/OracleModel.cfc +++ b/core/src/wheels/databaseAdapters/Oracle/OracleModel.cfc @@ -141,5 +141,12 @@ component extends="wheels.databaseAdapters.Base" output=false { return arguments.table & " " & arguments.alias; } + /** + * Define Oracle reserved words. + */ + public string function $escapeReservedWords(required string word) { + // TODO + return arguments.word; + } } diff --git a/core/src/wheels/databaseAdapters/PostgreSQL/PostgreSQLModel.cfc b/core/src/wheels/databaseAdapters/PostgreSQL/PostgreSQLModel.cfc index 7175a5f5d..343c1252d 100755 --- a/core/src/wheels/databaseAdapters/PostgreSQL/PostgreSQLModel.cfc +++ b/core/src/wheels/databaseAdapters/PostgreSQL/PostgreSQLModel.cfc @@ -172,5 +172,12 @@ component extends="wheels.databaseAdapters.Base" output=false { return "random()"; } + /** + * Define Postgres reserved words. + */ + public string function $escapeReservedWords(required string word) { + // TODO + return arguments.word; + } } diff --git a/core/src/wheels/databaseAdapters/SQLite/SQLiteModel.cfc b/core/src/wheels/databaseAdapters/SQLite/SQLiteModel.cfc index eae27d151..5c5b6f3fa 100755 --- a/core/src/wheels/databaseAdapters/SQLite/SQLiteModel.cfc +++ b/core/src/wheels/databaseAdapters/SQLite/SQLiteModel.cfc @@ -104,7 +104,7 @@ component extends="wheels.databaseAdapters.Base" output=false { ",," ); } - + // If the primary key column wasn't part of the INSERT, we fetch last inserted ID if (!ListFindNoCase(local.columnList, ListFirst(arguments.primaryKey))) { local.rv = {}; @@ -126,4 +126,12 @@ component extends="wheels.databaseAdapters.Base" output=false { return " DEFAULT VALUES"; } + /** + * Define SQLite reserved words. + */ + public string function $escapeReservedWords(required string word) { + // TODO + return arguments.word; + } + } diff --git a/core/src/wheels/model/create.cfc b/core/src/wheels/model/create.cfc index 5c6460a74..331e86c57 100644 --- a/core/src/wheels/model/create.cfc +++ b/core/src/wheels/model/create.cfc @@ -301,7 +301,7 @@ component { ) ) ) { - ArrayAppend(local.sql, variables.wheels.class.properties[local.key].column); + ArrayAppend(local.sql, variables.wheels.class.adapter.$escapeReservedWords(variables.wheels.class.properties[local.key].column)); ArrayAppend(local.sql, ","); ArrayAppend(local.sql2, $buildQueryParamValues(local.key)); ArrayAppend(local.sql2, ","); @@ -310,7 +310,7 @@ component { if (ArrayLen(local.sql)) { // Create wrapping SQL code and merge the second array that holds the values with the first one. - ArrayPrepend(local.sql, "INSERT INTO #tableName()# ("); + ArrayPrepend(local.sql, "INSERT INTO #variables.wheels.class.adapter.$escapeReservedWords(tableName())# ("); ArrayPrepend(local.sql2, " VALUES ("); ArrayDeleteAt(local.sql, ArrayLen(local.sql)); ArrayDeleteAt(local.sql2, ArrayLen(local.sql2)); @@ -332,7 +332,7 @@ component { local.pks = primaryKey(0); ArrayAppend( local.sql, - "INSERT INTO #tableName()#" & variables.wheels.class.adapter.$defaultValues($primaryKey = local.pks) + "INSERT INTO #variables.wheels.class.adapter.$escapeReservedWords(tableName())#" & variables.wheels.class.adapter.$defaultValues($primaryKey = local.pks) ); } diff --git a/core/src/wheels/model/sql.cfc b/core/src/wheels/model/sql.cfc index 2624688e6..2891cf539 100644 --- a/core/src/wheels/model/sql.cfc +++ b/core/src/wheels/model/sql.cfc @@ -11,12 +11,12 @@ component { adapterName = get("adapterName") ); if (Len(local.indexHint)) { - ArrayAppend(arguments.sql, "UPDATE #tableName()# #local.indexHint# SET #variables.wheels.class.softDeleteColumn# = "); + ArrayAppend(arguments.sql, "UPDATE #variables.wheels.class.adapter.$escapeReservedWords(tableName())# #local.indexHint# SET #variables.wheels.class.softDeleteColumn# = "); } else { - ArrayAppend(arguments.sql, "UPDATE #tableName()# SET #variables.wheels.class.softDeleteColumn# = "); + ArrayAppend(arguments.sql, "UPDATE #variables.wheels.class.adapter.$escapeReservedWords(tableName())# SET #variables.wheels.class.softDeleteColumn# = "); } } else { - ArrayAppend(arguments.sql, "UPDATE #tableName()# SET #variables.wheels.class.softDeleteColumn# = "); + ArrayAppend(arguments.sql, "UPDATE #variables.wheels.class.adapter.$escapeReservedWords(tableName())# SET #variables.wheels.class.softDeleteColumn# = "); } // Use cf_sql_varchar in SQLite for TEXT timestamps if(get("adapterName") eq "SQLiteModel") { @@ -28,9 +28,9 @@ component { ArrayAppend(arguments.sql, local.param); } else { if (structKeyExists(arguments, "useIndex") && !structIsEmpty(arguments.useIndex)) { - ArrayAppend(arguments.sql, "DELETE tbl FROM #tableName()# tbl"); + ArrayAppend(arguments.sql, "DELETE tbl FROM #variables.wheels.class.adapter.$escapeReservedWords(tableName())# tbl"); } else { - ArrayAppend(arguments.sql, "DELETE FROM #tableName()#"); + ArrayAppend(arguments.sql, "DELETE FROM #variables.wheels.class.adapter.$escapeReservedWords(tableName())#"); } } return arguments.sql; @@ -59,7 +59,7 @@ component { string adapterName = get("adapterName") ) { // start the from statement with the SQL keyword and the table name for the current model - local.rv = "FROM " & tableName(); + local.rv = "FROM " & variables.wheels.class.adapter.$escapeReservedWords(tableName()); // add the index hint local.indexHint = this.$indexHint( @@ -85,7 +85,7 @@ component { local.hasOuterJoins = false; local.hasThroughAssociation = false; local.iEnd = ArrayLen(local.associations); - + // Check if this is specifically a through association pattern local.originalInclude = Replace(arguments.include, " ", "", "all"); if (Find("(", local.originalInclude)) { @@ -95,7 +95,7 @@ component { local.hasThroughAssociation = true; } } - + for (local.i = 1; local.i <= local.iEnd; local.i++) { if (FindNoCase("INNER", local.associations[local.i].join)) { local.hasInnerJoins = true; @@ -104,7 +104,7 @@ component { local.hasOuterJoins = true; } } - + // Only apply nesting for through associations with mixed join types local.needsNesting = local.hasInnerJoins && local.hasOuterJoins && local.hasThroughAssociation; @@ -113,7 +113,7 @@ component { // group inner joins with parentheses and outer joins separately local.innerJoins = []; local.outerJoins = []; - + for (local.i = 1; local.i <= local.iEnd; local.i++) { local.indexHint = this.$indexHint( useIndex = arguments.useIndex, @@ -131,36 +131,36 @@ component { "one" ); } - + if (FindNoCase("INNER", local.join)) { ArrayAppend(local.innerJoins, local.join); } else { ArrayAppend(local.outerJoins, local.join); } } - + for (local.i = 1; local.i <= ArrayLen(local.outerJoins); local.i++) { local.outerJoin = local.outerJoins[local.i]; - + // If we have inner joins, we need to group them in the outer join if (ArrayLen(local.innerJoins) > 0) { // Find the table being joined in the outer join local.joinTableMatch = ReFindNoCase("LEFT OUTER JOIN ([^\s]+)", local.outerJoin, 1, true); if (ArrayLen(local.joinTableMatch.pos) >= 2 && local.joinTableMatch.pos[2] > 0) { local.joinTable = Mid(local.outerJoin, local.joinTableMatch.pos[2], local.joinTableMatch.len[2]); - + // Build grouped inner joins: (subscriptions INNER JOIN magazines ON ...) local.groupedInner = "(" & local.joinTable; for (local.j = 1; local.j <= ArrayLen(local.innerJoins); local.j++) { local.groupedInner &= " " & local.innerJoins[local.j]; } local.groupedInner &= ")"; - + // Replace in the outer join local.outerJoin = Replace(local.outerJoin, "LEFT OUTER JOIN " & local.joinTable, "LEFT OUTER JOIN " & local.groupedInner); } } - + local.rv = ListAppend(local.rv, local.outerJoin, " "); } } else { @@ -258,7 +258,7 @@ component { local.toAdd = ""; local.classData = local.classes[local.j]; if (StructKeyExists(local.classData.propertyStruct, local.property)) { - local.toAdd = local.classData.tableName & "." & local.classData.properties[local.property].column; + local.toAdd = variables.wheels.class.adapter.$escapeReservedWords(local.classData.tableName) & "." & variables.wheels.class.adapter.$escapeReservedWords(local.classData.properties[local.property].column); } else if (StructKeyExists(local.classData.calculatedProperties, local.property)) { local.sql = local.classData.calculatedProperties[local.property].sql; local.toAdd = "(" & Replace(local.sql, ",", "[[comma]]", "all") & ")"; @@ -349,13 +349,13 @@ component { includeSoftDeletes = arguments.includeSoftDeletes, returnAs = arguments.returnAs ); - + // Look for " AS " followed by text containing multiple dots (namespaced aliases) if (Find(" AS ", local.rv)) { // Wrap column aliases that contain multiple dots with double quotes (ANSI SQL standard) local.rv = REReplace(local.rv, " AS ([^,\s]+\.[^,\s]*\.[^,\s]*)", " AS ""\1""", "all"); } - + local.rv = "SELECT " & local.rv; return local.rv; } @@ -494,9 +494,9 @@ component { local.toAppend &= "[[duplicate]]" & local.j; } if (StructKeyExists(local.classData.propertyStruct, local.iItem)) { - local.toAppend &= local.classData.tableName & "."; + local.toAppend &= variables.wheels.class.adapter.$escapeReservedWords(local.classData.tableName) & "."; if (StructKeyExists(local.classData.columnStruct, local.iItem)) { - local.toAppend &= local.iItem; + local.toAppend &= variables.wheels.class.adapter.$escapeReservedWords(local.iItem); } else { local.toAppend &= local.classData.properties[local.iItem].column; if (arguments.clause == "select") { @@ -647,7 +647,7 @@ component { } else if(arguments.include != "" && ListFind('MicrosoftSQLServer', local.migration.adapter.adapterName()) && structKeyExists(arguments, "sql")){ if(left(arguments.sql[1], 6) == 'UPDATE'){ - ArrayAppend(arguments.sql, "FROM #tablename()#"); + ArrayAppend(arguments.sql, "FROM #variables.wheels.class.adapter.$escapeReservedWords(tableName())#"); } } else if(arguments.include != "" && ListFind('H2,Oracle,SQLite', local.migration.adapter.adapterName()) && structKeyExists(arguments, "sql")){ @@ -747,7 +747,7 @@ component { local.param.column = "tbl." & local.classData.properties[local.column].column; } else { local.param.column = local.classData.tableName & "." & local.classData.properties[local.column].column; - } + } local.param.dataType = local.classData.properties[local.column].dataType; local.param.type = local.classData.properties[local.column].type; local.param.scale = local.classData.properties[local.column].scale; @@ -812,12 +812,12 @@ component { if (!arguments.includeSoftDeletes) { local.addToWhere = ""; if ($softDeletion() && arguments.softDelete) { - local.addToWhere = ListAppend(local.addToWhere, tableName() & "." & $softDeleteColumn() & " IS NULL"); + local.addToWhere = ListAppend(local.addToWhere, variables.wheels.class.adapter.$escapeReservedWords(tableName()) & "." & $softDeleteColumn() & " IS NULL"); } else if ($softDeletion()) { if (structKeyExists(arguments, "useIndex") && !structIsEmpty(arguments.useIndex)) { local.addToWhere = ListAppend(local.addToWhere, "tbl." & $softDeleteColumn() & " IS NULL"); } else { - local.addToWhere = ListAppend(local.addToWhere, tableName() & "." & $softDeleteColumn() & " IS NULL"); + local.addToWhere = ListAppend(local.addToWhere, variables.wheels.class.adapter.$escapeReservedWords(tableName()) & "." & $softDeleteColumn() & " IS NULL"); } } local.addToWhere = Replace(local.addToWhere, ",", " AND ", "all"); @@ -858,14 +858,14 @@ component { if (Left(local.processedValue, 1) == "(" && Right(local.processedValue, 1) == ")") { local.processedValue = Mid(local.processedValue, 2, Len(local.processedValue) - 2); } - + // BoxLang: Only apply quote cleanup if the value contains quotes if (Find("'", local.processedValue) > 0 || Find(Chr(34), local.processedValue) > 0) { - local.cleanedValue = local.processedValue; + local.cleanedValue = local.processedValue; local.cleanedValue = ReReplace(local.cleanedValue, "'([^']*)'", "\1", "ALL"); local.doubleQuote = Chr(34); local.cleanedValue = ReReplace(local.cleanedValue, "#local.doubleQuote#([^#local.doubleQuote#]*)#local.doubleQuote#", "\1", "ALL"); - + ArrayAppend(local.originalValues, local.cleanedValue); } else { ArrayAppend(local.originalValues, local.processedValue); @@ -951,44 +951,44 @@ component { public string function $expandThroughAssociations(required string include) { local.rv = ""; local.associations = variables.wheels.class.associations; - + // If the include string contains parentheses, it's already a complex nested include // Don't try to process it for through associations - return as-is if (Find("(", arguments.include)) { return arguments.include; } - + // Split the include string by commas to handle multiple simple includes local.includeList = ListToArray(arguments.include); - + for (local.i = 1; local.i <= ArrayLen(local.includeList); local.i++) { local.currentInclude = Trim(local.includeList[local.i]); - + // Check if this association has a 'through' defined - if (StructKeyExists(local.associations, local.currentInclude) + if (StructKeyExists(local.associations, local.currentInclude) && StructKeyExists(local.associations[local.currentInclude], "through") && Len(local.associations[local.currentInclude].through)) { - + local.throughPath = local.associations[local.currentInclude].through; - + if (ListLen(local.throughPath) == 1) { local.intermediateAssociationName = local.throughPath; - + // Get the current association info for the target we're trying to include local.currentAssociation = local.associations[local.currentInclude]; - + // Check if we have a direct association to the intermediate model if (StructKeyExists(local.associations, local.intermediateAssociationName)) { local.intermediateAssociation = local.associations[local.intermediateAssociationName]; - + // Get the intermediate model to find what it relates to local.intermediateModel = model(local.intermediateAssociation.modelName); local.intermediateAssociations = local.intermediateModel.$classData().associations; - + // Find the association that leads to our target model local.targetModelName = local.currentAssociation.modelName; local.targetAssociation = ""; - + for (local.assocName in local.intermediateAssociations) { local.assoc = local.intermediateAssociations[local.assocName]; if (local.assoc.modelName == local.targetModelName) { @@ -996,7 +996,7 @@ component { break; } } - + if (Len(local.targetAssociation)) { local.expandedInclude = local.intermediateAssociationName & "(" & local.targetAssociation & ")"; local.rv = ListAppend(local.rv, local.expandedInclude); @@ -1011,7 +1011,7 @@ component { } else { local.firstAssociation = ListFirst(local.throughPath); local.targetAssociation = ListLast(local.throughPath); - + local.expandedInclude = local.firstAssociation & "(" & local.targetAssociation & ")"; local.rv = ListAppend(local.rv, local.expandedInclude); } @@ -1020,7 +1020,7 @@ component { local.rv = ListAppend(local.rv, local.currentInclude); } } - + return local.rv; } @@ -1043,7 +1043,7 @@ component { local.include = Replace(local.include, " ", "", "all") & ","; // store all tables used in the query so we can alias them when needed - local.tables = tableName(); + local.tables = variables.wheels.class.adapter.$escapeReservedWords(tableName()); local.pos = 1; @@ -1110,7 +1110,7 @@ component { // create the join string if it hasn't already been done if (!StructKeyExists(local.classAssociations[local.name], "join")) { local.joinType = UCase(ReplaceNoCase(local.classAssociations[local.name].joinType, "outer", "left outer", "one")); - local.join = local.joinType & " JOIN " & local.classAssociations[local.name].tableName; + local.join = local.joinType & " JOIN " & variables.wheels.class.adapter.$escapeReservedWords(local.classAssociations[local.name].tableName); // alias the table as the association name when joining to itself if (ListFindNoCase(local.tables, local.classAssociations[local.name].tableName)) { local.join = variables.wheels.class.adapter.$tableAlias( @@ -1157,7 +1157,7 @@ component { if (!arguments.includeSoftDeletes && local.associatedClass.$softDeletion()) { local.toAppend = ListAppend( local.toAppend, - "#local.associatedClass.tableName()#.#local.associatedClass.$softDeleteColumn()# IS NULL" + "#variables.wheels.class.adapter.$escapeReservedWords(local.associatedClass.tableName())#.#local.associatedClass.$softDeleteColumn()# IS NULL" ); } } diff --git a/core/src/wheels/model/update.cfc b/core/src/wheels/model/update.cfc index d4961c9d4..5200b7fea 100644 --- a/core/src/wheels/model/update.cfc +++ b/core/src/wheels/model/update.cfc @@ -79,7 +79,7 @@ component { modelName = variables.wheels.class.modelName, adapterName = get("adapterName") ); - + if (ListFind('MySQL', local.migration.adapter.adapterName())){ local.list = ""; local.associations = []; @@ -96,7 +96,7 @@ component { } else { ArrayAppend(arguments.sql, "UPDATE #tableName()# #local.list# SET"); } - + } else if (ListFind('MicrosoftSQLServer', local.migration.adapter.adapterName())){ if (Len(local.indexHint)) { @@ -111,7 +111,7 @@ component { local.pos = 0; for (local.key in arguments.properties) { local.pos++; - ArrayAppend(arguments.sql, "#variables.wheels.class.properties[local.key].column# = "); + ArrayAppend(arguments.sql, "#variables.wheels.class.adapter.$escapeReservedWords(variables.wheels.class.properties[local.key].column)# = "); local.param = { value = arguments.properties[local.key], type = variables.wheels.class.properties[local.key].type, @@ -314,12 +314,12 @@ component { $timestampProperty(property = variables.wheels.class.timeStampOnUpdateProperty); } local.sql = []; - ArrayAppend(local.sql, "UPDATE #tableName()# SET "); + ArrayAppend(local.sql, "UPDATE #variables.wheels.class.adapter.$escapeReservedWords(tableName())# SET "); // Include all changed non-key values in the update. for (local.key in variables.wheels.class.properties) { if (StructKeyExists(this, local.key) && !ListFindNoCase(primaryKeys(), local.key) && hasChanged(local.key)) { - ArrayAppend(local.sql, "#variables.wheels.class.properties[local.key].column# = "); + ArrayAppend(local.sql, "#variables.wheels.class.adapter.$escapeReservedWords(variables.wheels.class.properties[local.key].column)# = "); local.param = $buildQueryParamValues(local.key); ArrayAppend(local.sql, local.param); ArrayAppend(local.sql, ","); diff --git a/core/src/wheels/tests_testbox/specs/model/crudSpec.cfc b/core/src/wheels/tests_testbox/specs/model/crudSpec.cfc index 001bbc112..518f0889b 100644 --- a/core/src/wheels/tests_testbox/specs/model/crudSpec.cfc +++ b/core/src/wheels/tests_testbox/specs/model/crudSpec.cfc @@ -177,10 +177,10 @@ component extends="wheels.Testbox" { result = author.isPersisted() expect(result).toBeFalse() - + author.save(transaction = "none") result = author.isPersisted() - + expect(result).toBeTrue() transaction action="rollback"; @@ -204,7 +204,7 @@ component extends="wheels.Testbox" { result = author.hasChanged() expect(result).toBeTrue() - + author.lastName = "Djurner" result = author.hasChanged() @@ -234,7 +234,7 @@ component extends="wheels.Testbox" { result = author.hasChanged() expect(result).toBeFalse() - + transaction action="rollback"; } }) @@ -255,7 +255,7 @@ component extends="wheels.Testbox" { author = g.model("author").findOne(where = "lastName = 'Djurner'") author.lastName = "Petruzzi" result = author.changedFrom(property = "lastName") - + expect(result).toBe("Djurner") }) @@ -263,7 +263,7 @@ component extends="wheels.Testbox" { author = g.model("author").findOne(where = "lastName = 'Djurner'") author.lastName = "Petruzzi" result = author.lastNameChangedFrom(property = "lastName") - + expect(result).toBe("Djurner") }) @@ -274,7 +274,7 @@ component extends="wheels.Testbox" { } else { user.birthday = "11/01/1975 12:00 AM" } - + e = user.hasChanged("birthday") expect(e).toBeFalse() @@ -364,7 +364,7 @@ component extends="wheels.Testbox" { expect(results.shop).toBeInstanceOf("shop") expect(results.shop).toHaveKey(results.shop.primaryKey()) expect(results.shop[results.shop.primaryKey()]).toBe(99) - + transaction action="rollback"; } }) @@ -778,27 +778,27 @@ component extends="wheels.Testbox" { it("function findByKey returns correct type when no records", () => { q = user.findByKey("0") - + expect(q).toBeBoolean() expect(q).toBeFalse() q = user.findByKey(key = "0", returnas = "query") - + expect(q).toBeQuery() expect(q.recordcount).toBe(0) q = user.findByKey(key = "0", returnas = "object") - + expect(q).toBeBoolean() expect(q).toBeFalse() q = user.findByKey(key = "0", returnas = "struct") - + expect(q).toBeStruct() expect(q).toBeEmpty() q = user.findByKey(key = "0", returnas = "array") - + expect(q).toBeArray() expect(q).toBeEmpty() @@ -1023,7 +1023,7 @@ component extends="wheels.Testbox" { authorAfter = g.model("author").findByKey(1) transaction action="rollback"; } - + expect(authorAfter.lastName).toBe("D") application.wheels.cacheQueriesDuringRequest = local.oldCacheQueriesDuringRequest @@ -1047,9 +1047,9 @@ component extends="wheels.Testbox" { result = g.model("author").findOne(where = "lastName IS NULL") expect(result).toBeFalse() - + result = g.model("author").findOne(where = "lastName IS NOT NULL") - + expect(result).toBeInstanceOf("author") }) @@ -1243,6 +1243,19 @@ component extends="wheels.Testbox" { expect(actual).toBe("FROM c_o_r_e_authors USE INDEX(idx_authors_123) LEFT OUTER JOIN c_o_r_e_posts USE INDEX(idx_posts_123) ON c_o_r_e_authors.id = c_o_r_e_posts.authorid AND c_o_r_e_posts.deletedat IS NULL") }) + + it("escapes MySQL reserved word 'groups' in from clause", () => { + g.model("author").table("groups") + // it doesnt look like we can override the adapterName for $escapeReservedWords + actual = g.model("author").$fromClause(include = "") + g.model("author").table("c_o_r_e_authors") + + if (application.wheels.adapterName eq 'MySQLModel') { + expect(actual).toBe("FROM `groups`") + } else { + expect(actual).toBe("FROM groups") + } + }) }) describe("Tests that group", () => { @@ -1832,4 +1845,4 @@ component extends="wheels.Testbox" { }) }) } -} \ No newline at end of file +}