diff --git a/query-algebrizer/src/lib.rs b/query-algebrizer/src/lib.rs index 99741d04..57158301 100644 --- a/query-algebrizer/src/lib.rs +++ b/query-algebrizer/src/lib.rs @@ -29,10 +29,26 @@ pub struct AlgebraicQuery { default_source: SrcVar, pub find_spec: FindSpec, has_aggregates: bool, - pub limit: Option, + pub limit: Option, pub cc: cc::ConjoiningClauses, } +impl AlgebraicQuery { + pub fn apply_limit(&mut self, limit: Option) { + match self.limit { + None => self.limit = limit, + Some(existing) => + match limit { + None => (), + Some(new) => + if new < existing { + self.limit = limit; + }, + }, + }; + } +} + #[allow(dead_code)] pub fn algebrize(schema: &Schema, parsed: FindQuery) -> AlgebraicQuery { // TODO: integrate default source into pattern processing. diff --git a/query-sql/src/lib.rs b/query-sql/src/lib.rs index cbbb64c7..aee9d96d 100644 --- a/query-sql/src/lib.rs +++ b/query-sql/src/lib.rs @@ -108,6 +108,7 @@ pub struct SelectQuery { pub projection: Projection, pub from: FromClause, pub constraints: Vec, + pub limit: Option, } // We know that DatomsColumns are safe to serialize. @@ -264,6 +265,12 @@ impl QueryFragment for SelectQuery { constraint.push_sql(out)?; } + // Guaranteed to be positive: u64. + if let Some(limit) = self.limit { + out.push_sql(" LIMIT "); + out.push_sql(limit.to_string().as_str()); + } + Ok(()) } } @@ -316,6 +323,7 @@ mod tests { right: ColumnOrExpression::Entid(65536), }, ], + limit: None, }; let SQLQuery { sql, args } = query.to_sql_query().unwrap(); diff --git a/query-translator/src/translate.rs b/query-translator/src/translate.rs index 2942f228..0f947656 100644 --- a/query-translator/src/translate.rs +++ b/query-translator/src/translate.rs @@ -77,7 +77,7 @@ pub struct CombinedSelectQuery { pub projector: Box, } -fn cc_to_select_query(projection: Projection, cc: ConjoiningClauses) -> SelectQuery { +fn cc_to_select_query>>(projection: Projection, cc: ConjoiningClauses, limit: T) -> SelectQuery { SelectQuery { projection: projection, from: FromClause::TableList(TableList(cc.from)), @@ -85,23 +85,24 @@ fn cc_to_select_query(projection: Projection, cc: ConjoiningClauses) -> SelectQu .into_iter() .map(|c| c.to_constraint()) .collect(), + limit: limit.into(), } } /// Consume a provided `ConjoiningClauses` to yield a new /// `SelectQuery`. A projection list must also be provided. -pub fn cc_to_select(projection: CombinedProjection, cc: ConjoiningClauses) -> CombinedSelectQuery { +pub fn cc_to_select(projection: CombinedProjection, cc: ConjoiningClauses, limit: Option) -> CombinedSelectQuery { let CombinedProjection { sql_projection, datalog_projector } = projection; CombinedSelectQuery { - query: cc_to_select_query(sql_projection, cc), + query: cc_to_select_query(sql_projection, cc, limit), projector: datalog_projector, } } pub fn query_to_select(query: AlgebraicQuery) -> CombinedSelectQuery { - cc_to_select(query_projection(&query), query.cc) + cc_to_select(query_projection(&query), query.cc, query.limit) } pub fn cc_to_exists(cc: ConjoiningClauses) -> SelectQuery { - cc_to_select_query(Projection::One, cc) + cc_to_select_query(Projection::One, cc, 1) } diff --git a/query-translator/tests/translate.rs b/query-translator/tests/translate.rs index b2efbb37..21442afe 100644 --- a/query-translator/tests/translate.rs +++ b/query-translator/tests/translate.rs @@ -76,3 +76,22 @@ fn test_rel() { assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0"); assert_eq!(args, vec![("$v0".to_string(), "yyy".to_string())]); } + +#[test] +fn test_limit() { + let mut schema = Schema::default(); + associate_ident(&mut schema, NamespacedKeyword::new("foo", "bar"), 99); + add_attribute(&mut schema, 99, Attribute { + value_type: ValueType::String, + ..Default::default() + }); + + let input = r#"[:find ?x :where [?x :foo/bar "yyy"]]"#; + let parsed = parse_find_string(input).expect("parse failed"); + let mut algebrized = algebrize(&schema, parsed); + algebrized.limit = Some(5); + let select = query_to_select(algebrized); + let SQLQuery { sql, args } = select.query.to_sql_query().unwrap(); + assert_eq!(sql, "SELECT `datoms00`.e AS `?x` FROM `datoms` AS `datoms00` WHERE `datoms00`.a = 99 AND `datoms00`.v = $v0 LIMIT 5"); + assert_eq!(args, vec![("$v0".to_string(), "yyy".to_string())]); +} diff --git a/src/conn.rs b/src/conn.rs index 4b07f6ca..4d6c12ce 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -107,16 +107,20 @@ impl Conn { } /// Query the Mentat store, using the given connection and the current metadata. - pub fn q_once(&self, + pub fn q_once(&self, sqlite: &rusqlite::Connection, query: &str, - inputs: T) -> Result - where T: Into>> { + inputs: T, + limit: U) -> Result + where T: Into>>, + U: Into> + { q_once(sqlite, &*self.current_schema(), query, - inputs.into()) + inputs, + limit) } /// Transact entities against the Mentat store, using the given connection and the current diff --git a/src/query.rs b/src/query.rs index 822748e8..7b511ca1 100644 --- a/src/query.rs +++ b/src/query.rs @@ -53,14 +53,21 @@ pub type QueryExecutionResult = Result; /// The caller is responsible for ensuring that the SQLite connection is in a transaction if /// isolation is required. #[allow(unused_variables)] -pub fn q_once<'sqlite, 'schema, 'query> +pub fn q_once<'sqlite, 'schema, 'query, T, U> (sqlite: &'sqlite rusqlite::Connection, schema: &'schema Schema, query: &'query str, - inputs: Option>) -> QueryExecutionResult { + inputs: T, + limit: U) -> QueryExecutionResult + where T: Into>>, + U: Into> +{ // TODO: validate inputs. + let parsed = parse_find_string(query)?; - let algebrized = algebrize(schema, parsed); + let mut algebrized = algebrize(schema, parsed); + algebrized.apply_limit(limit.into()); + let select = query_to_select(algebrized); let SQLQuery { sql, args } = select.query.to_sql_query()?; diff --git a/tests/query.rs b/tests/query.rs index 9b3658d2..fe057c77 100644 --- a/tests/query.rs +++ b/tests/query.rs @@ -34,7 +34,7 @@ fn test_rel() { // Rel. let start = time::PreciseTime::now(); let results = q_once(&c, &db.schema, - "[:find ?x ?ident :where [?x :db/ident ?ident]]", None) + "[:find ?x ?ident :where [?x :db/ident ?ident]]", None, None) .expect("Query failed"); let end = time::PreciseTime::now(); @@ -64,7 +64,7 @@ fn test_failing_scalar() { // Scalar that fails. let start = time::PreciseTime::now(); let results = q_once(&c, &db.schema, - "[:find ?x . :where [?x :db/fulltext true]]", None) + "[:find ?x . :where [?x :db/fulltext true]]", None, None) .expect("Query failed"); let end = time::PreciseTime::now(); @@ -86,7 +86,7 @@ fn test_scalar() { // Scalar that succeeds. let start = time::PreciseTime::now(); let results = q_once(&c, &db.schema, - "[:find ?ident . :where [24 :db/ident ?ident]]", None) + "[:find ?ident . :where [24 :db/ident ?ident]]", None, None) .expect("Query failed"); let end = time::PreciseTime::now(); @@ -116,7 +116,7 @@ fn test_tuple() { "[:find [?index ?cardinality] :where [:db/txInstant :db/index ?index] [:db/txInstant :db/cardinality ?cardinality]]", - None) + None, None) .expect("Query failed"); let end = time::PreciseTime::now(); @@ -143,7 +143,7 @@ fn test_coll() { // Coll. let start = time::PreciseTime::now(); let results = q_once(&c, &db.schema, - "[:find [?e ...] :where [?e :db/ident _]]", None) + "[:find [?e ...] :where [?e :db/ident _]]", None, None) .expect("Query failed"); let end = time::PreciseTime::now();