$val) { if (is_object($val) and !$val instanceof Stringable) $val = null; if (is_bool($val)) $val = (int) $val; if (is_float($val)) $val = (string) $val; if ( $key == "updated_at" and is_datetime_zero($val) ) $val = SQL_DATETIME_ZERO; $param = ":".str_replace(array("(", ")", "."), "_", $key); $params[$param] = $val; } return "INSERT INTO \"__$table\"\n". self::build_insert_header($sql, $data)."\n". "VALUES\n". "(".implode(", ", array_keys($params)).")\n"; } /** * Function: build_update * Creates a full update query. * * Parameters: * $sql - The SQL instance calling this method. * $table - Table to update. * $conds - Conditions to update rows by. * $data - Data to update. * &$params - An associative array of parameters used in the query. * * Returns: * An @UPDATE@ query string. */ public static function build_update( $sql, $table, $conds, $data, &$params = array() ): string { $query = "UPDATE \"__$table\"\n". "SET ".self::build_update_values($sql, $data, $params)."\n"; if ($conds) $query.= "WHERE ".self::build_where($sql, $conds, $table, $params); return $query; } /** * Function: build_delete * Creates a full delete query. * * Parameters: * $sql - The SQL instance calling this method. * $table - Table to delete from. * $conds - Conditions to delete by. * &$params - An associative array of parameters used in the query. * * Returns: * A @DELETE@ query string. */ public static function build_delete( $sql, $table, $conds, &$params = array() ): string { $query = "DELETE FROM \"__$table\"\n"; if ($conds) $query.= "WHERE ".self::build_where($sql, $conds, $table, $params); return $query; } /** * Function: build_drop * Creates a full drop table query. * * Parameters: * $sql - The SQL instance calling this method. * $table - Table to drop. * * Returns: * A @DROP TABLE@ query string. */ public static function build_drop( $sql, $table ): string { return "DROP TABLE IF EXISTS \"__$table\""; } /** * Function: build_create * Creates a full create table query. * * Parameters: * $sql - The SQL instance calling this method. * $table - Table to create. * $cols - An array of column declarations. * * Returns: * A @CREATE TABLE@ query string. */ public static function build_create( $sql, $table, $cols ): string { $query = "CREATE TABLE IF NOT EXISTS \"__$table\" (\n ". implode(",\n ", $cols)."\n)"; switch ($sql->adapter) { case "sqlite": $query = str_ireplace( "AUTO_INCREMENT", "AUTOINCREMENT", $query ); break; case "mysql": $query = str_ireplace( "AUTOINCREMENT", "AUTO_INCREMENT", $query ); $query.= " DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci"; break; case "pgsql": $query = str_ireplace( array( "LONGTEXT", "DATETIME" ), array( "TEXT", "TIMESTAMP" ), $query ); $query = preg_replace( "/INTEGER( (PRIMARY )?KEY)? AUTO_?INCREMENT/i", "SERIAL$1", $query ); break; } return $query; } /** * Function: build_update_values * Creates an update data part. * * Parameters: * $sql - The SQL instance calling this method. * $data - Data to update. * &$params - An associative array of parameters used in the query. */ public static function build_update_values( $sql, $data, &$params = array() ): string { $set = self::build_conditions($sql, $data, $params, null, true); return implode(",\n ", $set); } /** * Function: build_insert_header * Creates an insert header. * * Parameters: * $sql - The SQL instance calling this method. * $data - Data to insert. */ public static function build_insert_header( $sql, $data ): string { $set = array(); foreach (array_keys($data) as $field) array_push($set, self::safecol($sql, $field)); return "(".implode(", ", $set).")"; } /** * Function: build_limits * Creates the LIMIT part of a query. * * Parameters: * $sql - The SQL instance calling this method. * $offset - Offset of the result. * $limit - Limit of the result. */ public static function build_limits( $sql, $offset, $limit ): string { if ($limit === null) return ""; if ($offset !== null) return "LIMIT ".$offset.", ".$limit; return "LIMIT ".$limit; } /** * Function: build_from * Creates a FROM header for select queries. * * Parameters: * $sql - The SQL instance calling this method. * $tables - Tables to select from. */ public static function build_from( $sql, $tables ): string { if (!is_array($tables)) $tables = array($tables); foreach ($tables as &$table) { if (substr($table, 0, 2) != "__") $table = "__".$table; $table = "\"".$table."\""; } return implode(",\n ", $tables); } /** * Function: build_count * Creates a SELECT COUNT(1) query. * * Parameters: * $sql - The SQL instance calling this method. * $tables - Tables to tablefy with. * $conds - Conditions to select by. * &$params - An associative array of parameters used in the query. */ public static function build_count( $sql, $tables, $conds, &$params = array() ): string { $query = "SELECT COUNT(1) AS count\n". "FROM ".self::build_from($sql, $tables)."\n"; if ($conds) $query.= "WHERE ".self::build_where($sql, $conds, $tables, $params); return $query; } /** * Function: build_select_header * Creates a SELECT fields header. * * Parameters: * $sql - The SQL instance calling this method. * $fields - Columns to select. * $tables - Tables to tablefy with. */ public static function build_select_header( $sql, $fields, $tables = null ): string { if (!is_array($fields)) $fields = array($fields); $tables = (array) $tables; foreach ($fields as &$field) { self::tablefy($sql, $field, $tables); $field = self::safecol($sql, $field); } return implode(",\n ", $fields); } /** * Function: build_where * Creates a WHERE query. * * Parameters: * $sql - The SQL instance calling this method. * $conds - Conditions to select by. * $tables - Tables to tablefy with. * &$params - An associative array of parameters used in the query. */ public static function build_where( $sql, $conds, $tables = null, &$params = array() ): string { $conds = (array) $conds; $tables = (array) $tables; $conditions = self::build_conditions($sql, $conds, $params, $tables); return empty($conditions) ? "" : "(".implode(")\n AND (", array_filter($conditions)).")" ; } /** * Function: build_group * Creates a GROUP BY argument. * * Parameters: * $sql - The SQL instance calling this method. * $order - Columns to group by. * $tables - Tables to tablefy with. */ public static function build_group( $sql, $by, $tables = null ): string { $by = (array) $by; $tables = (array) $tables; foreach ($by as &$column) { self::tablefy($sql, $column, $tables); $column = self::safecol($sql, $column); } return implode( ",\n ", array_unique(array_filter($by)) ); } /** * Function: build_order * Creates an ORDER BY argument. * * Parameters: * $sql - The SQL instance calling this method. * $order - Columns to order by. * $tables - Tables to tablefy with. */ public static function build_order( $sql, $order, $tables = null ): string { $tables = (array) $tables; if (!is_array($order)) { $parts = array_map("trim", explode(",", $order)); $order = array_diff(array_unique($parts), array("")); } foreach ($order as &$by) { self::tablefy($sql, $by, $tables); $by = self::safecol($sql, $by); } return implode(",\n ", $order); } /** * Function: build_list * Creates a list of values. * * Parameters: * $sql - The SQL instance calling this method. * $vals - An array of values. * &$params - An associative array of parameters used in the query. */ public static function build_list( $sql, $vals, $params = array() ): string { $return = array(); foreach ($vals as $val) { if (is_object($val) and !$val instanceof Stringable) $val = null; if (is_bool($val)) $val = (int) $val; if (is_float($val)) $val = (string) $val; $return[] = isset($params[$val]) ? $val : SQL::current()->escape($val, !is_int($val)) ; } return "(".join(", ", $return).")"; } /** * Function: safecol * Encloses a column name in quotes if it is a subset of SQL keywords. * * Parameters: * $sql - The SQL instance calling this method. * $name - Name of the column. */ public static function safecol( $sql, $name ): string|array|null { $keywords = array( "between", "case", "check", "collate", "constraint", "create", "default", "delete", "distinct", "drop", "except", "from", "groups?", "having", "insert", "intersect", "into", "join", "left", "like", "limit", "null", "offset", "order", "primary", "references", "select", "set", "table", "transaction", "union", "unique", "update", "using", "values", "when", "where", "with" ); $pattern = implode("|", $keywords); return preg_replace( "/(([^a-zA-Z0-9_]|^)($pattern)([^a-zA-Z0-9_]|$))/i", '\\2"\\3"\\4', $name ); } /** * Function: build_conditions * Builds an associative array of SQL values into parameterized query strings. * * Parameters: * $sql - The SQL instance calling this method. * $conds - Conditions. * &$params - Parameters array to fill. * $tables - If specified, conditions will be tablefied with these tables. * $insert - Is this an insert/update query? */ public static function build_conditions( $sql, $conds, &$params, $tables = null, $insert = false ): array { $conditions = array(); # PostgreSQL: cast to text to enable LIKE operator. $text = ($sql->adapter == "pgsql") ? "::text" : "" ; foreach ($conds as $key => $val) { if (is_int($key)) { # Full expression. $cond = $val; } else { # Key => Val expression. if (is_string($val) and strlen($val) and strpos($val, ":") === 0) { $cond = self::safecol($sql, $key)." = ".$val; } else { if (is_object($val) and !$val instanceof Stringable) $val = null; if (is_bool($val)) $val = (int) $val; if (is_float($val)) $val = (string) $val; $uck = strtoupper($key); if (substr($uck, -4) == " NOT") { # Negation. $key = self::safecol($sql, substr($key, 0, -4)); $param = str_replace( array("(", ")", "."), "_", $key ); if (is_array($val)) { $cond = $key." NOT IN ". self::build_list($sql, $val, $params); } elseif ($val === null) { $cond = $key." IS NOT NULL"; } else { $cond = $key." != :".$param; $params[":".$param] = $val; } } elseif (substr($uck, -9) == " LIKE ALL" and is_array($val)) { # multiple LIKE (AND). $key = self::safecol($sql, substr($key, 0, -9)); $likes = array(); foreach ($val as $index => $match) { $param = str_replace( array("(", ")", "."), "_", $key )."_".$index; $likes[] = $key.$text." LIKE :".$param; $params[":".$param] = $match; } $cond = "(".implode(" AND ", $likes).")"; } elseif (substr($uck, -9) == " NOT LIKE" and is_array($val)) { # multiple NOT LIKE. $key = self::safecol($sql, substr($key, 0, -9)); $likes = array(); foreach ($val as $index => $match) { $param = str_replace( array("(", ")", "."), "_", $key )."_".$index; $likes[] = $key.$text." NOT LIKE :".$param; $params[":".$param] = $match; } $cond = "(".implode(" AND ", $likes).")"; } elseif (substr($uck, -5) == " LIKE" and is_array($val)) { # multiple LIKE (OR). $key = self::safecol($sql, substr($key, 0, -5)); $likes = array(); foreach ($val as $index => $match) { $param = str_replace( array("(", ")", "."), "_", $key )."_".$index; $likes[] = $key.$text." LIKE :".$param; $params[":".$param] = $match; } $cond = "(".implode(" OR ", $likes).")"; } elseif (substr($uck, -9) == " NOT LIKE") { # NOT LIKE. $key = self::safecol($sql, substr($key, 0, -9)); $param = str_replace( array("(", ")", "."), "_", $key ); $cond = $key.$text." NOT LIKE :".$param; $params[":".$param] = $val; } elseif (substr($uck, -5) == " LIKE") { # LIKE. $key = self::safecol($sql, substr($key, 0, -5)); $param = str_replace( array("(", ")", "."), "_", $key ); $cond = $key.$text." LIKE :".$param; $params[":".$param] = $val; } elseif (substr_count($key, " ")) { # Custom operation, e.g. array("foo >" => $bar). list($param,) = explode(" ", $key); $param = str_replace( array("(", ")", "."), "_", $param ); $cond = self::safecol($sql, $key)." :".$param; $params[":".$param] = $val; } else { # Equation. if (is_array($val)) { $cond = self::safecol($sql, $key)." IN ". self::build_list($sql, $val, $params); } elseif ($val === null and $insert) { $cond = self::safecol($sql, $key)." = ''"; } elseif ($val === null) { $cond = self::safecol($sql, $key)." IS NULL"; } else { $param = str_replace( array("(", ")", "."), "_", $key ); $cond = self::safecol($sql, $key)." = :".$param; if ($insert) { if ( $key == "updated_at" and is_datetime_zero($val) ) $val = SQL_DATETIME_ZERO; } $params[":".$param] = $val; } } } } if ($tables) self::tablefy($sql, $cond, $tables); $conditions[] = $cond; } return $conditions; } /** * Function: tablefy * Prepends table names and prefixes to a field if it doesn't already have them. * * Parameters: * $sql - The SQL instance calling this method. * &$field - The field to "tablefy". * $tables - An array of tables. The first one will be used for prepending. */ public static function tablefy( $sql, &$field, $tables ): void { if ( !preg_match_all( "/(\(|[\s]+|^)(?!__)([a-z0-9_\.\*]+)(\)|[\s]+|$)/", $field, $matches ) ) return; foreach ($matches[0] as $index => $full) { $before = $matches[1][$index]; $name = $matches[2][$index]; $after = $matches[3][$index]; if (is_numeric($name)) continue; # Does it not already have a table specified? if (!substr_count($full, ".")) { # Don't replace things that are already either prefixed or parameterized. $field = preg_replace( "/([^\.:'\"_]|^)".preg_quote($full, "/")."/", "\\1".$before."\"__".$tables[0]."\".".$name.$after, $field, 1 ); } else { $field = preg_replace( "/([^\.:'\"_]|^)".preg_quote($full, "/")."/", "\\1".$before."\"__".str_replace(".", "\".", $name).$after, $field, 1 ); } } $field = preg_replace("/AS ([^ ]+)\./i", "AS ", $field); } }