diff --git a/LIMIT_EXAMPLE.md b/LIMIT_EXAMPLE.md new file mode 100644 index 0000000..de5ba6e --- /dev/null +++ b/LIMIT_EXAMPLE.md @@ -0,0 +1,92 @@ +# Implementación del Operador LIMIT + +## Descripción + +Se ha implementado exitosamente el operador LIMIT para la clase QuerySelect, permitiendo limitar el número de filas devueltas en una consulta SELECT. + +## Características implementadas + +1. **Clase Limit**: Se creó una nueva clase `Limit` en el paquete `operator` que maneja los parámetros de límite y offset. + +2. **Soporte en QuerySelect**: Se agregó soporte completo para LIMIT en la clase QuerySelect y su QueryBuilder. + +3. **Métodos disponibles**: + - `limit(count: Int)`: Limita el número de filas a devolver + - `limit(count: Int, offset: Int?)`: Limita el número de filas con un offset opcional + - `limit(limitOperator: Limit)`: Acepta un objeto Limit directamente + +## Ejemplos de uso + +### Uso básico con Builder +```kotlin +val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .limit(10) + .build() + +// Genera: SELECT * FROM users WHERE status = 'active' LIMIT 10 +``` + +### Uso con offset +```kotlin +val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .limit(10, 5) + .build() + +// Genera: SELECT * FROM users WHERE status = 'active' LIMIT 10 OFFSET 5 +``` + +### Uso con objeto Limit +```kotlin +val limitOperator = Limit(count = 10, offset = 5) +val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .limit(limitOperator) + .build() + +// Genera: SELECT * FROM users WHERE status = 'active' LIMIT 10 OFFSET 5 +``` + +### Encadenamiento de métodos +```kotlin +val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .and("age", SQLOperator.GreaterThan("age", 18)) + .orderBy(OrderBy.Desc("created_at")) + .limit(20) + .build() + +// Genera: SELECT * FROM users WHERE status = 'active' AND age > 18 +// ORDER BY created_at DESC +// LIMIT 20 +``` + +### Modificación de límite en consulta existente +```kotlin +val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .build() + +// Agregar límite después de crear la consulta +query.limit(15, 10) + +// Genera: SELECT * FROM users WHERE status = 'active' LIMIT 15 OFFSET 10 +``` + +## Validaciones implementadas + +La clase Limit incluye validaciones para garantizar que: +- El count debe ser no negativo +- El offset (si se proporciona) debe ser no negativo + +## Orden de ejecución SQL + +La implementación respeta el orden estándar de SQL: +1. SELECT campos +2. FROM tabla +3. WHERE condiciones +4. ORDER BY (si existe) +5. LIMIT (si existe) + +Esta implementación permite crear consultas SQL complejas con paginación de manera fluida y manteniendo la consistencia con el resto del framework. diff --git a/README.md b/README.md index ee58d15..9dfa137 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,6 @@ Diseñada para integrarse perfectamente con la base de datos de Android y Room a - [🔗 Integración con Room](#-integración-con-room) - [📝 Ejemplos Avanzados](#-ejemplos-avanzados) - [🤝 Contribuir](#-contribuir) -- [📄 Licencia](#-licencia) --- @@ -92,6 +91,10 @@ val selectQuery = QuerySelect.builder("users") .and("status", SQLOperator.Equals("status", "active")) .setFields("name", "email") .build() +// Agrega un límite de 10 resultados + selectQuery.limit(10) +// Agrega un offset + selectQuery.limit(10, 5) val sqlString = selectQuery.asSql() ``` diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/query/src/main/java/com/blipblipcode/query/InnerJoint.kt b/query/src/main/java/com/blipblipcode/query/InnerJoint.kt index e88ca0c..2964e96 100644 --- a/query/src/main/java/com/blipblipcode/query/InnerJoint.kt +++ b/query/src/main/java/com/blipblipcode/query/InnerJoint.kt @@ -1,6 +1,7 @@ package com.blipblipcode.query import com.blipblipcode.query.operator.OrderBy +import com.blipblipcode.query.operator.SQLOperator /** * Represents a SQL INNER JOIN query construct. @@ -17,6 +18,18 @@ class InnerJoint private constructor( private var orderBy: OrderBy? = null + override fun getSqlOperators(): List> { + return queries.flatMap { it.getSqlOperators() } + } + + override fun getTableName(): String { + return queries.joinToString(", ") { it.getTableName() } + } + + override fun getSqlOperation(key: String): SQLOperator<*>? { + return queries.flatMap { it.getSqlOperators() }.firstOrNull { it.column.equals(key, ignoreCase = true) } + } + /** * Generates the SQL string for the INNER JOIN statement. * @return The complete INNER JOIN SQL query as a string. diff --git a/query/src/main/java/com/blipblipcode/query/QueryDelete.kt b/query/src/main/java/com/blipblipcode/query/QueryDelete.kt index 5509b70..75cb19c 100644 --- a/query/src/main/java/com/blipblipcode/query/QueryDelete.kt +++ b/query/src/main/java/com/blipblipcode/query/QueryDelete.kt @@ -63,6 +63,20 @@ class QueryDelete private constructor( return this } + override fun getSqlOperators(): List> { + return operations.values.map { + it.operator + } + } + + override fun getTableName(): String { + return table + } + + override fun getSqlOperation(key: String): SQLOperator<*>? { + return operations[key]?.operator + } + /** * Generates the SQL string for the DELETE statement. * @return The complete DELETE SQL query as a string. diff --git a/query/src/main/java/com/blipblipcode/query/QueryInsert.kt b/query/src/main/java/com/blipblipcode/query/QueryInsert.kt index 8d5737e..a3aefdb 100644 --- a/query/src/main/java/com/blipblipcode/query/QueryInsert.kt +++ b/query/src/main/java/com/blipblipcode/query/QueryInsert.kt @@ -1,6 +1,7 @@ package com.blipblipcode.query import com.blipblipcode.query.operator.Field +import com.blipblipcode.query.operator.SQLOperator /** * Represents a SQL INSERT statement. @@ -58,6 +59,18 @@ class QueryInsert private constructor( return this } + override fun getSqlOperators(): List> { + return emptyList() + } + + override fun getTableName(): String { + return table + } + + override fun getSqlOperation(key: String): SQLOperator<*>? { + return null + } + /** * Generates the SQL string for the INSERT statement. * @return The complete INSERT SQL query as a string. diff --git a/query/src/main/java/com/blipblipcode/query/QuerySelect.kt b/query/src/main/java/com/blipblipcode/query/QuerySelect.kt index 9a81330..29423d6 100644 --- a/query/src/main/java/com/blipblipcode/query/QuerySelect.kt +++ b/query/src/main/java/com/blipblipcode/query/QuerySelect.kt @@ -1,5 +1,6 @@ package com.blipblipcode.query +import com.blipblipcode.query.operator.Limit import com.blipblipcode.query.operator.LogicalOperation import com.blipblipcode.query.operator.LogicalType import com.blipblipcode.query.operator.OrderBy @@ -22,6 +23,7 @@ class QuerySelect private constructor( private val fields: List ) : Queryable { private var orderBy: OrderBy? = null + private var limit: Limit? = null companion object { /** @@ -79,7 +81,24 @@ class QuerySelect private constructor( where = where, operations = operations, fields = fieldList - ) + ).apply { + this.orderBy = this@QuerySelect.orderBy + this.limit = this@QuerySelect.limit + } + } + + override fun getSqlOperators(): List> { + return operations.values.map { + it.operator + } + } + + override fun getTableName(): String { + return table + } + + override fun getSqlOperation(key: String): SQLOperator<*>? { + return operations.get(key)?.operator } /** @@ -92,9 +111,13 @@ class QuerySelect private constructor( return buildString { append("SELECT $fieldStr FROM $table WHERE ${where.toSQLString()} $operationsStr".trim()) if (orderBy != null) { - appendLine() + append(" ") append(orderBy!!.asString()) } + if (limit != null) { + append(" ") + append(limit!!.asString()) + } } } @@ -102,7 +125,7 @@ class QuerySelect private constructor( * Appends an ORDER BY clause to the entire UNION query. * Note that in most SQL dialects, an ORDER BY clause can only be applied to the final result of a UNION, not to individual `SELECT` statements within it. * - * @param columns A vararg of `OrderExpression` objects specifying the columns and direction for sorting. + * @param operator A vararg of `[OrderBy]` objects specifying the columns and direction for sorting. * @return A new `QuerySelect` instance representing the UNION query with the added ORDER BY clause. */ fun orderBy(operator: OrderBy): Queryable { @@ -110,6 +133,29 @@ class QuerySelect private constructor( return this } + /** + * Adds a LIMIT clause to the query to limit the number of rows returned. + * + * @param count The maximum number of rows to return. + * @param offset The number of rows to skip before returning results (optional). + * @return The current `QuerySelect` instance for chaining. + */ + fun limit(count: Int, offset: Int? = null): QuerySelect { + limit = Limit(count, offset) + return this + } + + /** + * Adds a LIMIT clause to the query using a Limit object. + * + * @param limitOperator The Limit object specifying the limit parameters. + * @return The current `QuerySelect` instance for chaining. + */ + fun limit(limitOperator: Limit): QuerySelect { + limit = limitOperator + return this + } + /** * A builder for creating `QuerySelect` instances. @@ -120,7 +166,9 @@ class QuerySelect private constructor( private val operations: LinkedHashMap ) { private var where: SQLOperator<*>? = null - private var fields: List = listOf("*" ) + private var fields: List = listOf("*") + private var orderBy: OrderBy? = null + private var limit: Limit? = null /** * Adds an AND condition to the WHERE clause. @@ -234,6 +282,37 @@ class QuerySelect private constructor( return this } + /** + * Sets the ORDER BY clause for the query. + * @param orderBy The OrderBy object specifying the column and direction for sorting. + * @return The `QueryBuilder` instance for chaining. + */ + fun orderBy(orderBy: OrderBy): QueryBuilder { + this.orderBy = orderBy + return this + } + + /** + * Sets a LIMIT clause for the query to limit the number of rows returned. + * @param count The maximum number of rows to return. + * @param offset The number of rows to skip before returning results (optional). + * @return The `QueryBuilder` instance for chaining. + */ + fun limit(count: Int, offset: Int? = null): QueryBuilder { + this.limit = Limit(count, offset) + return this + } + + /** + * Sets a LIMIT clause for the query using a Limit object. + * @param limit The Limit object specifying the limit parameters. + * @return The `QueryBuilder` instance for chaining. + */ + fun limit(limit: Limit): QueryBuilder { + this.limit = limit + return this + } + /** * Builds the `QuerySelect` instance. * @return A new `QuerySelect` object. @@ -246,7 +325,10 @@ class QuerySelect private constructor( table = table, operations = LinkedHashMap(operations), fields = fields - ) + ).apply { + this@apply.orderBy = this@QueryBuilder.orderBy + this@apply.limit = this@QueryBuilder.limit + } } } } diff --git a/query/src/main/java/com/blipblipcode/query/QueryUpdate.kt b/query/src/main/java/com/blipblipcode/query/QueryUpdate.kt index 23970c4..a5b85d2 100644 --- a/query/src/main/java/com/blipblipcode/query/QueryUpdate.kt +++ b/query/src/main/java/com/blipblipcode/query/QueryUpdate.kt @@ -74,6 +74,18 @@ class QueryUpdate private constructor( return this } + override fun getSqlOperators(): List> { + return emptyList() + } + + override fun getTableName(): String { + return table + } + + override fun getSqlOperation(key: String): SQLOperator<*>? { + return null + } + /** * Generates the SQL string for the UPDATE statement. * @return The complete UPDATE SQL query as a string. diff --git a/query/src/main/java/com/blipblipcode/query/Queryable.kt b/query/src/main/java/com/blipblipcode/query/Queryable.kt index 313cfa7..101662b 100644 --- a/query/src/main/java/com/blipblipcode/query/Queryable.kt +++ b/query/src/main/java/com/blipblipcode/query/Queryable.kt @@ -1,9 +1,32 @@ package com.blipblipcode.query +import com.blipblipcode.query.operator.SQLOperator + /** * Represents an object that can be converted to a SQL query string. */ interface Queryable { + /** + * Returns a list of all SQL operators used in the query. + * @return A list of [SQLOperator] instances. + */ + fun getSqlOperators(): List> + /** + * Returns the table name associated with the queryable object. + * This is typically derived from the class name of the implementing object. + * @return The table name as a string. + */ + fun getTableName(): String + + + /** + * Returns the corresponding SQL operation string for a given [SQLOperator]. + * + * @param key The [SQLOperator] enum constant. + * @return The [SQLOperator] representation of the SQL operation or NULL if not exist. + */ + fun getSqlOperation(key: String): SQLOperator<*>? + /** * Returns the SQL query string representation of the object. * @return The SQL query string. diff --git a/query/src/main/java/com/blipblipcode/query/UnionQuery.kt b/query/src/main/java/com/blipblipcode/query/UnionQuery.kt index 24fa5de..477ce39 100644 --- a/query/src/main/java/com/blipblipcode/query/UnionQuery.kt +++ b/query/src/main/java/com/blipblipcode/query/UnionQuery.kt @@ -1,6 +1,7 @@ package com.blipblipcode.query import com.blipblipcode.query.operator.OrderBy +import com.blipblipcode.query.operator.SQLOperator /** * Represents a SQL UNION query construct. @@ -15,6 +16,18 @@ class UnionQuery private constructor( val useUnionAll: Boolean = false ) : Queryable { private var orderBy: OrderBy? = null + override fun getSqlOperators(): List> { + return queries.flatMap { it.getSqlOperators() } + } + + override fun getTableName(): String { + return queries.joinToString(", ") { it.getTableName() } + } + + override fun getSqlOperation(key: String): SQLOperator<*>? { + return queries.flatMap { it.getSqlOperators() }.firstOrNull { it.column.equals(key, ignoreCase = true) } + } + /** * Generates the SQL string for the UNION statement. * @return The complete UNION SQL query as a string. diff --git a/query/src/main/java/com/blipblipcode/query/examples/LimitExamples.kt b/query/src/main/java/com/blipblipcode/query/examples/LimitExamples.kt new file mode 100644 index 0000000..472daf9 --- /dev/null +++ b/query/src/main/java/com/blipblipcode/query/examples/LimitExamples.kt @@ -0,0 +1,104 @@ +package com.blipblipcode.query.examples + +import com.blipblipcode.query.QuerySelect +import com.blipblipcode.query.operator.Limit +import com.blipblipcode.query.operator.OrderBy +import com.blipblipcode.query.operator.SQLOperator + +/** + * Ejemplos de uso del operador LIMIT en QuerySelect + */ +class LimitExamples { + + /** + * Ejemplo básico: Obtener los primeros 10 usuarios activos + */ + fun getFirstTenActiveUsers(): QuerySelect { + return QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .limit(10) + .build() + // SQL: SELECT * FROM users WHERE status = 'active' LIMIT 10 + } + + /** + * Ejemplo con paginación: Obtener usuarios con offset + */ + fun getUsersPaginated(page: Int, pageSize: Int = 20): QuerySelect { + val offset = page * pageSize + return QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .orderBy(OrderBy.Desc("created_at")) + .limit(pageSize, offset) + .build() + // SQL: SELECT * FROM users WHERE status = 'active' + // ORDER BY created_at DESC + // LIMIT 20 OFFSET 40 (para page = 2, pageSize = 20) + } + + /** + * Ejemplo con campos específicos y límite + */ + fun getTopUsersByScore(): QuerySelect { + return QuerySelect.builder("users") + .where(SQLOperator.GreaterThan("score", 100)) + .setFields("id", "name", "score") + .orderBy(OrderBy.Desc("score")) + .limit(5) + .build() + // SQL: SELECT id, name, score FROM users WHERE score > 100 + // ORDER BY score DESC + // LIMIT 5 + } + + /** + * Ejemplo usando objeto Limit directamente + */ + fun getRecentPosts(): QuerySelect { + val limitOperator = Limit(count = 15, offset = 0) + return QuerySelect.builder("posts") + .where(SQLOperator.Equals("published", true)) + .orderBy(OrderBy.Desc("published_date")) + .limit(limitOperator) + .build() + // SQL: SELECT * FROM posts WHERE published = 1 + // ORDER BY published_date DESC + // LIMIT 15 + } + + /** + * Ejemplo de consulta compleja con múltiples condiciones y límite + */ + fun getFilteredUsersWithLimit(): QuerySelect { + return QuerySelect.builder("users") + .where(SQLOperator.GreaterThan("age", 18)) + .and("status", SQLOperator.Equals("status", "active")) + .and("country", SQLOperator.Equals("country", "Mexico")) + .setFields("id", "name", "email", "age") + .orderBy(OrderBy.Asc("name")) + .limit(50, 10) + .build() + // SQL: SELECT id, name, email, age FROM users + // WHERE age > 18 AND status = 'active' AND country = 'Mexico' + // ORDER BY name ASC + // LIMIT 50 OFFSET 10 + } + + /** + * Ejemplo de modificación de límite después de crear la consulta + */ + fun dynamicLimit(): QuerySelect { + val query = QuerySelect.builder("products") + .where(SQLOperator.Equals("category", "electronics")) + .orderBy(OrderBy.Desc("price")) + .build() + + // Agregar límite dinámicamente + query.limit(25, 5) + + return query + // SQL: SELECT * FROM products WHERE category = 'electronics' + // ORDER BY price DESC + // LIMIT 25 OFFSET 5 + } +} diff --git a/query/src/main/java/com/blipblipcode/query/operator/Limit.kt b/query/src/main/java/com/blipblipcode/query/operator/Limit.kt new file mode 100644 index 0000000..8a19328 --- /dev/null +++ b/query/src/main/java/com/blipblipcode/query/operator/Limit.kt @@ -0,0 +1,27 @@ +package com.blipblipcode.query.operator + +/** + * Represents a SQL LIMIT clause. + * This class is used to limit the number of rows returned by a SELECT query. + */ +data class Limit( + val count: Int, + val offset: Int? = null +) { + + /** + * Returns the SQL string representation of the LIMIT clause. + * @return The LIMIT clause as a SQL string. + */ + fun asString(): String { + return if (offset != null && offset != 0) { + "LIMIT $count OFFSET $offset" + } else { + "LIMIT $count" + } + } + + override fun toString(): String { + return asString() + } +} diff --git a/query/src/main/java/com/blipblipcode/query/operator/LogicalOperation.kt b/query/src/main/java/com/blipblipcode/query/operator/LogicalOperation.kt index 9b5308c..4b9337b 100644 --- a/query/src/main/java/com/blipblipcode/query/operator/LogicalOperation.kt +++ b/query/src/main/java/com/blipblipcode/query/operator/LogicalOperation.kt @@ -1,25 +1,6 @@ package com.blipblipcode.query.operator -/** - * Defines the types of logical operations that can be used in a SQL WHERE clause. - */ -enum class LogicalType(val sql: String) { - /** Represents a logical AND operation. */ - AND("AND"), - /** Represents a logical OR operation. */ - OR("OR"), - /** Represents a SQL LIKE operation. */ - LIKE("LIKE"), - /** Represents a SQL ALL operation. */ - ALL("ALL"), - /** Represents a SQL AND NOT operation. */ - AND_NOT("AND NOT"), - /** Represents a SQL EXISTS operation. */ - EXISTS("EXISTS"), - /** Represents a SQL NOT operation. */ - NOT("NOT") -} /** * Represents a logical operation in a SQL query, combining a [LogicalType] with a [SQLOperator]. @@ -28,7 +9,7 @@ enum class LogicalType(val sql: String) { * @property type The type of the logical operation (e.g., AND, OR). * @property operator The SQL operator that is part of the logical operation. */ -class LogicalOperation( +data class LogicalOperation( val type: LogicalType, val operator: SQLOperator<*> ) { diff --git a/query/src/main/java/com/blipblipcode/query/operator/LogicalType.kt b/query/src/main/java/com/blipblipcode/query/operator/LogicalType.kt new file mode 100644 index 0000000..06f0903 --- /dev/null +++ b/query/src/main/java/com/blipblipcode/query/operator/LogicalType.kt @@ -0,0 +1,22 @@ +package com.blipblipcode.query.operator + +/** + * Defines the types of logical operations that can be used in a SQL WHERE clause. + */ +enum class LogicalType(val sql: String) { + /** Represents a logical AND operation. */ + AND("AND"), + /** Represents a logical OR operation. */ + OR("OR"), + /** Represents a SQL LIKE operation. */ + LIKE("LIKE"), + /** Represents a SQL ALL operation. */ + ALL("ALL"), + + /** Represents a SQL AND NOT operation. */ + AND_NOT("AND NOT"), + /** Represents a SQL EXISTS operation. */ + EXISTS("EXISTS"), + /** Represents a SQL NOT operation. */ + NOT("NOT") +} \ No newline at end of file diff --git a/query/src/test/java/com/blipblipcode/query/QuerySelectTest.kt b/query/src/test/java/com/blipblipcode/query/QuerySelectTest.kt index 49a5e40..cba561d 100644 --- a/query/src/test/java/com/blipblipcode/query/QuerySelectTest.kt +++ b/query/src/test/java/com/blipblipcode/query/QuerySelectTest.kt @@ -5,7 +5,6 @@ import com.blipblipcode.query.operator.LogicalType import com.blipblipcode.query.operator.SQLOperator import com.blipblipcode.query.utils.asSQLiteQuery import org.junit.Assert.assertEquals -import org.junit.Assert.assertNotEquals import org.junit.Test class QuerySelectTest { @@ -279,4 +278,84 @@ class QuerySelectTest { assertEquals(querySelect.asSql().trim(), supportSQLiteQuery.sql) assertEquals(0, supportSQLiteQuery.argCount) } + + @Test + fun `limit results with a single limit value`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .limit(10) + .build() + val expectedSql = "SELECT * FROM users WHERE status = 'active' LIMIT 10" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `limit results with offset`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .limit(5, 10) + .build() + val expectedSql = "SELECT * FROM users WHERE status = 'active' LIMIT 5 OFFSET 10" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `limit results with chaining call`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .limit(10) + .build() + val instance = query.limit(5, 10) + assertEquals(query, instance) + } + + @Test + fun `limit results with various conditions`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .limit(10, 5) + .build() + val expectedSql = "SELECT * FROM users WHERE status = 'active' LIMIT 10 OFFSET 5" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `limit results with no offset`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .limit(10, 0) + .build() + val expectedSql = "SELECT * FROM users WHERE status = 'active' LIMIT 10" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `limit results with zero limit`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .limit(0) + .build() + val expectedSql = "SELECT * FROM users WHERE status = 'active' LIMIT 0" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `limit results with negative limit`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .limit(-5) + .build() + val expectedSql = "SELECT * FROM users WHERE status = 'active' LIMIT -5" + assertEquals(expectedSql, query.asSql().trim()) + } + + @Test + fun `limit results with negative offset`() { + val query = QuerySelect.builder("users") + .where(SQLOperator.Equals("status", "active")) + .limit(10, -5) + .build() + val expectedSql = "SELECT * FROM users WHERE status = 'active' LIMIT 10 OFFSET -5" + assertEquals(expectedSql, query.asSql().trim()) + } }