diff --git a/core/src/test/scala/app/softnetwork/elastic/client/help/HelpCorpusSpec.scala b/core/src/test/scala/app/softnetwork/elastic/client/help/HelpCorpusSpec.scala index 606aa66d5..1d28d46bd 100644 --- a/core/src/test/scala/app/softnetwork/elastic/client/help/HelpCorpusSpec.scala +++ b/core/src/test/scala/app/softnetwork/elastic/client/help/HelpCorpusSpec.scala @@ -802,6 +802,14 @@ class HelpCorpusSpec extends AnyFlatSpec with Matchers { // and no user can type one as a statement of its own, so the package walk's coverage is // unchanged. Verified by the superset assertion below and by this file's parser -> doc gate // staying green with no new help document. + // `app$softnetwork$elastic$sql$parser$WhereParser$$subqueryBody` is the TRAIT-PRIVATE + // `subqueryBody` of `WhereParser`. It appears here - and only here - because the packrat + // memoisation fix declares every production as a `lazy val`: a trait-private `def` compiles to + // a private method, while a trait-private `lazy val` needs a mangled but PUBLIC accessor in + // the mixing class, which `getMethods` can see. It names no new statement leaf: its body is + // `start ~> derivedTableBodyInner <~ end`, i.e. the already-enumerated `derivedTableBodyInner` + // in parentheses, so the package walk's coverage is unchanged (the superset assertion below + // stays green and no new help document is required). val expectedAbstract = Set( "statement", @@ -809,7 +817,8 @@ class HelpCorpusSpec extends AnyFlatSpec with Matchers { "ddlStatement", "dmlStatement", "searchStatement", - "derivedTableBodyInner" + "derivedTableBodyInner", + "app$softnetwork$elastic$sql$parser$WhereParser$$subqueryBody" ) withClue( "the set of productions returning a SEALED TRAIT has changed. Every one of them hides its " + diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/FromParser.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/FromParser.scala index 0475bcf7e..c116528dc 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/FromParser.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/FromParser.scala @@ -36,20 +36,20 @@ import app.softnetwork.elastic.sql.query.{ trait FromParser { self: Parser with WhereParser with LimitParser => - def unnest: PackratParser[Join] = + lazy val unnest: PackratParser[Join] = Unnest.regex ~ start ~ identifier ~ end ~ alias.? ^^ { case _ ~ i ~ _ ~ a => Unnest(i, None, a) } - def inner_join: PackratParser[JoinType] = InnerJoin.regex ^^ { _ => InnerJoin } - def left_join: PackratParser[JoinType] = LeftJoin.regex ^^ { _ => LeftJoin } - def right_join: PackratParser[JoinType] = RightJoin.regex ^^ { _ => RightJoin } - def full_join: PackratParser[JoinType] = FullJoin.regex ^^ { _ => FullJoin } - def cross_join: PackratParser[JoinType] = CrossJoin.regex ^^ { _ => CrossJoin } - def join_type: PackratParser[JoinType] = + lazy val inner_join: PackratParser[JoinType] = InnerJoin.regex ^^ { _ => InnerJoin } + lazy val left_join: PackratParser[JoinType] = LeftJoin.regex ^^ { _ => LeftJoin } + lazy val right_join: PackratParser[JoinType] = RightJoin.regex ^^ { _ => RightJoin } + lazy val full_join: PackratParser[JoinType] = FullJoin.regex ^^ { _ => FullJoin } + lazy val cross_join: PackratParser[JoinType] = CrossJoin.regex ^^ { _ => CrossJoin } + lazy val join_type: PackratParser[JoinType] = inner_join | left_join | right_join | full_join | cross_join - def on: PackratParser[On] = On.regex ~> whereCriteria >> { rawTokens => + lazy val on: PackratParser[On] = On.regex ~> whereCriteria >> { rawTokens => // `On(criteria: Criteria)` is not optional (query/From.scala), which is why an ON whose // criteria resolve to nothing used to `throw new Exception`. #250: `err`, like every other // rejection in this package - see `WhereParser.where` for the full reasoning. @@ -79,7 +79,7 @@ trait FromParser { * would render `"logs-2025"."03"` and re-parse as qualifier `logs-2025` + index `03`. * `StandardJoin.sql` renders each `NamePart` as ONE lexeme instead (21.2 AD-5). */ - def source: PackratParser[StandardJoin] = + lazy val source: PackratParser[StandardJoin] = derivedTable ^^ { dt => // A derived JOIN leg owns its alias and its parts are empty — the AD-1 invariant that // `StandardJoin.validate()` ENFORCES. @@ -95,12 +95,13 @@ trait FromParser { ) } - def join: PackratParser[Join] = opt(join_type) ~ Join.regex ~ (unnest | source) ~ opt(on) ^^ { - case _ ~ _ ~ (u: Unnest) ~ _ => - u // Unnest cannot have a join type or an ON clause - case jt ~ _ ~ (sj: StandardJoin) ~ o => - sj.copy(joinType = jt, on = o) - } + lazy val join: PackratParser[Join] = + opt(join_type) ~ Join.regex ~ (unnest | source) ~ opt(on) ^^ { + case _ ~ _ ~ (u: Unnest) ~ _ => + u // Unnest cannot have a join type or an ON clause + case jt ~ _ ~ (sj: StandardJoin) ~ o => + sj.copy(joinType = jt, on = o) + } /** The FROM (and DELETE, and CTAS/MV/WATCHER body) table reference. * @@ -109,7 +110,7 @@ trait FromParser { * discarded, which is what the `quotedSchemaPrefix` this replaces used to do (#85): the render * no longer deletes a clause the statement carried. */ - def table: PackratParser[Table] = + lazy val table: PackratParser[Table] = (derivedTable ^^ { dt => Table(dt.name, None, Nil, Nil, derived = Some(dt)) } | tableParts ~ alias.? ^^ { case ps ~ a => Table(ps.last.value, a, Nil, parts = ps) }) ~ rep(join) ^^ { case t ~ js => t.copy(joins = js) } @@ -142,7 +143,7 @@ trait FromParser { * Recursion (`table -> derivedTable -> searchStatement -> single -> from -> table`) runs through * a CONSUMED `(`, so it is not left recursion; Packrat memoises it and nesting is unbounded. */ - override def derivedTable: PackratParser[DerivedTable] = + override lazy val derivedTable: PackratParser[DerivedTable] = (start ~> (derivedTableBodyInner | err( "A derived table body must be a SELECT: write FROM (SELECT ...) AS " )) <~ end) ~ alias.? >> { @@ -154,8 +155,9 @@ trait FromParser { ) } - def from: PackratParser[From] = From.regex ~ rep1sep(table, separator) ^^ { case _ ~ tables => - From(tables) + lazy val from: PackratParser[From] = From.regex ~ rep1sep(table, separator) ^^ { + case _ ~ tables => + From(tables) } } diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/GroupByParser.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/GroupByParser.scala index 1de5b121a..e79393198 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/GroupByParser.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/GroupByParser.scala @@ -22,7 +22,7 @@ import app.softnetwork.elastic.sql.query.{Bucket, GroupBy} trait GroupByParser { self: Parser with WhereParser => - def bucketWithFunction: PackratParser[Identifier] = + lazy val bucketWithFunction: PackratParser[Identifier] = // #284 - see quotedIdentifierUnlessArithmetic. quotedIdentifierUnlessArithmetic | identifierWithArithmeticExpression | @@ -33,11 +33,11 @@ trait GroupByParser { identifierWithFunction | identifier - def bucket: PackratParser[Bucket] = (long | bucketWithFunction) ^^ { i => + lazy val bucket: PackratParser[Bucket] = (long | bucketWithFunction) ^^ { i => Bucket(i) } - def groupBy: PackratParser[GroupBy] = + lazy val groupBy: PackratParser[GroupBy] = GroupBy.regex ~ rep1sep(bucket, separator) ^^ { case _ ~ buckets => GroupBy(buckets) } diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/HavingParser.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/HavingParser.scala index d9b576c08..80197c20a 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/HavingParser.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/HavingParser.scala @@ -21,7 +21,7 @@ import app.softnetwork.elastic.sql.query.Having trait HavingParser { self: Parser with WhereParser => - def having: PackratParser[Having] = Having.regex ~> whereCriteria >> { rawTokens => + lazy val having: PackratParser[Having] = Having.regex ~> whereCriteria >> { rawTokens => // `err`, not `throw` and not `failure` (#250) - same treatment as `WhereParser.where`, whose // comment carries the full reasoning. `~>` binds tighter than `>>`. processTokens(rawTokens) match { diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/LimitParser.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/LimitParser.scala index fbbe37d49..747c41243 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/LimitParser.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/LimitParser.scala @@ -21,11 +21,11 @@ import app.softnetwork.elastic.sql.query.{Limit, Offset} trait LimitParser { self: Parser => - def offset: PackratParser[Offset] = Offset.regex ~ long ^^ { case _ ~ i => + lazy val offset: PackratParser[Offset] = Offset.regex ~ long ^^ { case _ ~ i => Offset(i.value.toInt) } - def limit: PackratParser[Limit] = Limit.regex ~ long ~ offset.? ^^ { case _ ~ i ~ o => + lazy val limit: PackratParser[Limit] = Limit.regex ~ long ~ offset.? ^^ { case _ ~ i ~ o => Limit(i.value.toInt, o) } diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/OrderByParser.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/OrderByParser.scala index 7305c93e2..a6fa77070 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/OrderByParser.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/OrderByParser.scala @@ -31,20 +31,20 @@ import app.softnetwork.elastic.sql.query.{ trait OrderByParser { self: Parser => - def asc: PackratParser[Asc.type] = Asc.regex ^^ (_ => Asc) + lazy val asc: PackratParser[Asc.type] = Asc.regex ^^ (_ => Asc) - def desc: PackratParser[Desc.type] = Desc.regex ^^ (_ => Desc) + lazy val desc: PackratParser[Desc.type] = Desc.regex ^^ (_ => Desc) - def nullsFirst: PackratParser[NullsFirst.type] = NullsFirst.regex ^^ (_ => NullsFirst) + lazy val nullsFirst: PackratParser[NullsFirst.type] = NullsFirst.regex ^^ (_ => NullsFirst) - def nullsLast: PackratParser[NullsLast.type] = NullsLast.regex ^^ (_ => NullsLast) + lazy val nullsLast: PackratParser[NullsLast.type] = NullsLast.regex ^^ (_ => NullsLast) - def nullOrdering: PackratParser[NullOrdering] = nullsFirst | nullsLast + lazy val nullOrdering: PackratParser[NullOrdering] = nullsFirst | nullsLast - private def fieldName: PackratParser[String] = + private lazy val fieldName: PackratParser[String] = """\b(?!(?i)limit\b)[a-zA-Z_][a-zA-Z0-9_]*""".r ^^ (f => f) - def fieldWithFunction: PackratParser[Identifier] = + lazy val fieldWithFunction: PackratParser[Identifier] = // #284 - see quotedIdentifierUnlessArithmetic. quotedIdentifierUnlessArithmetic | identifierWithArithmeticExpression | @@ -55,13 +55,14 @@ trait OrderByParser { identifierWithFunction | identifier - def sort: PackratParser[FieldSort] = + lazy val sort: PackratParser[FieldSort] = fieldWithFunction ~ (asc | desc).? ~ nullOrdering.? ^^ { case f ~ o ~ n => FieldSort(f, o, n) } - def orderBy: PackratParser[OrderBy] = OrderBy.regex ~ rep1sep(sort, separator) ^^ { case _ ~ s => - OrderBy(s) + lazy val orderBy: PackratParser[OrderBy] = OrderBy.regex ~ rep1sep(sort, separator) ^^ { + case _ ~ s => + OrderBy(s) } } diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/Parser.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/Parser.scala index 7d87441c9..cf68afb5c 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/Parser.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/Parser.scala @@ -87,16 +87,16 @@ object Parser with OrderByParser with LimitParser { - def single: PackratParser[SingleSearch] = { + lazy val single: PackratParser[SingleSearch] = { select ~ from ~ where.? ~ groupBy.? ~ having.? ~ orderBy.? ~ limit.? ~ onConflict.? ^^ { case s ~ f ~ w ~ g ~ h ~ o ~ l ~ oc => SingleSearch(s, f, w, g, h, o, l, onConflict = oc).update() } } - def union: PackratParser[UNION.type] = UNION.regex ^^ (_ => UNION) + lazy val union: PackratParser[UNION.type] = UNION.regex ^^ (_ => UNION) - def searchStatement: PackratParser[SearchStatement] = rep1sep(single, union) ^^ { + lazy val searchStatement: PackratParser[SearchStatement] = rep1sep(single, union) ^^ { case x :: Nil => x case s => MultiSearch(s) } @@ -105,13 +105,13 @@ object Parser * 1 LIMIT 100` must parse — it is Superset's engine probe AND what the Flight sidecar's own * schemaProbeSql rewrites `SELECT 1` into. */ - def fromlessSelect: PackratParser[FromlessSelect] = + lazy val fromlessSelect: PackratParser[FromlessSelect] = select ~ limit.? ^^ { case s ~ l => FromlessSelect(s, l) } - def row: PackratParser[List[Value[_]]] = + lazy val row: PackratParser[List[Value[_]]] = lparen ~> repsep(array_of_struct | struct | value, comma) <~ rparen - def rows: PackratParser[List[List[Value[_]]]] = + lazy val rows: PackratParser[List[List[Value[_]]]] = repsep(row, comma) /** DELIBERATELY still `ident` (story 21.7 Task 1.1). This selects a processor TYPE, not a name: @@ -119,7 +119,7 @@ object Parser * `processorType.name.toUpperCase`, so a quoted spelling could not round-trip even if it parsed. * The one `ident` position that is not a name is not converted. */ - def processorType: PackratParser[IngestProcessorType] = + lazy val processorType: PackratParser[IngestProcessorType] = ident ^^ { name => name.toLowerCase match { case "set" => IngestProcessorType.Set @@ -132,12 +132,12 @@ object Parser } } - def processor: PackratParser[IngestProcessor] = + lazy val processor: PackratParser[IngestProcessor] = processorType ~ objectValue ^^ { case pt ~ opts => IngestProcessor(pt, opts) } - def createOrReplacePipeline: PackratParser[CreatePipeline] = + lazy val createOrReplacePipeline: PackratParser[CreatePipeline] = (keyword("CREATE") ~ keyword("OR") ~ keyword("REPLACE") ~ keyword( "PIPELINE" )) ~ identRef ~ (keyword("WITH") ~ keyword("PROCESSORS")) ~ start ~ repsep( @@ -153,7 +153,7 @@ object Parser ) } - def createPipeline: PackratParser[CreatePipeline] = + lazy val createPipeline: PackratParser[CreatePipeline] = (keyword("CREATE") ~ keyword("PIPELINE")) ~ ifNotExists ~ identRef ~ (keyword( "WITH" ) ~ keyword( @@ -171,48 +171,48 @@ object Parser ) } - def dropPipeline: PackratParser[DropPipeline] = + lazy val dropPipeline: PackratParser[DropPipeline] = (keyword("DROP") ~ keyword("PIPELINE")) ~ ifExists ~ identRef ^^ { case _ ~ ie ~ name => DropPipeline(name._1, ifExists = ie, parts = name._2) } - def showPipeline: PackratParser[ShowPipeline] = + lazy val showPipeline: PackratParser[ShowPipeline] = (keyword("SHOW") ~ keyword("PIPELINE")) ~ identRef ^^ { case _ ~ pipeline => ShowPipeline(pipeline._1, parts = pipeline._2) } - def showPipelines: PackratParser[ShowPipelines.type] = + lazy val showPipelines: PackratParser[ShowPipelines.type] = (keyword("SHOW") ~ keyword("PIPELINES")) ^^ { _ => ShowPipelines } - def showCreatePipeline: PackratParser[ShowCreatePipeline] = + lazy val showCreatePipeline: PackratParser[ShowCreatePipeline] = (keyword("SHOW") ~ keyword("CREATE") ~ keyword("PIPELINE")) ~ identRef ^^ { case _ ~ _ ~ _ ~ pipeline => ShowCreatePipeline(pipeline._1, parts = pipeline._2) } - def describePipeline: PackratParser[DescribePipeline] = + lazy val describePipeline: PackratParser[DescribePipeline] = ((keyword("DESCRIBE") | keyword("DESC")) ~ keyword("PIPELINE")) ~ identRef ^^ { case _ ~ pipeline => DescribePipeline(pipeline._1, parts = pipeline._2) } - def addProcessor: PackratParser[AddPipelineProcessor] = + lazy val addProcessor: PackratParser[AddPipelineProcessor] = (keyword("ADD") ~ keyword("PROCESSOR")) ~ processor ^^ { case _ ~ proc => AddPipelineProcessor(proc) } - def dropProcessor: PackratParser[DropPipelineProcessor] = + lazy val dropProcessor: PackratParser[DropPipelineProcessor] = (keyword("DROP") ~ keyword("PROCESSOR")) ~ processorType ~ start ~ identName ~ end ^^ { case _ ~ pt ~ _ ~ name ~ _ => DropPipelineProcessor(pt, name) } - def alterPipelineStatement: PackratParser[AlterPipelineStatement] = + lazy val alterPipelineStatement: PackratParser[AlterPipelineStatement] = addProcessor | dropProcessor - def alterPipeline: PackratParser[AlterPipeline] = + lazy val alterPipeline: PackratParser[AlterPipeline] = (keyword("ALTER") ~ keyword("PIPELINE")) ~ ifExists ~ identRef ~ start.? ~ repsep( alterPipelineStatement, separator @@ -235,42 +235,42 @@ object Parser * KEYWORD` unconsumed — so `SET FIELD` silently did nothing before #213 made trailing input an * error, and could not parse at all afterwards. */ - def multiFields: PackratParser[List[Column]] = + lazy val multiFields: PackratParser[List[Column]] = keyword("FIELDS") ~ start ~> repsep(column, separator) <~ end ^^ (cols => cols) - def optionalMultiFields: PackratParser[List[Column]] = multiFields | success(Nil) + lazy val optionalMultiFields: PackratParser[List[Column]] = multiFields | success(Nil) - def ifExists: PackratParser[Boolean] = + lazy val ifExists: PackratParser[Boolean] = opt(keyword("IF") ~ keyword("EXISTS")) ^^ { case Some(_) => true case None => false } - def ifNotExists: PackratParser[Boolean] = + lazy val ifNotExists: PackratParser[Boolean] = opt(keyword("IF") ~ keyword("NOT") ~ keyword("EXISTS")) ^^ { case Some(_) => true case None => false } - def notNull: PackratParser[Boolean] = + lazy val notNull: PackratParser[Boolean] = opt(keyword("NOT") ~ keyword("NULL")) ^^ { case Some(_) => true case None => false } - def defaultVal: PackratParser[Option[Value[_]]] = + lazy val defaultVal: PackratParser[Option[Value[_]]] = opt(keyword("DEFAULT") ~ (value | ingest_id | ingest_timestamp)) ^^ { case Some(_ ~ v) => Some(v) case None => None } - def comment: PackratParser[Option[String]] = + lazy val comment: PackratParser[Option[String]] = opt(keyword("COMMENT") ~ literal) ^^ { case Some(_ ~ v) => Some(v.value) case None => None } - def scriptValue: PackratParser[PainlessScript] = identifierWithArithmeticExpression | + lazy val scriptValue: PackratParser[PainlessScript] = identifierWithArithmeticExpression | identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction @@ -337,7 +337,7 @@ object Parser } } - def script: PackratParser[PainlessScript] = + lazy val script: PackratParser[PainlessScript] = (keyword("SCRIPT") ~ keyword("AS")) ~> scriptBody >> { body => parseAll(scriptValue, body) match { case Success(s, _) => success(s) @@ -352,10 +352,10 @@ object Parser * `_meta.columns` from the parsed columns (it drops the incoming `columns` key), so anything not * recoverable from the column list is erased on the first round-trip. Measured, not assumed. */ - def storedScript: PackratParser[(PainlessScript, Boolean)] = + lazy val storedScript: PackratParser[(PainlessScript, Boolean)] = script ~ opt(keyword("STORED")) ^^ { case s ~ stored => (s, stored.isDefined) } - def column: PackratParser[Column] = + lazy val column: PackratParser[Column] = identName ~ extension_type ~ (storedScript | optionalMultiFields) ~ defaultVal ~ notNull ~ comment ~ (options | success( ListMap.empty[String, Value[_]] )) ^^ { case name ~ dt ~ mfs ~ dv ~ nn ~ ct ~ opts => @@ -376,10 +376,10 @@ object Parser } } - def columns: PackratParser[List[Column]] = + lazy val columns: PackratParser[List[Column]] = start ~ repsep(column, separator) ~ end ^^ { case _ ~ cols ~ _ => cols } - def primaryKey: PackratParser[List[String]] = + lazy val primaryKey: PackratParser[List[String]] = separator ~ keyword("PRIMARY") ~ keyword("KEY") ~ start ~ repsep( identName, separator @@ -387,7 +387,7 @@ object Parser keys } | success(Nil) - def granularity: PackratParser[TimeUnit] = start ~ + lazy val granularity: PackratParser[TimeUnit] = start ~ ((keyword("YEAR") ^^^ TimeUnit.YEARS) | (keyword("MONTH") ^^^ TimeUnit.MONTHS) | (keyword("DAY") ^^^ TimeUnit.DAYS) | @@ -395,13 +395,13 @@ object Parser (keyword("MINUTE") ^^^ TimeUnit.MINUTES) | (keyword("SECOND") ^^^ TimeUnit.SECONDS)) ~ end ^^ { case _ ~ gf ~ _ => gf } - def partitionBy: PackratParser[Option[PartitionDate]] = + lazy val partitionBy: PackratParser[Option[PartitionDate]] = opt(keyword("PARTITION") ~ keyword("BY") ~ identName ~ opt(granularity)) ^^ { case Some(_ ~ _ ~ pb ~ gf) => Some(PartitionDate(pb, gf.getOrElse(TimeUnit.DAYS))) case None => None } - def columnsWithPartitionBy + lazy val columnsWithPartitionBy : PackratParser[(List[Column], List[String], Option[PartitionDate], ListMap[String, Any])] = start ~ repsep( column, @@ -412,7 +412,7 @@ object Parser (cols, pk, pb, opts) } - def createOrReplaceTable: PackratParser[CreateTable] = + lazy val createOrReplaceTable: PackratParser[CreateTable] = (keyword("CREATE") ~ keyword("OR") ~ keyword("REPLACE") ~ keyword( "TABLE" )) ~ identRef ~ (columnsWithPartitionBy | (keyword("AS") ~> searchStatement)) ^^ { @@ -445,7 +445,7 @@ object Parser } } - def createTable: PackratParser[CreateTable] = + lazy val createTable: PackratParser[CreateTable] = (keyword("CREATE") ~ keyword( "TABLE" )) ~ ifNotExists ~ identRef ~ (columnsWithPartitionBy | (keyword( @@ -525,45 +525,45 @@ object Parser "that Elasticsearch does not have. Use CREATE TABLE for a regular index and DROP TABLE it " + "when you are done." - def patterns: PackratParser[List[String]] = keyword("LIKE") ~> repsep(literal, comma) ^^ { + lazy val patterns: PackratParser[List[String]] = keyword("LIKE") ~> repsep(literal, comma) ^^ { patterns => patterns.map(_.value) } - def showTables: PackratParser[ShowTables] = + lazy val showTables: PackratParser[ShowTables] = (keyword("SHOW") ~ keyword("TABLES")) ~> opt(patterns) ^^ { indices => ShowTables(indices.getOrElse(Seq.empty)) } - def showTable: PackratParser[ShowTable] = + lazy val showTable: PackratParser[ShowTable] = (keyword("SHOW") ~ keyword("TABLE")) ~ identRef ^^ { case _ ~ table => ShowTable(table._1, parts = table._2) } - def showCreateTable: PackratParser[ShowCreateTable] = + lazy val showCreateTable: PackratParser[ShowCreateTable] = (keyword("SHOW") ~ keyword("CREATE") ~ keyword("TABLE")) ~ identRef ^^ { case _ ~ _ ~ _ ~ table => ShowCreateTable(table._1, parts = table._2) } - def describeTable: PackratParser[DescribeTable] = + lazy val describeTable: PackratParser[DescribeTable] = ((keyword("DESCRIBE") | keyword("DESC")) ~ opt(keyword("TABLE"))) ~ identRef ^^ { case _ ~ table => DescribeTable(table._1, parts = table._2) } - def dropTable: PackratParser[DropTable] = + lazy val dropTable: PackratParser[DropTable] = (keyword("DROP") ~ (keyword("TABLE") | keyword("INDEX"))) ~ ifExists ~ identRef ^^ { case _ ~ ie ~ name => DropTable(name._1, ifExists = ie, parts = name._2) } - def truncateTable: PackratParser[TruncateTable] = + lazy val truncateTable: PackratParser[TruncateTable] = (keyword("TRUNCATE") ~ keyword("TABLE")) ~ identRef ^^ { case _ ~ name => TruncateTable(name._1, parts = name._2) } - def frequency: PackratParser[Frequency] = + lazy val frequency: PackratParser[Frequency] = (keyword("REFRESH") ~ keyword( "EVERY" )) ~> """\d+\s+(MILLISECOND|SECOND|MINUTE|HOUR|DAY|WEEK|MONTH|YEAR)S?""".r ^^ { str => @@ -571,12 +571,12 @@ object Parser Frequency(TransformTimeUnit(parts(1)), parts(0).toLong) } - def withOptions: PackratParser[ListMap[String, Value[_]]] = + lazy val withOptions: PackratParser[ListMap[String, Value[_]]] = (keyword("WITH") ~ lparen) ~> repsep(option, separator) <~ rparen ^^ { opts => ListMap(opts: _*) } - def createOrReplaceMaterializedView: PackratParser[CreateMaterializedView] = + lazy val createOrReplaceMaterializedView: PackratParser[CreateMaterializedView] = (keyword("CREATE") ~ keyword("OR") ~ keyword("REPLACE") ~ keyword("MATERIALIZED") ~ keyword( "VIEW" )) ~ identRef ~ opt(frequency) ~ opt( @@ -593,7 +593,7 @@ object Parser ) } - def createMaterializedView: PackratParser[CreateMaterializedView] = + lazy val createMaterializedView: PackratParser[CreateMaterializedView] = (keyword("CREATE") ~ keyword("MATERIALIZED") ~ keyword("VIEW")) ~ ifNotExists ~ identRef ~ opt( frequency ) ~ opt( @@ -610,117 +610,117 @@ object Parser ) } - def dropMaterializedView: PackratParser[DropMaterializedView] = + lazy val dropMaterializedView: PackratParser[DropMaterializedView] = (keyword("DROP") ~ keyword("MATERIALIZED") ~ keyword("VIEW")) ~ ifExists ~ identRef ^^ { case _ ~ ie ~ name => DropMaterializedView(name._1, ifExists = ie, parts = name._2) } - def refreshMaterializedView: PackratParser[RefreshMaterializedView] = + lazy val refreshMaterializedView: PackratParser[RefreshMaterializedView] = (keyword("REFRESH") ~ keyword("MATERIALIZED") ~ keyword("VIEW")) ~ ifExists ~ identRef ~ opt( keyword("WITH") ~ keyword("SCHEDULE") ~ keyword("NOW") ) ^^ { case _ ~ ie ~ view ~ wn => RefreshMaterializedView(view._1, ifExists = ie, scheduleNow = wn.isDefined, parts = view._2) } - def showMaterializedViewStatus: PackratParser[ShowMaterializedViewStatus] = + lazy val showMaterializedViewStatus: PackratParser[ShowMaterializedViewStatus] = (keyword("SHOW") ~ keyword("MATERIALIZED") ~ keyword("VIEW") ~ keyword( "STATUS" )) ~ identRef ^^ { case _ ~ _ ~ _ ~ _ ~ view => ShowMaterializedViewStatus(view._1, parts = view._2) } - def showCreateMaterializedView: PackratParser[ShowCreateMaterializedView] = + lazy val showCreateMaterializedView: PackratParser[ShowCreateMaterializedView] = (keyword("SHOW") ~ keyword("CREATE") ~ keyword("MATERIALIZED") ~ keyword( "VIEW" )) ~ identRef ^^ { case _ ~ _ ~ _ ~ _ ~ view => ShowCreateMaterializedView(view._1, parts = view._2) } - def showMaterializedView: PackratParser[ShowMaterializedView] = + lazy val showMaterializedView: PackratParser[ShowMaterializedView] = (keyword("SHOW") ~ keyword("MATERIALIZED") ~ keyword("VIEW")) ~ identRef ^^ { case _ ~ _ ~ view => ShowMaterializedView(view._1, parts = view._2) } - def showMaterializedViews: PackratParser[ShowMaterializedViews.type] = + lazy val showMaterializedViews: PackratParser[ShowMaterializedViews.type] = (keyword("SHOW") ~ keyword("MATERIALIZED") ~ keyword("VIEWS")) ^^ { _ => ShowMaterializedViews } - def describeMaterializedView: PackratParser[DescribeMaterializedView] = + lazy val describeMaterializedView: PackratParser[DescribeMaterializedView] = ((keyword("DESCRIBE") | keyword("DESC")) ~ keyword("MATERIALIZED") ~ keyword( "VIEW" )) ~ identRef ^^ { case _ ~ _ ~ _ ~ view => DescribeMaterializedView(view._1, parts = view._2) } - def addColumn: PackratParser[AddColumn] = + lazy val addColumn: PackratParser[AddColumn] = (keyword("ADD") ~ keyword("COLUMN")) ~ ifNotExists ~ column ^^ { case _ ~ ine ~ col => AddColumn(col, ifNotExists = ine) } - def dropColumn: PackratParser[DropColumn] = + lazy val dropColumn: PackratParser[DropColumn] = (keyword("DROP") ~ keyword("COLUMN")) ~ ifExists ~ identName ^^ { case _ ~ ie ~ name => DropColumn(name, ifExists = ie) } - def renameColumn: PackratParser[RenameColumn] = + lazy val renameColumn: PackratParser[RenameColumn] = (keyword("RENAME") ~ keyword("COLUMN")) ~ identName ~ (keyword("TO") ~> identName) ^^ { case _ ~ oldName ~ newName => RenameColumn(oldName, newName) } - def alterColumnIfExists: PackratParser[Boolean] = + lazy val alterColumnIfExists: PackratParser[Boolean] = (keyword("ALTER") ~ keyword("COLUMN")) ~ ifExists ^^ { case _ ~ ie => ie } - def alterColumnOptions: PackratParser[AlterColumnOptions] = + lazy val alterColumnOptions: PackratParser[AlterColumnOptions] = alterColumnIfExists ~ identName ~ keyword("SET") ~ options ^^ { case ie ~ col ~ _ ~ opts => AlterColumnOptions(col, opts, ifExists = ie) } - def alterColumnOption: PackratParser[AlterColumnOption] = + lazy val alterColumnOption: PackratParser[AlterColumnOption] = alterColumnIfExists ~ identName ~ ((keyword("SET") | keyword("ADD")) ~ keyword( "OPTION" )) ~ start ~ option ~ end ^^ { case ie ~ col ~ _ ~ _ ~ opt ~ _ => AlterColumnOption(col, opt._1, opt._2, ifExists = ie) } - def dropColumnOption: PackratParser[DropColumnOption] = + lazy val dropColumnOption: PackratParser[DropColumnOption] = alterColumnIfExists ~ identName ~ (keyword("DROP") ~ keyword("OPTION")) ~ identName ^^ { case ie ~ col ~ _ ~ optionName => DropColumnOption(col, optionName, ifExists = ie) } - def alterColumnFields: PackratParser[AlterColumnFields] = + lazy val alterColumnFields: PackratParser[AlterColumnFields] = alterColumnIfExists ~ identName ~ keyword("SET") ~ multiFields ^^ { case ie ~ col ~ _ ~ fields => AlterColumnFields(col, fields, ifExists = ie) } - def alterColumnField: PackratParser[AlterColumnField] = + lazy val alterColumnField: PackratParser[AlterColumnField] = alterColumnIfExists ~ identName ~ ((keyword("SET") | keyword("ADD")) ~ keyword( "FIELD" )) ~ column ^^ { case ie ~ col ~ _ ~ field => AlterColumnField(col, field, ifExists = ie) } - def dropColumnField: PackratParser[DropColumnField] = + lazy val dropColumnField: PackratParser[DropColumnField] = alterColumnIfExists ~ identName ~ (keyword("DROP") ~ keyword("FIELD")) ~ identName ^^ { case ie ~ col ~ _ ~ fieldName => DropColumnField(col, fieldName, ifExists = ie) } - def alterColumnType: PackratParser[AlterColumnType] = + lazy val alterColumnType: PackratParser[AlterColumnType] = alterColumnIfExists ~ identName ~ (keyword("SET") ~ keyword("DATA") ~ keyword( "TYPE" )) ~ extension_type ^^ { case ie ~ name ~ _ ~ newType => AlterColumnType(name, newType, ifExists = ie) } - def alterColumnScript: PackratParser[AlterColumnScript] = + lazy val alterColumnScript: PackratParser[AlterColumnScript] = alterColumnIfExists ~ identName ~ keyword("SET") ~ storedScript ^^ { case ie ~ name ~ _ ~ ((ns, stored)) => AlterColumnScript( @@ -730,7 +730,7 @@ object Parser ) } - def dropColumnScript: PackratParser[DropColumnScript] = + lazy val dropColumnScript: PackratParser[DropColumnScript] = alterColumnIfExists ~ identName ~ (keyword("DROP") ~ keyword("SCRIPT")) ^^ { case ie ~ name ~ _ => DropColumnScript(name, ifExists = ie) @@ -742,57 +742,57 @@ object Parser * whenever that column already exists (`TableDiff` renders `ColumnDefaultSet` and the extension * runs the rendered SQL). */ - def alterColumnDefault: PackratParser[AlterColumnDefault] = + lazy val alterColumnDefault: PackratParser[AlterColumnDefault] = alterColumnIfExists ~ identName ~ (keyword("SET") ~ keyword( "DEFAULT" )) ~ (value | ingest_id | ingest_timestamp) ^^ { case ie ~ name ~ _ ~ dv => AlterColumnDefault(name, dv, ifExists = ie) } - def dropColumnDefault: PackratParser[DropColumnDefault] = + lazy val dropColumnDefault: PackratParser[DropColumnDefault] = alterColumnIfExists ~ identName ~ (keyword("DROP") ~ keyword("DEFAULT")) ^^ { case ie ~ name ~ _ => DropColumnDefault(name, ifExists = ie) } - def alterColumnNotNull: PackratParser[AlterColumnNotNull] = + lazy val alterColumnNotNull: PackratParser[AlterColumnNotNull] = alterColumnIfExists ~ identName ~ (keyword("SET") ~ keyword("NOT") ~ keyword("NULL")) ^^ { case ie ~ name ~ _ => AlterColumnNotNull(name, ifExists = ie) } - def dropColumnNotNull: PackratParser[DropColumnNotNull] = + lazy val dropColumnNotNull: PackratParser[DropColumnNotNull] = alterColumnIfExists ~ identName ~ (keyword("DROP") ~ keyword("NOT") ~ keyword("NULL")) ^^ { case ie ~ name ~ _ => DropColumnNotNull(name, ifExists = ie) } - def alterColumnComment: PackratParser[AlterColumnComment] = + lazy val alterColumnComment: PackratParser[AlterColumnComment] = alterColumnIfExists ~ identName ~ (keyword("SET") ~ keyword("COMMENT")) ~ literal ^^ { case ie ~ name ~ _ ~ c => AlterColumnComment(name, c.value, ifExists = ie) } - def dropColumnComment: PackratParser[DropColumnComment] = + lazy val dropColumnComment: PackratParser[DropColumnComment] = alterColumnIfExists ~ identName ~ (keyword("DROP") ~ keyword("COMMENT")) ^^ { case ie ~ name ~ _ => DropColumnComment(name, ifExists = ie) } - def alterTableMapping: PackratParser[AlterTableMapping] = + lazy val alterTableMapping: PackratParser[AlterTableMapping] = ((keyword("SET") | keyword("ADD")) ~ keyword("MAPPING")) ~ option ^^ { case _ ~ opt => AlterTableMapping(opt._1, opt._2) } - def dropTableMapping: PackratParser[DropTableMapping] = + lazy val dropTableMapping: PackratParser[DropTableMapping] = (keyword("DROP") ~ keyword("MAPPING")) ~> identName ^^ { m => DropTableMapping(m) } - def alterTableSetting: PackratParser[AlterTableSetting] = + lazy val alterTableSetting: PackratParser[AlterTableSetting] = ((keyword("SET") | keyword("ADD")) ~ keyword("SETTING")) ~ option ^^ { case _ ~ opt => AlterTableSetting(opt._1, opt._2) } - def dropTableSetting: PackratParser[DropTableSetting] = + lazy val dropTableSetting: PackratParser[DropTableSetting] = (keyword("DROP") ~ keyword("SETTING")) ~> identName ^^ { m => DropTableSetting(m) } /** `SET SCHEMA CACHE TTL = '10m'` — sugar over the metadata write it desugars to, NOT a second @@ -804,7 +804,7 @@ object Parser * refused at parse time rather than silently ignored for the lifetime of the index. `err`, never * `throw`: `Parser.apply` is typed `Either[ParserError, Statement]` (#250). */ - def alterTableSchemaCacheTtl: PackratParser[AlterTableMapping] = + lazy val alterTableSchemaCacheTtl: PackratParser[AlterTableMapping] = ((keyword("SET") ~ keyword("SCHEMA") ~ keyword("CACHE") ~ keyword( "TTL" )) ~ "=".? ~ literal) >> { case _ ~ _ ~ ttl => @@ -814,20 +814,20 @@ object Parser } } - def dropTableSchemaCacheTtl: PackratParser[DropTableMapping] = + lazy val dropTableSchemaCacheTtl: PackratParser[DropTableMapping] = (keyword("DROP") ~ keyword("SCHEMA") ~ keyword("CACHE") ~ keyword("TTL")) ^^ { _ => DropTableMapping(SchemaCacheTtl.MetadataPath) } - def alterTableAlias: PackratParser[AlterTableAlias] = + lazy val alterTableAlias: PackratParser[AlterTableAlias] = ((keyword("SET") | keyword("ADD")) ~ keyword("ALIAS")) ~ option ^^ { case _ ~ opt => AlterTableAlias(opt._1, opt._2) } - def dropTableAlias: PackratParser[DropTableAlias] = + lazy val dropTableAlias: PackratParser[DropTableAlias] = (keyword("DROP") ~ keyword("ALIAS")) ~> identName ^^ { m => DropTableAlias(m) } - def alterTableStatement: PackratParser[AlterTableStatement] = + lazy val alterTableStatement: PackratParser[AlterTableStatement] = addColumn | dropColumn | renameColumn | @@ -855,7 +855,7 @@ object Parser alterTableAlias | dropTableAlias - def alterTable: PackratParser[AlterTable] = + lazy val alterTable: PackratParser[AlterTable] = (keyword("ALTER") ~ keyword("TABLE")) ~ ifExists ~ identRef ~ start.? ~ repsep( alterTableStatement, separator @@ -879,20 +879,20 @@ object Parser // Watcher parsers // Watcher condition parsers - def alwaysWatcherCondition: PackratParser[AlwaysWatcherCondition.type] = + lazy val alwaysWatcherCondition: PackratParser[AlwaysWatcherCondition.type] = keyword("ALWAYS") ^^ { _ => AlwaysWatcherCondition } - def neverWatcherCondition: PackratParser[NeverWatcherCondition.type] = + lazy val neverWatcherCondition: PackratParser[NeverWatcherCondition.type] = keyword("NEVER") ^^ { _ => NeverWatcherCondition } - private def comparison_operator: PackratParser[ComparisonOperator] = + private lazy val comparison_operator: PackratParser[ComparisonOperator] = eq | ne | diff | gt | ge | lt | le - private def dateMathScript + private lazy val dateMathScript : PackratParser[DateTimeFunction with FunctionWithIdentifier with DateMathScript] = date_add | datetime_add | date_sub | datetime_sub - def compareWatcherCondition: PackratParser[CompareWatcherCondition] = + lazy val compareWatcherCondition: PackratParser[CompareWatcherCondition] = keyword("WHEN") ~> opt(not) ~ identName ~ comparison_operator ~ opt(value) ~ opt( dateMathScript ) >> { case n ~ field ~ op ~ v ~ fun => @@ -928,13 +928,13 @@ object Parser } } - private def scriptParams: PackratParser[ListMap[String, Value[_]]] = + private lazy val scriptParams: PackratParser[ListMap[String, Value[_]]] = (keyword("WITH") ~ keyword("PARAMS")) ~> lparen ~ repsep(option, comma) ~ rparen ^^ { case _ ~ opts ~ _ => ListMap(opts: _*) } - def scriptWatcherCondition: PackratParser[ScriptWatcherCondition] = + lazy val scriptWatcherCondition: PackratParser[ScriptWatcherCondition] = (keyword("WHEN") ~ keyword("SCRIPT")) ~> literal ~ opt( keyword("USING") ~ keyword("LANG") ~> literal ) ~ opt( @@ -947,33 +947,33 @@ object Parser ) } - def watcherCondition: PackratParser[WatcherCondition] = + lazy val watcherCondition: PackratParser[WatcherCondition] = neverWatcherCondition | alwaysWatcherCondition | compareWatcherCondition | scriptWatcherCondition // Watcher trigger parsers - def triggerWatcherEveryInterval: PackratParser[IntervalWatcherTrigger] = + lazy val triggerWatcherEveryInterval: PackratParser[IntervalWatcherTrigger] = keyword("EVERY") ~> """\d+\s+(MILLISECOND|SECOND|MINUTE|HOUR|DAY|WEEK|MONTH|YEAR)S?""".r ^^ { str => val parts = str.trim.split("\\s+") IntervalWatcherTrigger(Delay(TransformTimeUnit(parts(1)), parts(0).toLong)) } - def triggerWatcherAtSchedule: PackratParser[CronWatcherTrigger] = + lazy val triggerWatcherAtSchedule: PackratParser[CronWatcherTrigger] = (keyword("AT") ~ keyword("SCHEDULE")) ~> literal ^^ { cronExpr => CronWatcherTrigger(cronExpr.value) } - def watcherTrigger: PackratParser[WatcherTrigger] = + lazy val watcherTrigger: PackratParser[WatcherTrigger] = triggerWatcherEveryInterval | triggerWatcherAtSchedule // Watcher input parsers - def simpleWatcherInput: PackratParser[SimpleWatcherInput] = + lazy val simpleWatcherInput: PackratParser[SimpleWatcherInput] = opt(keyword("WITH") ~ keyword("INPUT")) ~> start ~ repsep(option, comma) ~ end ^^ { case _ ~ opts ~ _ => SimpleWatcherInput(payload = ObjectValue(ListMap(opts: _*))) } - def withinTimeout: PackratParser[Option[Delay]] = + lazy val withinTimeout: PackratParser[Option[Delay]] = opt( keyword("WITHIN") ~> """(\d+\s+(MILLISECOND|SECOND|MINUTE|HOUR|DAY|WEEK|MONTH|YEAR)S?)""".r ) ^^ { @@ -1027,7 +1027,7 @@ object Parser // `err` (not `failure`) is deliberate: it short-circuits the enclosing alternatives instead of // letting `watcherInput` fall through to `success(EmptyWatcherInput)` and report a position // error that names neither JOIN nor the watcher. - def searchInput: PackratParser[SearchWatcherInput] = + lazy val searchInput: PackratParser[SearchWatcherInput] = from ~ opt(where) ~ withinTimeout >> { case f ~ w ~ t => // Story 22.1 — a watcher input keeps only `tables.map(_.name)`, and a derived table's name // is its ALIAS, so the watcher would silently watch an index named after the subquery. Same @@ -1081,17 +1081,17 @@ object Parser } } - def httpInput: PackratParser[HttpInput] = + lazy val httpInput: PackratParser[HttpInput] = opt(keyword("WITH") ~ keyword("INPUT")) ~> httpRequest ^^ { req => HttpInput(req) } - def chainInput: PackratParser[(String, WatcherInput)] = + lazy val chainInput: PackratParser[(String, WatcherInput)] = identName ~ opt(keyword("AS")) ~ watcherInput ^^ { case name ~ _ ~ input => (name, input) } - def chainInputs: PackratParser[WatcherInput] = + lazy val chainInputs: PackratParser[WatcherInput] = (keyword("WITH") ~ keyword("INPUTS")) ~> rep1sep( chainInput, comma @@ -1099,7 +1099,7 @@ object Parser ChainInput(ListMap(inputs: _*)) } - def watcherInput: PackratParser[WatcherInput] = + lazy val watcherInput: PackratParser[WatcherInput] = chainInputs | searchInput | httpInput | simpleWatcherInput | success(EmptyWatcherInput) // logging action parsers @@ -1108,17 +1108,17 @@ object Parser def warn: Parser[LoggingLevel] = "(?i)(WARN)\\b".r ^^ { _ => LoggingLevel.WARN } def error: Parser[LoggingLevel] = "(?i)(ERROR)\\b".r ^^ { _ => LoggingLevel.ERROR } - def loggingLevel: PackratParser[LoggingLevel] = + lazy val loggingLevel: PackratParser[LoggingLevel] = info | debug | warn | error // action foreach limit parser - def foreachWithLimit: PackratParser[(String, Int)] = + lazy val foreachWithLimit: PackratParser[(String, Int)] = (keyword("FOREACH") ~> literal) ~ (keyword("LIMIT") ~> """\d+""".r) ^^ { case fe ~ l => (fe.value, l.toInt) } // simple logging action parser - def loggingAction: PackratParser[Option[LoggingAction]] = + lazy val loggingAction: PackratParser[Option[LoggingAction]] = (keyword("LOG") ~> literal) ~ opt(keyword("AT") ~> loggingLevel) ~ opt(foreachWithLimit) ^^ { case text ~ levelOpt ~ feOpt => val foreach = feOpt.map(_._1) @@ -1127,14 +1127,14 @@ object Parser } // webhook action parser - def webhookAction: PackratParser[Option[WebhookAction]] = + lazy val webhookAction: PackratParser[Option[WebhookAction]] = keyword("WEBHOOK") ~> httpRequest ~ opt(foreachWithLimit) ^^ { case req ~ feOpt => val foreach = feOpt.map(_._1) val limit = feOpt.map(_._2) Some(WebhookAction(req, foreach, limit)) } - def watcherAction: PackratParser[(String, WatcherAction)] = + lazy val watcherAction: PackratParser[(String, WatcherAction)] = identName ~ opt(keyword("AS")) ~ (loggingAction | webhookAction) >> { case name ~ _ ~ wa => wa match { case Some(wa) => success((name, wa)) @@ -1142,7 +1142,7 @@ object Parser } } - def watcherActions: PackratParser[ListMap[String, WatcherAction]] = + lazy val watcherActions: PackratParser[ListMap[String, WatcherAction]] = rep1sep( watcherAction, separator @@ -1157,7 +1157,7 @@ object Parser * accept the SAME watcher-name spellings; the discarded qualifier run is the price of that * uniformity, and it is one allocation on a statement that opens a watch. */ - def createOrReplaceWatcher: PackratParser[CreateWatcher] = + lazy val createOrReplaceWatcher: PackratParser[CreateWatcher] = (keyword("CREATE") ~ keyword("OR") ~ keyword("REPLACE") ~ keyword( "WATCHER" )) ~> identRef ~ opt( @@ -1177,7 +1177,7 @@ object Parser ) } - def createWatcher: PackratParser[CreateWatcher] = + lazy val createWatcher: PackratParser[CreateWatcher] = (keyword("CREATE") ~ keyword("WATCHER")) ~ ifNotExists ~ identRef ~ opt( keyword("AS") ) ~ watcherTrigger ~ watcherInput ~ watcherCondition ~ (keyword( @@ -1195,22 +1195,22 @@ object Parser ) } - def showWatcherStatus: PackratParser[ShowWatcherStatus] = + lazy val showWatcherStatus: PackratParser[ShowWatcherStatus] = (keyword("SHOW") ~ keyword("WATCHER") ~ keyword("STATUS")) ~> identRef ^^ { name => ShowWatcherStatus(name._1, parts = name._2) } - def showWatchers: PackratParser[ShowWatchers.type] = + lazy val showWatchers: PackratParser[ShowWatchers.type] = (keyword("SHOW") ~ keyword("WATCHERS")) ^^ { _ => ShowWatchers } - def dropWatcher: PackratParser[DropWatcher] = + lazy val dropWatcher: PackratParser[DropWatcher] = (keyword("DROP") ~ keyword("WATCHER")) ~ ifExists ~ identRef ^^ { case _ ~ ie ~ name => DropWatcher(name._1, ifExists = ie, parts = name._2) } - def createEnrichPolicy: PackratParser[CreateEnrichPolicy] = + lazy val createEnrichPolicy: PackratParser[CreateEnrichPolicy] = (keyword("CREATE") ~ keyword("ENRICH") ~ keyword("POLICY")) ~ ifNotExists ~ identRef ~ @@ -1235,7 +1235,7 @@ object Parser ) } - def createOrReplaceEnrichPolicy: PackratParser[CreateEnrichPolicy] = + lazy val createOrReplaceEnrichPolicy: PackratParser[CreateEnrichPolicy] = (keyword("CREATE") ~ keyword("OR") ~ keyword("REPLACE") ~ keyword("ENRICH") ~ keyword( "POLICY" )) ~ @@ -1263,38 +1263,38 @@ object Parser ) } - def executeEnrichPolicy: PackratParser[ExecuteEnrichPolicy] = + lazy val executeEnrichPolicy: PackratParser[ExecuteEnrichPolicy] = (keyword("EXECUTE") ~ keyword("ENRICH") ~ keyword("POLICY")) ~> identRef ^^ { name => ExecuteEnrichPolicy(name._1, parts = name._2) } - def dropEnrichPolicy: PackratParser[DropEnrichPolicy] = + lazy val dropEnrichPolicy: PackratParser[DropEnrichPolicy] = (keyword("DROP") ~ keyword("ENRICH") ~ keyword("POLICY")) ~ ifExists ~ identRef ^^ { case _ ~ ie ~ name => DropEnrichPolicy(name._1, ifExists = ie, parts = name._2) } - def showEnrichPolicy: PackratParser[ShowEnrichPolicy] = + lazy val showEnrichPolicy: PackratParser[ShowEnrichPolicy] = (keyword("SHOW") ~ keyword("ENRICH") ~ keyword("POLICY")) ~> identRef ^^ { name => ShowEnrichPolicy(name._1, parts = name._2) } - def showEnrichPolicies: PackratParser[ShowEnrichPolicies.type] = + lazy val showEnrichPolicies: PackratParser[ShowEnrichPolicies.type] = (keyword("SHOW") ~ keyword("ENRICH") ~ keyword("POLICIES")) ^^ { _ => ShowEnrichPolicies } - def showClusterName: PackratParser[ShowClusterName.type] = + lazy val showClusterName: PackratParser[ShowClusterName.type] = (keyword("SHOW") ~ keyword("CLUSTER") ~ keyword("NAME")) ^^ { _ => ShowClusterName } - def showLicense: PackratParser[ShowLicense.type] = + lazy val showLicense: PackratParser[ShowLicense.type] = (keyword("SHOW") ~ keyword("LICENSE")) ^^ { _ => ShowLicense } - def refreshLicense: PackratParser[RefreshLicense.type] = + lazy val refreshLicense: PackratParser[RefreshLicense.type] = (keyword("REFRESH") ~ keyword("LICENSE")) ^^ { _ => RefreshLicense } @@ -1310,10 +1310,10 @@ object Parser * The ascription is needed because `Parser[+T].|[U >: T]` cannot unify `SearchStatement` with * `FromlessSelect`; their common supertype is `DqlStatement`. */ - override def derivedTableBodyInner: PackratParser[DqlStatement] = + override lazy val derivedTableBodyInner: PackratParser[DqlStatement] = (searchStatement: PackratParser[DqlStatement]) | fromlessSelect - def dqlStatement: PackratParser[DqlStatement] = { + lazy val dqlStatement: PackratParser[DqlStatement] = { searchStatement | // Issue #251 — FROM-less SELECT. MUST stay immediately AFTER searchStatement: `|` commits // to the first SUCCEEDING alternative, and searchStatement FAILS (not partially succeeds) @@ -1342,7 +1342,7 @@ object Parser refreshLicense } - def ddlStatement: PackratParser[DdlStatement] = + lazy val ddlStatement: PackratParser[DdlStatement] = // Recognise-to-reject, FIRST on purpose — measured, not assumed. `TEMPORARY` is mandatory and // no other alternative accepts it in that position, so this can never commit to a prefix of a // statement another alternative handles (proved by a 994-statement differential probe: zero @@ -1374,18 +1374,18 @@ object Parser executeEnrichPolicy | dropEnrichPolicy - def onConflict: PackratParser[OnConflict] = + lazy val onConflict: PackratParser[OnConflict] = (keyword("ON") ~ keyword("CONFLICT") ~> opt(conflictTarget) <~ keyword("DO")) ~ (keyword( "UPDATE" ) | keyword("NOTHING")) ^^ { case target ~ action => OnConflict(target, action == "UPDATE") } - def conflictTarget: PackratParser[List[String]] = + lazy val conflictTarget: PackratParser[List[String]] = start ~> repsep(identName, separator) <~ end /** INSERT INTO table [(col1, col2, ...)] VALUES (v1, v2, ...) */ - def insert: PackratParser[Insert] = + lazy val insert: PackratParser[Insert] = (keyword("INSERT") ~ keyword("INTO")) ~ identRef ~ opt( lparen ~> repsep(identName, comma) <~ rparen ) ~ @@ -1412,7 +1412,7 @@ object Parser * the loss. A FILE_FORMAT followed by anything but a known format is a hard `err` for the same * reason: backtracking here can only ever mean dropping what the user wrote. */ - def fileFormat: PackratParser[FileFormat] = + lazy val fileFormat: PackratParser[FileFormat] = (keyword("FILE_FORMAT") ~ opt("=")) ~> ( (keyword("PARQUET") ^^^ Parquet) | (keyword("JSON_ARRAY") ^^^ JsonArray) | @@ -1438,7 +1438,7 @@ object Parser ) /** COPY INTO table FROM source */ - def copy: PackratParser[CopyInto] = + lazy val copy: PackratParser[CopyInto] = (keyword("COPY") ~ keyword("INTO")) ~ identRef ~ (keyword("FROM") ~> literal) ~ opt( fileFormat ) ~ opt(onConflict) ^^ { case _ ~ table ~ source ~ format ~ conflict => @@ -1458,7 +1458,7 @@ object Parser * to be discarded in silence and the UPDATE ran against the first table alone (#213). It catches * both operand orders: written before the WHERE, `where.?` yields None and this fires. */ - def update: PackratParser[Update] = + lazy val update: PackratParser[Update] = (keyword("UPDATE") ~> identRef) ~ (keyword("SET") ~> repsep( identName ~ "=" ~ (value | scriptValue), separator @@ -1490,7 +1490,7 @@ object Parser * `DELETE FROM a` with **no** WHERE, which the client turns into `match_all` — wiping the whole * index instead of the matching rows (#213). */ - def delete: PackratParser[Delete] = + lazy val delete: PackratParser[Delete] = (keyword("DELETE") ~ keyword("FROM")) ~> rep1sep(table, separator) ~ where.? >> { case tables ~ w => tables.flatMap(_.joins) match { @@ -1531,9 +1531,9 @@ object Parser } } - def dmlStatement: PackratParser[DmlStatement] = insert | update | delete | copy + lazy val dmlStatement: PackratParser[DmlStatement] = insert | update | delete | copy - def statement: PackratParser[Statement] = ddlStatement | dqlStatement | dmlStatement + lazy val statement: PackratParser[Statement] = ddlStatement | dqlStatement | dmlStatement /** Strip `--` comments and collapse newlines OUTSIDE string literals only. The previous * line-based normalizer (`split("\n").map(_.split("--")(0))`) was blind to quotes: it cut `WHERE @@ -1757,12 +1757,12 @@ trait Parser val startStruct: Parser[String] = "{" val endStruct: Parser[String] = "}" - def objectValue: PackratParser[ObjectValue] = + lazy val objectValue: PackratParser[ObjectValue] = lparen ~> repsep(option, comma) <~ rparen ^^ { opts => ObjectValue(ListMap(opts: _*)) } - def objectValues: PackratParser[ObjectValues] = + lazy val objectValues: PackratParser[ObjectValues] = lbracket ~> rep1sep(objectValue, comma) <~ rbracket ^^ { ovs => ObjectValues(ovs) } @@ -1770,7 +1770,7 @@ trait Parser // `ingest_id | ingest_timestamp` for the same reason as `alterColumnDefault`: the mapping // metadata a column's DEFAULT is mirrored into (`_meta.columns..default_value`) is written // through this production. - def option: PackratParser[(String, Value[_])] = + lazy val option: PackratParser[(String, Value[_])] = (identName | literal) ~ "=" ~ (objectValues | objectValue | value | ingest_id | ingest_timestamp) ^^ { case key ~ _ ~ value => key match { @@ -1779,33 +1779,33 @@ trait Parser } } - def options: PackratParser[ListMap[String, Value[_]]] = + lazy val options: PackratParser[ListMap[String, Value[_]]] = keyword("OPTIONS") ~ lparen ~ repsep(option, comma) ~ rparen ^^ { case _ ~ _ ~ opts ~ _ => ListMap(opts: _*) } - def array_of_struct: PackratParser[ObjectValues] = + lazy val array_of_struct: PackratParser[ObjectValues] = lbracket ~> repsep(struct, comma) <~ rbracket ^^ { ovs => ObjectValues(ovs) } - def struct_entry: PackratParser[(String, Value[_])] = + lazy val struct_entry: PackratParser[(String, Value[_])] = identName ~ "=" ~ (array_of_struct | struct | value) ^^ { case key ~ _ ~ v => key -> v } - def struct: PackratParser[ObjectValue] = + lazy val struct: PackratParser[ObjectValue] = startStruct ~> repsep(struct_entry, comma) <~ endStruct ^^ { entries => ObjectValue(ListMap(entries: _*)) } - def start: PackratParser[Delimiter] = "(" ^^ (_ => StartPredicate) + lazy val start: PackratParser[Delimiter] = "(" ^^ (_ => StartPredicate) - def end: PackratParser[Delimiter] = ")" ^^ (_ => EndPredicate) + lazy val end: PackratParser[Delimiter] = ")" ^^ (_ => EndPredicate) - def separator: PackratParser[Delimiter] = "," ^^ (_ => Separator) + lazy val separator: PackratParser[Delimiter] = "," ^^ (_ => Separator) - def valueExpr: PackratParser[PainlessScript] = { + lazy val valueExpr: PackratParser[PainlessScript] = { // the order is important here identifierWithWindowFunction | identifierWithTransformation | // transformations applied to an identifier @@ -1823,7 +1823,7 @@ trait Parser case _ => Identifier(mf) } - def sql_function: PackratParser[Function] = + lazy val sql_function: PackratParser[Function] = aggregate_function | time_function | conditional_function private val reservedKeywords = Seq( @@ -2104,10 +2104,10 @@ trait Parser */ private val bareFirstPartRegex: Regex = bareFirstPartStr.r - private def quotedPart: PackratParser[(String, Boolean)] = + private lazy val quotedPart: PackratParser[(String, Boolean)] = quotedNameRegex ^^ (lexeme => (unquoteName(lexeme), true)) - private def bareFirstPart: PackratParser[(String, Boolean)] = + private lazy val bareFirstPart: PackratParser[(String, Boolean)] = bareFirstPartRegex ^^ (n => (n, false)) /** One dot-separated tail element, **separator included**, matched as a SINGLE regex so the dot @@ -2130,7 +2130,7 @@ trait Parser /** Compiled once, for the same reason as `bareFirstPartRegex`. */ private val nameTailPartRegex: Regex = nameTailPartStr.r - private def nameTailPart: PackratParser[(String, Boolean)] = + private lazy val nameTailPart: PackratParser[(String, Boolean)] = nameTailPartRegex ^^ { lexeme => val part = lexeme.substring(1) // drop the leading dot, which this regex owns part.charAt(0) match { @@ -2139,7 +2139,7 @@ trait Parser } } - private def nameTail: PackratParser[List[(String, Boolean)]] = rep(nameTailPart) + private lazy val nameTail: PackratParser[List[(String, Boolean)]] = rep(nameTailPart) private def joinNameParts(parts: List[(String, Boolean)]): (String, Boolean) = (parts.map(_._1).mkString("."), parts.exists(_._2)) @@ -2152,14 +2152,14 @@ trait Parser * PUBLIC on purpose: story 21.2 rewrites `FromParser.table` on top of this so the FROM/JOIN * surface cannot become a second lexer. */ - def qualifiedName: PackratParser[(String, Boolean)] = + lazy val qualifiedName: PackratParser[(String, Boolean)] = (quotedPart | bareFirstPart) ~ nameTail ^^ { case h ~ t => joinNameParts(h :: t) } /** `qualifiedName`, but the FIRST part must be quoted -- which makes `quotedIdentifier` a strict * subset of `identifier` and lets every existing `quotedIdentifier | ...` alternation keep its * exact current behaviour. */ - def quotedQualifiedName: PackratParser[(String, Boolean)] = + lazy val quotedQualifiedName: PackratParser[(String, Boolean)] = quotedPart ~ nameTail ^^ { case h ~ t => joinNameParts(h :: t) } /** One QUOTED name part consumed as a leading TABLE-name qualifier: `` `prod_us`. `` or @@ -2185,7 +2185,7 @@ trait Parser * * `quotedPart` is private to this trait; this is its one FROM-side export. */ - def qualifierPart: PackratParser[NamePart] = + lazy val qualifierPart: PackratParser[NamePart] = quotedPart <~ "." ^^ (p => NamePart(p._1, quoted = true)) /** A table reference as the ordered part list the statement wrote -- never split, never @@ -2215,7 +2215,7 @@ trait Parser * * PUBLIC because `FromParser` has `self: Parser with ... =>` and can only see public members. */ - def tableParts: PackratParser[Seq[NamePart]] = + lazy val tableParts: PackratParser[Seq[NamePart]] = rep(qualifierPart) ~ qualifiedName ^^ { case ps ~ nq => ps :+ NamePart(nq._1, nq._2) } // ----------------------------------------------------------------------------------------------- @@ -2260,7 +2260,7 @@ trait Parser * measurement that forced it was an OPTION key (`OPTIONS (a. = 1)`), and a rule justified by "no * regression in existing parsing" cannot hold for option keys and not for table names. */ - def identParts: PackratParser[Seq[NamePart]] = + lazy val identParts: PackratParser[Seq[NamePart]] = (tableParts <~ not(".")) | (ident ^^ (n => Seq(NamePart(n, quoted = false)))) /** `identParts` reduced to what an AST node carries: the name (the LAST part's value, @@ -2275,7 +2275,7 @@ trait Parser * normalised at the ONE site that produces it, so the AST and the render of every bare-spelled * statement stay byte-identical to what they were before this story (AC-5). */ - def identRef: PackratParser[(String, Seq[NamePart])] = + lazy val identRef: PackratParser[(String, Seq[NamePart])] = identParts ^^ { ps => (ps.last.value, if (ps.size == 1 && !ps.head.quoted) Nil else ps) } @@ -2310,7 +2310,7 @@ trait Parser * backtick struct-entry key, and a hyphenated key such as `Content-Type` that `ident`'s charset * could never spell), 0 narrowed. */ - def identName: PackratParser[String] = + lazy val identName: PackratParser[String] = ((qualifiedName ^^ (_._1)) <~ not(".")) | ident /** Kept, and kept FIRST in `SelectParser.field`, `GroupByParser.bucketWithFunction`, @@ -2327,7 +2327,7 @@ trait Parser * `identifierWithValue` inside `identifierWithIntervalFunction` -- see that call site for why * the alternative had to exist for the render to be a fixed point. */ - def quotedIdentifier: PackratParser[Identifier] = + lazy val quotedIdentifier: PackratParser[Identifier] = (Distinct.regex.? ~ quotedQualifiedName ^^ { case d ~ nq => GenericIdentifier(nq._1, None, d.isDefined, quoted = nq._2) }) >> cast @@ -2354,7 +2354,7 @@ trait Parser * expression. Guarding it there would push the operand down to `identifierWithValue` and turn it * back into a string, which is the AD-13 corruption in reverse. */ - def quotedIdentifierUnlessArithmetic: PackratParser[Identifier] = + lazy val quotedIdentifierUnlessArithmetic: PackratParser[Identifier] = quotedIdentifier <~ not(add | subtract | multiply | divide | modulo) /** A quoted lexeme that can ONLY be an identifier: its first part is quoted AND at least one @@ -2379,7 +2379,7 @@ trait Parser * (`equality`, `comparison`). `IN` / `BETWEEN` / `LIKE` take literals only; giving them an * identifier operand would be a new feature, not this fix. */ - def quotedQualifiedIdentifier: PackratParser[Identifier] = + lazy val quotedQualifiedIdentifier: PackratParser[Identifier] = (Distinct.regex.? ~ (quotedPart ~ rep1(nameTailPart) ^^ { case h ~ t => joinNameParts(h :: t) }) ^^ { case d ~ nq => @@ -2390,12 +2390,12 @@ trait Parser * that end in `| identifier` -- the four-alternative operand idiom alone occurs 21 times -- so a * production added later inherits it instead of having to remember it. */ - def identifier: PackratParser[Identifier] = + lazy val identifier: PackratParser[Identifier] = (Distinct.regex.? ~ qualifiedName ^^ { case d ~ nq => GenericIdentifier(nq._1, None, d.isDefined, quoted = nq._2) }) >> cast - def identifierWithTransformation: PackratParser[Identifier] = + lazy val identifierWithTransformation: PackratParser[Identifier] = (mathematicalFunctionWithIdentifier | conversionFunctionWithIdentifier | conditionalFunctionWithIdentifier | @@ -2403,7 +2403,7 @@ trait Parser stringFunctionWithIdentifier | geoFunctionWithIdentifier) >> cast - def identifierWithFunction: PackratParser[Identifier] = + lazy val identifierWithFunction: PackratParser[Identifier] = ((rep1sep( sql_function, start @@ -2453,14 +2453,14 @@ trait Parser * `UNNEST(...) alias`, a JOIN source's alias and the FROM table's alias (`FromParser`) -- so a * backticked table alias and `SELECT a AS "my col"` are the same fix. */ - def alias: PackratParser[Alias] = + lazy val alias: PackratParser[Alias] = Alias.regex.? ~ (quotedNameRegex ^^ (l => Alias(unquoteName(l), quoted = true)) | regexAliasRegex ^^ (b => Alias(b))) ^^ { case _ ~ a => a } /** Retained for `SelectParser.field`'s `(quotedAlias | alias)`, and now a strict subset of * `alias` -- same lexeme, same un-escaping, same `quoted` bit. */ - def quotedAlias: PackratParser[Alias] = + lazy val quotedAlias: PackratParser[Alias] = Alias.regex.? ~ quotedNameRegex ^^ { case _ ~ l => Alias(unquoteName(l), quoted = true) } } diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/SelectParser.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/SelectParser.scala index baece19df..1d7ea11c3 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/SelectParser.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/SelectParser.scala @@ -21,7 +21,7 @@ import app.softnetwork.elastic.sql.query.{Except, Field, Select} trait SelectParser { self: Parser with WhereParser => - def field: PackratParser[Field] = + lazy val field: PackratParser[Field] = // #284: decline the quoted lexeme when an arithmetic operator follows, so // `SELECT `amount` + 1` reaches identifierWithArithmeticExpression below. (quotedIdentifierUnlessArithmetic | @@ -35,12 +35,12 @@ trait SelectParser { Field(i, a) } - def except: PackratParser[Except] = Except.regex ~ start ~ rep1sep(field, separator) ~ end ^^ { - case _ ~ _ ~ e ~ _ => + lazy val except: PackratParser[Except] = + Except.regex ~ start ~ rep1sep(field, separator) ~ end ^^ { case _ ~ _ ~ e ~ _ => Except(e) - } + } - def select: PackratParser[Select] = + lazy val select: PackratParser[Select] = Select.regex ~ rep1sep( field, separator diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/WhereParser.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/WhereParser.scala index c807cfd61..84c9cb001 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/WhereParser.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/WhereParser.scala @@ -88,21 +88,21 @@ import app.softnetwork.elastic.sql.query.{ trait WhereParser { self: Parser with GroupByParser with OrderByParser => - def isNull: PackratParser[Criteria] = (quotedIdentifier | identifier) ~ IS_NULL.regex ^^ { + lazy val isNull: PackratParser[Criteria] = (quotedIdentifier | identifier) ~ IS_NULL.regex ^^ { case i ~ _ => IsNullExpr(i) } - def isNotNull: PackratParser[Criteria] = + lazy val isNotNull: PackratParser[Criteria] = (quotedIdentifier | identifier) ~ IS_NOT_NULL.regex ^^ { case i ~ _ => IsNotNullExpr(i) } - def eq: PackratParser[ComparisonOperator] = EQ.sql ^^ (_ => EQ) + lazy val eq: PackratParser[ComparisonOperator] = EQ.sql ^^ (_ => EQ) - def ne: PackratParser[ComparisonOperator] = NE.sql ^^ (_ => NE) + lazy val ne: PackratParser[ComparisonOperator] = NE.sql ^^ (_ => NE) - def diff: PackratParser[ComparisonOperator] = DIFF.sql ^^ (_ => DIFF) + lazy val diff: PackratParser[ComparisonOperator] = DIFF.sql ^^ (_ => DIFF) /** 🔴 `lazy val`, NOT `def` — and it is a PERFORMANCE contract, not a style choice. * @@ -135,37 +135,37 @@ trait WhereParser { identifierWithValue | identifier - private def equality: PackratParser[GenericExpression] = + private lazy val equality: PackratParser[GenericExpression] = not.? ~ any_identifier ~ (eq | ne | diff) ~ (boolean | quotedQualifiedIdentifier | literal | double | pi | geo_distance | long | any_identifier) ^^ { case n ~ i ~ o ~ v => GenericExpression(i, o, v, n) } - def like: PackratParser[GenericExpression] = + lazy val like: PackratParser[GenericExpression] = any_identifier ~ not.? ~ LIKE.regex ~ literal ^^ { case i ~ n ~ _ ~ v => GenericExpression(i, LIKE, v, n) } - def rlike: PackratParser[GenericExpression] = + lazy val rlike: PackratParser[GenericExpression] = any_identifier ~ not.? ~ RLIKE.regex ~ literal ^^ { case i ~ n ~ _ ~ v => GenericExpression(i, RLIKE, v, n) } - def ge: PackratParser[ComparisonOperator] = GE.sql ^^ (_ => GE) + lazy val ge: PackratParser[ComparisonOperator] = GE.sql ^^ (_ => GE) - def gt: PackratParser[ComparisonOperator] = GT.sql ^^ (_ => GT) + lazy val gt: PackratParser[ComparisonOperator] = GT.sql ^^ (_ => GT) - def le: PackratParser[ComparisonOperator] = LE.sql ^^ (_ => LE) + lazy val le: PackratParser[ComparisonOperator] = LE.sql ^^ (_ => LE) - def lt: PackratParser[ComparisonOperator] = LT.sql ^^ (_ => LT) + lazy val lt: PackratParser[ComparisonOperator] = LT.sql ^^ (_ => LT) - private def comparison: PackratParser[GenericExpression] = + private lazy val comparison: PackratParser[GenericExpression] = not.? ~ any_identifier ~ (ge | gt | le | lt) ~ (double | pi | random | geo_distance | long | quotedQualifiedIdentifier | literal | any_identifier) ^^ { case n ~ i ~ o ~ v => GenericExpression(i, o, v, n) } - def in: PackratParser[ExpressionOperator] = IN.regex ^^ (_ => IN) + lazy val in: PackratParser[ExpressionOperator] = IN.regex ^^ (_ => IN) - private def inLiteral: PackratParser[Criteria] = + private lazy val inLiteral: PackratParser[Criteria] = any_identifier ~ not.? ~ in ~ start ~ rep1sep(literal, separator) ~ end ^^ { case i ~ n ~ _ ~ _ ~ v ~ _ => InExpr( @@ -175,7 +175,7 @@ trait WhereParser { ) } - private def inDoubles: PackratParser[Criteria] = + private lazy val inDoubles: PackratParser[Criteria] = any_identifier ~ not.? ~ in ~ start ~ rep1sep( double, separator @@ -187,7 +187,7 @@ trait WhereParser { ) } - private def inLongs: PackratParser[Criteria] = + private lazy val inLongs: PackratParser[Criteria] = any_identifier ~ not.? ~ in ~ start ~ rep1sep( long, separator @@ -199,27 +199,27 @@ trait WhereParser { ) } - def between: PackratParser[Criteria] = + lazy val between: PackratParser[Criteria] = any_identifier ~ not.? ~ BETWEEN.regex ~ literal ~ and ~ literal ^^ { case i ~ n ~ _ ~ from ~ _ ~ to => BetweenExpr(i, LiteralFromTo(from, to), n) } - def betweenLongs: PackratParser[Criteria] = + lazy val betweenLongs: PackratParser[Criteria] = any_identifier ~ not.? ~ BETWEEN.regex ~ long ~ and ~ long ^^ { case i ~ n ~ _ ~ from ~ _ ~ to => BetweenExpr(i, LongFromTo(from, to), n) } - def betweenDoubles: PackratParser[Criteria] = + lazy val betweenDoubles: PackratParser[Criteria] = any_identifier ~ not.? ~ BETWEEN.regex ~ double ~ and ~ double ^^ { case i ~ n ~ _ ~ from ~ _ ~ to => BetweenExpr(i, DoubleFromTo(from, to), n) } - def betweenIdentifiers: PackratParser[Criteria] = + lazy val betweenIdentifiers: PackratParser[Criteria] = any_identifier ~ not.? ~ BETWEEN.regex ~ any_identifier ~ and ~ any_identifier ^^ { case i ~ n ~ _ ~ from ~ _ ~ to => BetweenExpr(i, IdentifierFromTo(from, to), n) } - def betweenDistances: PackratParser[Criteria] = + lazy val betweenDistances: PackratParser[Criteria] = distance_identifier ~ not.? ~ BETWEEN.regex ~ (geo_distance | long) ~ and ~ (geo_distance | long) ^^ { case i ~ n ~ _ ~ from ~ _ ~ to => BetweenExpr( @@ -243,7 +243,7 @@ trait WhereParser { DistanceCriteria(d, o, g) }*/ - def matchCriteria: PackratParser[MultiMatchCriteria] = + lazy val matchCriteria: PackratParser[MultiMatchCriteria] = MATCH.regex ~ start ~ rep1sep( any_identifier, separator @@ -251,13 +251,13 @@ trait WhereParser { MultiMatchCriteria(i, l) } - def and: PackratParser[PredicateOperator] = AND.regex ^^ (_ => AND) + lazy val and: PackratParser[PredicateOperator] = AND.regex ^^ (_ => AND) - def or: PackratParser[PredicateOperator] = OR.regex ^^ (_ => OR) + lazy val or: PackratParser[PredicateOperator] = OR.regex ^^ (_ => OR) - def not: PackratParser[NOT.type] = NOT.regex ^^ (_ => NOT) + lazy val not: PackratParser[NOT.type] = NOT.regex ^^ (_ => NOT) - def logical_criteria: PackratParser[Criteria] = + lazy val logical_criteria: PackratParser[Criteria] = (is_null | is_notnull) ^^ { case ConditionalFunctionAsCriteria(c) => c } @@ -272,23 +272,24 @@ trait WhereParser { * rejected. With `derivedTableBodyInner` the failure inside the parentheses is a plain `Failure` * and the fall-through keeps the parenthesised-expression reading. Pinned by the neighbour test. */ - private def subqueryBody: PackratParser[DqlStatement] = start ~> derivedTableBodyInner <~ end + private lazy val subqueryBody: PackratParser[DqlStatement] = start ~> derivedTableBodyInner <~ end - private def comparisonOp: PackratParser[ComparisonOperator] = eq | ne | diff | ge | gt | le | lt + private lazy val comparisonOp: PackratParser[ComparisonOperator] = + eq | ne | diff | ge | gt | le | lt /** `SOME` is canonicalised to `ANY` here (ANSI synonyms), so the AST carries one spelling and `x * > SOME (S)` renders — and re-parses — as `x > ANY (S)`. */ - private def quantifier: PackratParser[Quantifier] = + private lazy val quantifier: PackratParser[Quantifier] = ANY.regex ^^ (_ => ANY) | SOME.regex ^^ (_ => ANY) | ALL.regex ^^ (_ => ALL) - private def existsSubquery: PackratParser[Criteria] = + private lazy val existsSubquery: PackratParser[Criteria] = not.? ~ (EXISTS.regex ~> subqueryBody) ^^ { case n ~ q => ExistsSubquery(q, n) } - private def inSubquery: PackratParser[Criteria] = + private lazy val inSubquery: PackratParser[Criteria] = any_identifier ~ not.? ~ in ~ subqueryBody ^^ { case i ~ n ~ _ ~ q => InSubquery(i, q, n) } - private def scalarSubquery: PackratParser[Criteria] = + private lazy val scalarSubquery: PackratParser[Criteria] = not.? ~ any_identifier ~ comparisonOp ~ subqueryBody ^^ { case n ~ i ~ o ~ q => ScalarSubquery(i, o, q, n) } @@ -308,7 +309,7 @@ trait WhereParser { * production fails at `subqueryBody` and the alternation falls through to `equality` with the * column reading intact. */ - private def quantifiedSubquery: PackratParser[Criteria] = + private lazy val quantifiedSubquery: PackratParser[Criteria] = not.? ~ any_identifier ~ comparisonOp ~ quantifier ~ subqueryBody ^^ { case n ~ i ~ EQ ~ ANY ~ q => InSubquery(i, q, n) case n ~ i ~ (NE | DIFF) ~ ALL ~ q => @@ -356,7 +357,7 @@ trait WhereParser { matchCriteria | logical_criteria) ^^ (c => c) - def predicate: PackratParser[Predicate] = criteria ~ (and | or) ~ not.? ~ criteria ^^ { + lazy val predicate: PackratParser[Predicate] = criteria ~ (and | or) ~ not.? ~ criteria ^^ { case l ~ o ~ n ~ r => Predicate(l, o, r, n) } @@ -366,7 +367,7 @@ trait WhereParser { * exactly the token shape it sees at top level and builds the same tree (including * `Predicate.group = true`, which is what renders the parentheses back). */ - private def relationGroup: PackratParser[List[Token]] = + private lazy val relationGroup: PackratParser[List[Token]] = start ~ relationTokens ~ end ^^ { case s ~ ts ~ e => (s :: ts) :+ e } /** The token stream inside a relation predicate's parentheses. @@ -393,7 +394,7 @@ trait WhereParser { * tree the same expression produces at top level"* (`ParserTotalitySpec`): if one alternation * gains a shape the other cannot reach, the two trees stop being equal and it fails. */ - private def relationTokens: PackratParser[List[Token]] = + private lazy val relationTokens: PackratParser[List[Token]] = rep1( relationGroup | allPredicate ^^ (c => List(c: Token)) | @@ -443,39 +444,39 @@ trait WhereParser { * happily as `CHILD(a = 1) AND b = 2`. An opening parenthesis now commits to `childPredicate`, * which requires the closing one. */ - def nestedCriteria: PackratParser[ElasticRelation] = + lazy val nestedCriteria: PackratParser[ElasticRelation] = Nested.regex ~> criteria ^^ { c => ElasticNested(c, None, fromCriteria = false) } - def nestedPredicate: PackratParser[ElasticRelation] = + lazy val nestedPredicate: PackratParser[ElasticRelation] = Nested.regex ~> relationCriteria("NESTED") ^^ { c => ElasticNested(c, None, fromCriteria = false) } - def childCriteria: PackratParser[ElasticRelation] = Child.regex ~> criteria ^^ { c => + lazy val childCriteria: PackratParser[ElasticRelation] = Child.regex ~> criteria ^^ { c => ElasticChild(c) } - def childPredicate: PackratParser[ElasticRelation] = + lazy val childPredicate: PackratParser[ElasticRelation] = Child.regex ~> relationCriteria("CHILD") ^^ { c => ElasticChild(c) } - def parentCriteria: PackratParser[ElasticRelation] = + lazy val parentCriteria: PackratParser[ElasticRelation] = Parent.regex ~> criteria ^^ { c => ElasticParent(c) } - def parentPredicate: PackratParser[ElasticRelation] = + lazy val parentPredicate: PackratParser[ElasticRelation] = Parent.regex ~> relationCriteria("PARENT") ^^ { c => ElasticParent(c) } - private def allPredicate: PackratParser[Criteria] = + private lazy val allPredicate: PackratParser[Criteria] = nestedPredicate | childPredicate | parentPredicate | predicate - private def allCriteria: PackratParser[Token] = + private lazy val allCriteria: PackratParser[Token] = nestedCriteria | childCriteria | parentCriteria | criteria /** The token stream of a WHERE / HAVING / CASE-WHEN / JOIN-ON condition. @@ -509,7 +510,7 @@ trait WhereParser { * Written as a plain `Parser[List[Token]]` over the existing item parser: PackratParser memoises * the items exactly as before, and the depth is a local of one scan, never parser state. */ - def whereCriteria: PackratParser[List[Token]] = new self.Parser[List[Token]] { + lazy val whereCriteria: PackratParser[List[Token]] = new self.Parser[List[Token]] { // The SAME alternation, in the SAME order, as the `rep1` this replaces. private val item: self.Parser[Token] = @@ -532,7 +533,7 @@ trait WhereParser { override def apply(in: Input): ParseResult[List[Token]] = scan(in, 0, Nil) } - def where: PackratParser[Where] = + lazy val where: PackratParser[Where] = Where.regex ~ whereCriteria >> { case _ ~ rawTokens => // `err`, not `throw` and not `failure` (#250, same reasoning as `alterTable`, // Parser.scala:713-729). `Error.append` returns `this` (scala-parser-combinators 1.1.2, diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/aggregate/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/aggregate/package.scala index 58de7d2cc..8bff9ed15 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/aggregate/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/aggregate/package.scala @@ -25,48 +25,48 @@ package object aggregate { trait AggregateParser { self: Parser with OrderByParser with LimitParser => - def count: PackratParser[AggregateFunction] = COUNT.regex ^^ (_ => COUNT) + lazy val count: PackratParser[AggregateFunction] = COUNT.regex ^^ (_ => COUNT) - def min: PackratParser[AggregateFunction] = MIN.regex ^^ (_ => MIN) + lazy val min: PackratParser[AggregateFunction] = MIN.regex ^^ (_ => MIN) - def max: PackratParser[AggregateFunction] = MAX.regex ^^ (_ => MAX) + lazy val max: PackratParser[AggregateFunction] = MAX.regex ^^ (_ => MAX) - def avg: PackratParser[AggregateFunction] = AVG.regex ^^ (_ => AVG) + lazy val avg: PackratParser[AggregateFunction] = AVG.regex ^^ (_ => AVG) - def sum: PackratParser[AggregateFunction] = SUM.regex ^^ (_ => SUM) + lazy val sum: PackratParser[AggregateFunction] = SUM.regex ^^ (_ => SUM) - def stddev: PackratParser[AggregateFunction] = STDDEV.regex ^^ (_ => STDDEV) + lazy val stddev: PackratParser[AggregateFunction] = STDDEV.regex ^^ (_ => STDDEV) - def stddev_pop: PackratParser[AggregateFunction] = STDDEV_POP.regex ^^ (_ => STDDEV_POP) + lazy val stddev_pop: PackratParser[AggregateFunction] = STDDEV_POP.regex ^^ (_ => STDDEV_POP) - def stddev_samp: PackratParser[AggregateFunction] = STDDEV_SAMP.regex ^^ (_ => STDDEV_SAMP) + lazy val stddev_samp: PackratParser[AggregateFunction] = STDDEV_SAMP.regex ^^ (_ => STDDEV_SAMP) - def variance: PackratParser[AggregateFunction] = VARIANCE.regex ^^ (_ => VARIANCE) + lazy val variance: PackratParser[AggregateFunction] = VARIANCE.regex ^^ (_ => VARIANCE) - def var_pop: PackratParser[AggregateFunction] = VAR_POP.regex ^^ (_ => VAR_POP) + lazy val var_pop: PackratParser[AggregateFunction] = VAR_POP.regex ^^ (_ => VAR_POP) - def var_samp: PackratParser[AggregateFunction] = VAR_SAMP.regex ^^ (_ => VAR_SAMP) + lazy val var_samp: PackratParser[AggregateFunction] = VAR_SAMP.regex ^^ (_ => VAR_SAMP) // Longest-prefix alternation: STDDEV_POP / STDDEV_SAMP / VAR_POP / VAR_SAMP must be tried // before the bare STDDEV / VARIANCE so the suffixed forms are not shadowed. - def aggregate_function: PackratParser[AggregateFunction] = + lazy val aggregate_function: PackratParser[AggregateFunction] = count | min | max | avg | sum | stddev_pop | stddev_samp | stddev | var_pop | var_samp | variance - def aggWithFunction: PackratParser[Identifier] = + lazy val aggWithFunction: PackratParser[Identifier] = identifierWithArithmeticExpression | identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier - def identifierWithAggregation: PackratParser[Identifier] = + lazy val identifierWithAggregation: PackratParser[Identifier] = aggregate_function ~ start ~ aggWithFunction ~ end ^^ { case a ~ _ ~ i ~ _ => i.withFunctions(a +: i.functions) } - def partition_by: PackratParser[Seq[Identifier]] = + lazy val partition_by: PackratParser[Seq[Identifier]] = PARTITION_BY.regex ~> rep1sep(identifierWithTransformation | identifier, separator) private[this] def over: Parser[(Seq[Identifier], Option[OrderBy], Option[Limit])] = @@ -84,7 +84,7 @@ package object aggregate { } } - def first_value: PackratParser[WindowFunction] = + lazy val first_value: PackratParser[WindowFunction] = FIRST_VALUE.regex ~ window_function() ^^ { case _ ~ top => FirstValue( top._1, @@ -93,7 +93,7 @@ package object aggregate { ) } - def last_value: PackratParser[WindowFunction] = + lazy val last_value: PackratParser[WindowFunction] = LAST_VALUE.regex ~ window_function() ^^ { case _ ~ top => LastValue( top._1, @@ -102,7 +102,7 @@ package object aggregate { ) } - def array_agg: PackratParser[WindowFunction] = + lazy val array_agg: PackratParser[WindowFunction] = ARRAY_AGG.regex ~ window_function() ^^ { case _ ~ top => ArrayAgg( top._1, @@ -117,32 +117,32 @@ package object aggregate { * `CountAgg`'s identifier and, via `identifierWithWindowFunction`, the outer identifier -- and * from then on it is indistinguishable from a `COUNT(*)` the user typed. */ - def count_agg: PackratParser[WindowFunction] = + lazy val count_agg: PackratParser[WindowFunction] = count ~ window_function(aggWithFunction) ^^ { case _ ~ top => CountAgg(CountAgg.rowCountingOperand(top._1), top._2) } - def min_agg: PackratParser[WindowFunction] = + lazy val min_agg: PackratParser[WindowFunction] = min ~ window_function(aggWithFunction) ^^ { case _ ~ top => MinAgg(top._1, top._2) } - def max_agg: PackratParser[WindowFunction] = + lazy val max_agg: PackratParser[WindowFunction] = max ~ window_function(aggWithFunction) ^^ { case _ ~ top => MaxAgg(top._1, top._2) } - def avg_agg: PackratParser[WindowFunction] = + lazy val avg_agg: PackratParser[WindowFunction] = avg ~ window_function(aggWithFunction) ^^ { case _ ~ top => AvgAgg(top._1, top._2) } - def sum_agg: PackratParser[WindowFunction] = + lazy val sum_agg: PackratParser[WindowFunction] = sum ~ window_function(aggWithFunction) ^^ { case _ ~ top => SumAgg(top._1, top._2) } - def stddev_agg: PackratParser[WindowFunction] = + lazy val stddev_agg: PackratParser[WindowFunction] = (stddev_pop | stddev_samp | stddev) ~ window_function(aggWithFunction) ^^ { case fn ~ top => val kind = fn match { case STDDEV_POP => ExtendedStatsKind.StddevPop @@ -152,7 +152,7 @@ package object aggregate { ExtendedStatsAgg(top._1, kind, top._2) } - def variance_agg: PackratParser[WindowFunction] = + lazy val variance_agg: PackratParser[WindowFunction] = (var_pop | var_samp | variance) ~ window_function(aggWithFunction) ^^ { case fn ~ top => val kind = fn match { case VAR_POP => ExtendedStatsKind.VarPop @@ -162,18 +162,18 @@ package object aggregate { ExtendedStatsAgg(top._1, kind, top._2) } - def percentile_cont: PackratParser[AggregateFunction] = + lazy val percentile_cont: PackratParser[AggregateFunction] = PERCENTILE_CONT.regex ^^ (_ => PERCENTILE_CONT) - def percentile_disc: PackratParser[AggregateFunction] = + lazy val percentile_disc: PackratParser[AggregateFunction] = PERCENTILE_DISC.regex ^^ (_ => PERCENTILE_DISC) // Numeric percentile literal in [0,1] — accepts decimals (0.99) and whole 0/1. - private[this] def percentile_literal: PackratParser[Double] = + private[this] lazy val percentile_literal: PackratParser[Double] = (double ^^ (_.value)) | (long ^^ (_.value.toDouble)) // (col, p) shorthand OR (p) - private[this] def percentile_args: PackratParser[(Option[Identifier], Double)] = + private[this] lazy val percentile_args: PackratParser[(Option[Identifier], Double)] = (start ~> aggWithFunction ~ (separator ~> percentile_literal) <~ end ^^ { case id ~ p => (Some(id), p) }) | @@ -182,7 +182,7 @@ package object aggregate { // WITHIN GROUP ( ORDER BY ) -> value column(s). A percentile takes a // SINGLE value column; a multi-column ORDER BY is rejected in `percentile_agg` // (the full sort list is surfaced here so the guard can count columns). - private[this] def percentile_within_group: PackratParser[Seq[Identifier]] = + private[this] lazy val percentile_within_group: PackratParser[Seq[Identifier]] = """(?i)\bwithin\b""".r ~> """(?i)\bgroup\b""".r ~> start ~> orderBy <~ end ^^ (_.sorts.map( _.field )) @@ -199,7 +199,7 @@ package object aggregate { * The `^?` guard rejects (parse failure) when there is no value column, more than one source, * or `p` outside `[0,1]`. */ - def percentile_agg: PackratParser[WindowFunction] = + lazy val percentile_agg: PackratParser[WindowFunction] = ((percentile_cont | percentile_disc) ~ percentile_args ~ percentile_within_group.? ~ over.?) ^? ({ case fn ~ ((shorthandCol, p)) ~ wg ~ ov if { @@ -233,22 +233,22 @@ package object aggregate { (pb.getOrElse(Seq.empty), ob, l) } - def row_number: PackratParser[WindowFunction] = + lazy val row_number: PackratParser[WindowFunction] = ROW_NUMBER.regex ~ start ~ end ~ ranking_over ^^ { case _ ~ _ ~ _ ~ ((pb, ob, l)) => RowNumber(partitionBy = pb, orderBy = Some(ob), limit = l) } - def rank: PackratParser[WindowFunction] = + lazy val rank: PackratParser[WindowFunction] = RANK.regex ~ start ~ end ~ ranking_over ^^ { case _ ~ _ ~ _ ~ ((pb, ob, l)) => Ranking(partitionBy = pb, orderBy = Some(ob), limit = l) } - def dense_rank: PackratParser[WindowFunction] = + lazy val dense_rank: PackratParser[WindowFunction] = DENSE_RANK.regex ~ start ~ end ~ ranking_over ^^ { case _ ~ _ ~ _ ~ ((pb, ob, l)) => DenseRank(partitionBy = pb, orderBy = Some(ob), limit = l) } - def identifierWithWindowFunction: PackratParser[Identifier] = + lazy val identifierWithWindowFunction: PackratParser[Identifier] = (first_value | last_value | array_agg | count_agg | min_agg | max_agg | avg_agg | sum_agg | stddev_agg | variance_agg | percentile_agg | row_number | rank | dense_rank) ^^ { th => diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/cond/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/cond/package.scala index cfb152578..ee82b7480 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/cond/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/cond/package.scala @@ -45,17 +45,17 @@ package object cond { trait CondParser { self: Parser with WhereParser => - def is_null: PackratParser[ConditionalFunction[_]] = + lazy val is_null: PackratParser[ConditionalFunction[_]] = "(?i)isnull".r ~ start ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) ~ end ^^ { case _ ~ _ ~ i ~ _ => IsNull(i) } - def is_notnull: PackratParser[ConditionalFunction[_]] = + lazy val is_notnull: PackratParser[ConditionalFunction[_]] = "(?i)isnotnull".r ~ start ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) ~ end ^^ { case _ ~ _ ~ i ~ _ => IsNotNull(i) } - def coalesce: PackratParser[Coalesce] = + lazy val coalesce: PackratParser[Coalesce] = Coalesce.regex ~ start ~ rep1sep( valueExpr, separator @@ -63,30 +63,30 @@ package object cond { Coalesce(ids) } - def nullif: PackratParser[NullIf] = + lazy val nullif: PackratParser[NullIf] = NullIf.regex ~ start ~ valueExpr ~ separator ~ valueExpr ~ end ^^ { case _ ~ _ ~ id1 ~ _ ~ id2 ~ _ => NullIf(id1, id2) } - def greatest: PackratParser[Greatest] = + lazy val greatest: PackratParser[Greatest] = Greatest.regex ~ start ~ rep1sep(valueExpr, separator) ~ end ^^ { case _ ~ _ ~ vs ~ _ => Greatest(vs) } - def least: PackratParser[Least] = + lazy val least: PackratParser[Least] = Least.regex ~ start ~ rep1sep(valueExpr, separator) ~ end ^^ { case _ ~ _ ~ vs ~ _ => Least(vs) } - def start_case: PackratParser[StartCase.type] = Case.regex ^^ (_ => StartCase) + lazy val start_case: PackratParser[StartCase.type] = Case.regex ^^ (_ => StartCase) - def when_case: PackratParser[WhenCase.type] = WHEN.regex ^^ (_ => WhenCase) + lazy val when_case: PackratParser[WhenCase.type] = WHEN.regex ^^ (_ => WhenCase) - def then_case: PackratParser[ThenCase.type] = THEN.regex ^^ (_ => ThenCase) + lazy val then_case: PackratParser[ThenCase.type] = THEN.regex ^^ (_ => ThenCase) - def else_case: PackratParser[ELSE.type] = ELSE.regex ^^ (_ => ELSE) + lazy val else_case: PackratParser[ELSE.type] = ELSE.regex ^^ (_ => ELSE) - def end_case: PackratParser[EndCase.type] = END.regex ^^ (_ => EndCase) + lazy val end_case: PackratParser[EndCase.type] = END.regex ^^ (_ => EndCase) def case_condition: Parser[(PainlessScript, PainlessScript)] = when_case ~ (whereCriteria | valueExpr) ~ then_case.? ~ valueExpr >> { case _ ~ c ~ _ ~ r => @@ -110,7 +110,7 @@ package object cond { def case_else: Parser[PainlessScript] = else_case ~ valueExpr ^^ { case _ ~ r => r } - def case_when: PackratParser[Case] = + lazy val case_when: PackratParser[Case] = start_case ~ valueExpr.? ~ rep1(case_condition) ~ case_else.? ~ end_case ^^ { case _ ~ e ~ c ~ r ~ _ => Case(e, c, r) } @@ -119,10 +119,10 @@ package object cond { Identifier(cw) } - def conditional_function: PackratParser[FunctionWithIdentifier] = + lazy val conditional_function: PackratParser[FunctionWithIdentifier] = is_null | is_notnull | coalesce | nullif | greatest | least - def conditionalFunctionWithIdentifier: PackratParser[Identifier] = + lazy val conditionalFunctionWithIdentifier: PackratParser[Identifier] = conditional_function ^^ { t => t.identifier.withFunctions(t +: t.identifier.functions) } | case_when_identifier diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/convert/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/convert/package.scala index a9b077841..4d5a3b486 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/convert/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/convert/package.scala @@ -25,7 +25,7 @@ package object convert { trait ConvertParser { self: Parser => - def cast_identifier: PackratParser[Identifier] = + lazy val cast_identifier: PackratParser[Identifier] = Cast.regex ~ start ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | @@ -33,7 +33,7 @@ package object convert { i.withFunctions(Cast(i, targetType = t, as = as.isDefined) +: i.functions) } - def try_cast_identifier: PackratParser[Identifier] = + lazy val try_cast_identifier: PackratParser[Identifier] = TryCast.regex ~ start ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | @@ -43,7 +43,7 @@ package object convert { ) } - def convert_identifier: PackratParser[Identifier] = + lazy val convert_identifier: PackratParser[Identifier] = Convert.regex ~ start ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | @@ -65,7 +65,7 @@ package object convert { * 'utf8'`. Taking only the bare form would half-support a spelling the lead confirmed we keep * (OQ-3), and `ident` cannot express a quoted one. */ - def convert_using_identifier: PackratParser[Identifier] = + lazy val convert_using_identifier: PackratParser[Identifier] = Convert.regex ~ start ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | @@ -73,7 +73,7 @@ package object convert { i.withFunctions(Convert(i, targetType = SQLTypes.Varchar) +: i.functions) } - def convert_transact_sql_identifier: PackratParser[Identifier] = + lazy val convert_transact_sql_identifier: PackratParser[Identifier] = Convert.regex ~ start ~> sql_type ~ separator ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | @@ -88,7 +88,7 @@ package object convert { i.withFunctions(CastOperator(i, targetType = t) +: i.functions) } - def conversionFunctionWithIdentifier: PackratParser[Identifier] = + lazy val conversionFunctionWithIdentifier: PackratParser[Identifier] = (cast_identifier | try_cast_identifier | convert_identifier | diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/geo/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/geo/package.scala index 752f8db2e..f778e0c55 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/geo/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/geo/package.scala @@ -24,40 +24,41 @@ package object geo { trait GeoParser { self: Parser => - def point: PackratParser[Point] = + lazy val point: PackratParser[Point] = Point.regex ~> start ~> double ~ separator ~ double <~ end ^^ { case lat ~ _ ~ lon => Point(lat, lon) } - def pointOrIdentifier: PackratParser[Either[Identifier, Point]] = + lazy val pointOrIdentifier: PackratParser[Either[Identifier, Point]] = (point | identifier) ^^ { case id: Identifier => Left(id) case p: Point => Right(p) } - def distance: PackratParser[Distance] = + lazy val distance: PackratParser[Distance] = Distance.regex ~> start ~> pointOrIdentifier ~ separator ~ pointOrIdentifier <~ end ^^ { case from ~ _ ~ to => Distance(from, to) } - def kilometers: PackratParser[DistanceUnit] = Kilometers.regex ^^ (_ => Kilometers) - def meters: PackratParser[DistanceUnit] = Meters.regex ^^ (_ => Meters) - def centimeters: PackratParser[DistanceUnit] = Centimeters.regex ^^ (_ => Centimeters) - def millimeters: PackratParser[DistanceUnit] = Millimeters.regex ^^ (_ => Millimeters) - def miles: PackratParser[DistanceUnit] = Miles.regex ^^ (_ => Miles) - def yards: PackratParser[DistanceUnit] = Yards.regex ^^ (_ => Yards) - def feet: PackratParser[DistanceUnit] = Feet.regex ^^ (_ => Feet) - def inches: PackratParser[DistanceUnit] = Inches.regex ^^ (_ => Inches) - def nauticalMiles: PackratParser[DistanceUnit] = NauticalMiles.regex ^^ (_ => NauticalMiles) - - def distance_unit: PackratParser[DistanceUnit] = + lazy val kilometers: PackratParser[DistanceUnit] = Kilometers.regex ^^ (_ => Kilometers) + lazy val meters: PackratParser[DistanceUnit] = Meters.regex ^^ (_ => Meters) + lazy val centimeters: PackratParser[DistanceUnit] = Centimeters.regex ^^ (_ => Centimeters) + lazy val millimeters: PackratParser[DistanceUnit] = Millimeters.regex ^^ (_ => Millimeters) + lazy val miles: PackratParser[DistanceUnit] = Miles.regex ^^ (_ => Miles) + lazy val yards: PackratParser[DistanceUnit] = Yards.regex ^^ (_ => Yards) + lazy val feet: PackratParser[DistanceUnit] = Feet.regex ^^ (_ => Feet) + lazy val inches: PackratParser[DistanceUnit] = Inches.regex ^^ (_ => Inches) + lazy val nauticalMiles: PackratParser[DistanceUnit] = + NauticalMiles.regex ^^ (_ => NauticalMiles) + + lazy val distance_unit: PackratParser[DistanceUnit] = kilometers | meters | centimeters | millimeters | miles | yards | feet | inches | nauticalMiles - def geo_distance: PackratParser[GeoDistance] = + lazy val geo_distance: PackratParser[GeoDistance] = long ~ distance_unit ^^ { case value ~ unit => GeoDistance(value, unit) } - def distance_identifier: PackratParser[Identifier] = distance ^^ functionAsIdentifier + lazy val distance_identifier: PackratParser[Identifier] = distance ^^ functionAsIdentifier - def geoFunctionWithIdentifier: PackratParser[Identifier] = distance_identifier + lazy val geoFunctionWithIdentifier: PackratParser[Identifier] = distance_identifier } } diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/math/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/math/package.scala index 7be5474c9..10f7ba9c5 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/math/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/math/package.scala @@ -48,76 +48,76 @@ package object math { trait MathParser { self: Parser => - private[this] def abs: PackratParser[MathOp] = Abs.regex ^^ (_ => Abs) + private[this] lazy val abs: PackratParser[MathOp] = Abs.regex ^^ (_ => Abs) - private[this] def ceil: PackratParser[MathOp] = Ceil.regex ^^ (_ => Ceil) + private[this] lazy val ceil: PackratParser[MathOp] = Ceil.regex ^^ (_ => Ceil) - private[this] def floor: PackratParser[MathOp] = Floor.regex ^^ (_ => Floor) + private[this] lazy val floor: PackratParser[MathOp] = Floor.regex ^^ (_ => Floor) - private[this] def exp: PackratParser[MathOp] = Exp.regex ^^ (_ => Exp) + private[this] lazy val exp: PackratParser[MathOp] = Exp.regex ^^ (_ => Exp) - private[this] def sqrt: PackratParser[MathOp] = Sqrt.regex ^^ (_ => Sqrt) + private[this] lazy val sqrt: PackratParser[MathOp] = Sqrt.regex ^^ (_ => Sqrt) - private[this] def log: PackratParser[MathOp] = Log.regex ^^ (_ => Log) + private[this] lazy val log: PackratParser[MathOp] = Log.regex ^^ (_ => Log) - private[this] def log10: PackratParser[MathOp] = Log10.regex ^^ (_ => Log10) + private[this] lazy val log10: PackratParser[MathOp] = Log10.regex ^^ (_ => Log10) - def arithmetic_function: PackratParser[MathematicalFunction] = + lazy val arithmetic_function: PackratParser[MathematicalFunction] = (abs | ceil | exp | floor | log | log10 | sqrt) ~ start ~ valueExpr ~ end ^^ { case op ~ _ ~ v ~ _ => MathematicalFunctionWithOp(op, v) } - private[this] def sin: PackratParser[Trigonometric] = Sin.regex ^^ (_ => Sin) + private[this] lazy val sin: PackratParser[Trigonometric] = Sin.regex ^^ (_ => Sin) - private[this] def asin: PackratParser[Trigonometric] = Asin.regex ^^ (_ => Asin) + private[this] lazy val asin: PackratParser[Trigonometric] = Asin.regex ^^ (_ => Asin) - private[this] def cos: PackratParser[Trigonometric] = Cos.regex ^^ (_ => Cos) + private[this] lazy val cos: PackratParser[Trigonometric] = Cos.regex ^^ (_ => Cos) - private[this] def acos: PackratParser[Trigonometric] = Acos.regex ^^ (_ => Acos) + private[this] lazy val acos: PackratParser[Trigonometric] = Acos.regex ^^ (_ => Acos) - private[this] def tan: PackratParser[Trigonometric] = Tan.regex ^^ (_ => Tan) + private[this] lazy val tan: PackratParser[Trigonometric] = Tan.regex ^^ (_ => Tan) - private[this] def atan: PackratParser[Trigonometric] = Atan.regex ^^ (_ => Atan) + private[this] lazy val atan: PackratParser[Trigonometric] = Atan.regex ^^ (_ => Atan) - private[this] def atan2: PackratParser[Trigonometric] = Atan2.regex ^^ (_ => Atan2) + private[this] lazy val atan2: PackratParser[Trigonometric] = Atan2.regex ^^ (_ => Atan2) - private[this] def degrees: PackratParser[Trigonometric] = Degrees.regex ^^ (_ => Degrees) + private[this] lazy val degrees: PackratParser[Trigonometric] = Degrees.regex ^^ (_ => Degrees) - private[this] def radians: PackratParser[Trigonometric] = Radians.regex ^^ (_ => Radians) + private[this] lazy val radians: PackratParser[Trigonometric] = Radians.regex ^^ (_ => Radians) - def atan2_function: PackratParser[MathematicalFunction] = + lazy val atan2_function: PackratParser[MathematicalFunction] = atan2 ~ start ~ (double | valueExpr) ~ separator ~ (double | valueExpr) ~ end ^^ { case _ ~ _ ~ y ~ _ ~ x ~ _ => Atan2(y, x) } - def trigonometric_function: PackratParser[MathematicalFunction] = + lazy val trigonometric_function: PackratParser[MathematicalFunction] = atan2_function | ((sin | asin | cos | acos | tan | atan | degrees | radians) ~ start ~ valueExpr ~ end ^^ { case op ~ _ ~ v ~ _ => MathematicalFunctionWithOp(op, v) }) - private[this] def round: PackratParser[MathOp] = Round.regex ^^ (_ => Round) + private[this] lazy val round: PackratParser[MathOp] = Round.regex ^^ (_ => Round) - def round_function: PackratParser[MathematicalFunction] = + lazy val round_function: PackratParser[MathematicalFunction] = round ~ start ~ valueExpr ~ separator.? ~ long.? ~ end ^^ { case _ ~ _ ~ v ~ _ ~ s ~ _ => Round(v, s.map(_.value.toInt)) } - private[this] def pow: PackratParser[MathOp] = Pow.regex ^^ (_ => Pow) + private[this] lazy val pow: PackratParser[MathOp] = Pow.regex ^^ (_ => Pow) - def pow_function: PackratParser[MathematicalFunction] = + lazy val pow_function: PackratParser[MathematicalFunction] = pow ~ start ~ valueExpr ~ separator ~ long ~ end ^^ { case _ ~ _ ~ v1 ~ _ ~ e ~ _ => Pow(v1, e.value.toInt) } - private[this] def sign: PackratParser[MathOp] = Sign.regex ^^ (_ => Sign) + private[this] lazy val sign: PackratParser[MathOp] = Sign.regex ^^ (_ => Sign) - def sign_function: PackratParser[MathematicalFunction] = + lazy val sign_function: PackratParser[MathematicalFunction] = sign ~ start ~ valueExpr ~ end ^^ { case _ ~ _ ~ v ~ _ => Sign(v) } - def mathematical_function: PackratParser[MathematicalFunction] = + lazy val mathematical_function: PackratParser[MathematicalFunction] = arithmetic_function | trigonometric_function | round_function | pow_function | sign_function - def mathematicalFunctionWithIdentifier: PackratParser[Identifier] = + lazy val mathematicalFunctionWithIdentifier: PackratParser[Identifier] = mathematical_function ^^ functionAsIdentifier } diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/string/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/string/package.scala index 2268fb5ed..650821b14 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/string/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/string/package.scala @@ -27,47 +27,47 @@ package object string { trait StringParser { self: Parser => - def concat: PackratParser[StringFunction[SQLVarchar]] = + lazy val concat: PackratParser[StringFunction[SQLVarchar]] = Concat.regex ~ start ~ rep1sep(valueExpr, separator) ~ end ^^ { case _ ~ _ ~ vs ~ _ => Concat(vs) } - def substr: PackratParser[StringFunction[SQLVarchar]] = + lazy val substr: PackratParser[StringFunction[SQLVarchar]] = Substring.regex ~ start ~ valueExpr ~ (From.regex | separator) ~ long ~ ((For.regex | separator) ~ long).? ~ end ^^ { case _ ~ _ ~ v ~ _ ~ s ~ eOpt ~ _ => Substring(v, s.value.toInt, eOpt.map { case _ ~ e => e.value.toInt }) } - def left: PackratParser[StringFunction[SQLVarchar]] = + lazy val left: PackratParser[StringFunction[SQLVarchar]] = LeftOp.regex ~ start ~ valueExpr ~ (For.regex | separator) ~ long ~ end ^^ { case _ ~ _ ~ v ~ _ ~ l ~ _ => LeftFunction(v, l.value.toInt) } - def right: PackratParser[StringFunction[SQLVarchar]] = + lazy val right: PackratParser[StringFunction[SQLVarchar]] = RightOp.regex ~ start ~ valueExpr ~ (For.regex | separator) ~ long ~ end ^^ { case _ ~ _ ~ v ~ _ ~ l ~ _ => RightFunction(v, l.value.toInt) } - def replace: PackratParser[StringFunction[SQLVarchar]] = + lazy val replace: PackratParser[StringFunction[SQLVarchar]] = Replace.regex ~ start ~ valueExpr ~ separator ~ valueExpr ~ separator ~ valueExpr ~ end ^^ { case _ ~ _ ~ v ~ _ ~ f ~ _ ~ r ~ _ => Replace(v, f, r) } - def reverse: PackratParser[StringFunction[SQLVarchar]] = + lazy val reverse: PackratParser[StringFunction[SQLVarchar]] = Reverse.regex ~ start ~ valueExpr ~ end ^^ { case _ ~ _ ~ v ~ _ => Reverse(v) } - def position: PackratParser[StringFunction[SQLBigInt]] = + lazy val position: PackratParser[StringFunction[SQLBigInt]] = Position.regex ~ start ~ valueExpr ~ (separator | IN.regex) ~ valueExpr ~ ((separator | From.regex) ~ long).? ~ end ^^ { case _ ~ _ ~ sub ~ _ ~ str ~ from ~ _ => Position(sub, str, from.map { case _ ~ f => f.value.toInt }.getOrElse(1)) } - def regexp: PackratParser[StringFunction[SQLBool]] = + lazy val regexp: PackratParser[StringFunction[SQLBool]] = RegexpLike.regex ~ start ~ valueExpr ~ separator ~ valueExpr ~ (separator ~ literal).? ~ end ^^ { case _ ~ _ ~ str ~ _ ~ pattern ~ flags ~ _ => RegexpLike( @@ -80,37 +80,37 @@ package object string { ) } - def length: PackratParser[StringFunction[SQLBigInt]] = + lazy val length: PackratParser[StringFunction[SQLBigInt]] = Length.regex ~ start ~ valueExpr ~ end ^^ { case _ ~ _ ~ v ~ _ => Length(v) } - def lower: PackratParser[StringFunction[SQLVarchar]] = + lazy val lower: PackratParser[StringFunction[SQLVarchar]] = Lower.regex ~ start ~ valueExpr ~ end ^^ { case _ ~ _ ~ v ~ _ => StringFunctionWithOp(v, Lower) } - def upper: PackratParser[StringFunction[SQLVarchar]] = + lazy val upper: PackratParser[StringFunction[SQLVarchar]] = Upper.regex ~ start ~ valueExpr ~ end ^^ { case _ ~ _ ~ v ~ _ => StringFunctionWithOp(v, Upper) } - def trim: PackratParser[StringFunction[SQLVarchar]] = + lazy val trim: PackratParser[StringFunction[SQLVarchar]] = Trim.regex ~ start ~ valueExpr ~ end ^^ { case _ ~ _ ~ v ~ _ => StringFunctionWithOp(v, Trim) } - def ltrim: PackratParser[StringFunction[SQLVarchar]] = + lazy val ltrim: PackratParser[StringFunction[SQLVarchar]] = Ltrim.regex ~ start ~ valueExpr ~ end ^^ { case _ ~ _ ~ v ~ _ => StringFunctionWithOp(v, Ltrim) } - def rtrim: PackratParser[StringFunction[SQLVarchar]] = + lazy val rtrim: PackratParser[StringFunction[SQLVarchar]] = Rtrim.regex ~ start ~ valueExpr ~ end ^^ { case _ ~ _ ~ v ~ _ => StringFunctionWithOp(v, Rtrim) } - def stringFunctionWithIdentifier: PackratParser[Identifier] = + lazy val stringFunctionWithIdentifier: PackratParser[Identifier] = (concat | substr | left | diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/time/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/time/package.scala index e45161f73..16568ad78 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/time/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/function/time/package.scala @@ -32,49 +32,49 @@ package object time { trait CurrentParser { self: Parser with TimeParser => - def parens: PackratParser[List[Delimiter]] = + lazy val parens: PackratParser[List[Delimiter]] = start ~ end ^^ { case s ~ e => s :: e :: Nil } - def current_date: PackratParser[Identifier] = + lazy val current_date: PackratParser[Identifier] = CurrentDate.regex ~ parens.? ^^ { case _ ~ p => Identifier(CurrentDate(p.isDefined)) } - def current_time: PackratParser[Identifier] = + lazy val current_time: PackratParser[Identifier] = CurrentTime.regex ~ parens.? ^^ { case _ ~ p => Identifier(CurrentTime(p.isDefined)) } - def current_timestamp: PackratParser[Identifier] = + lazy val current_timestamp: PackratParser[Identifier] = CurrentTimestamp.regex ~ parens.? ^^ { case _ ~ p => Identifier(CurrentTimestamp(p.isDefined)) } - def now: PackratParser[Identifier] = Now.regex ~ parens.? ^^ { case _ ~ p => + lazy val now: PackratParser[Identifier] = Now.regex ~ parens.? ^^ { case _ ~ p => Identifier(Now(p.isDefined)) } - def today: PackratParser[Identifier] = Today.regex ~ parens.? ^^ { case _ ~ p => + lazy val today: PackratParser[Identifier] = Today.regex ~ parens.? ^^ { case _ ~ p => Identifier(Today(p.isDefined)) } - private[this] def current_function: PackratParser[Identifier] = + private[this] lazy val current_function: PackratParser[Identifier] = current_date | current_time | current_timestamp | now | today - def currentFunctionWithIdentifier: PackratParser[Identifier] = + lazy val currentFunctionWithIdentifier: PackratParser[Identifier] = current_function ^^ functionAsIdentifier } trait DateParser { self: Parser with TemporalParser => - def date_add: PackratParser[DateFunction with FunctionWithIdentifier with DateMathScript] = + lazy val date_add: PackratParser[DateFunction with FunctionWithIdentifier with DateMathScript] = DateAdd.regex ~ start ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) ~ separator ~ interval ~ end ^^ { case _ ~ _ ~ i ~ _ ~ t ~ _ => DateAdd(i, t) } - def date_add_transact_sql + lazy val date_add_transact_sql : PackratParser[DateFunction with FunctionWithIdentifier with DateMathScript] = DateAdd.regex ~ start ~> time_unit ~ separator ~ long ~ separator ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) <~ end ^^ { @@ -82,20 +82,21 @@ package object time { DateAdd(i, TimeInterval(l.value.toInt, u), transactSql = true) } - def date_sub: PackratParser[DateFunction with FunctionWithIdentifier with DateMathScript] = + lazy val date_sub: PackratParser[DateFunction with FunctionWithIdentifier with DateMathScript] = DateSub.regex ~ start ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) ~ separator ~ interval ~ end ^^ { case _ ~ _ ~ i ~ _ ~ t ~ _ => DateSub(i, t) } - def date_sub_transact_sql + lazy val date_sub_transact_sql : PackratParser[DateFunction with FunctionWithIdentifier with DateMathScript] = DateSub.regex ~ start ~> time_unit ~ separator ~ long ~ separator ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) <~ end ^^ { case u ~ _ ~ l ~ _ ~ i => DateSub(i, TimeInterval(l.value.toInt, u), transactSql = true) } - def date_parse: PackratParser[DateFunction with FunctionWithIdentifier with DateMathScript] = + lazy val date_parse + : PackratParser[DateFunction with FunctionWithIdentifier with DateMathScript] = DateParse.regex ~ start ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | literal | identifier) ~ separator ~ literal ~ end ^^ { case _ ~ _ ~ li ~ _ ~ f ~ _ => li match { @@ -106,7 +107,7 @@ package object time { } } - def date_format: PackratParser[DateFunction with FunctionWithIdentifier] = + lazy val date_format: PackratParser[DateFunction with FunctionWithIdentifier] = DateFormat.regex ~ start ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) ~ separator ~ literal ~ end ^^ { case _ ~ _ ~ i ~ _ ~ f ~ _ => DateFormat(i, f.value) @@ -119,7 +120,7 @@ package object time { LastDayOfMonth(i) } - def date_function: PackratParser[DateFunction with FunctionWithIdentifier] = + lazy val date_function: PackratParser[DateFunction with FunctionWithIdentifier] = date_add | date_add_transact_sql | date_sub | @@ -128,21 +129,21 @@ package object time { date_format | last_day - def dateFunctionWithIdentifier: PackratParser[Identifier] = + lazy val dateFunctionWithIdentifier: PackratParser[Identifier] = date_function ^^ (t => t.identifier.withFunctions(t +: t.identifier.functions)) } trait DateTimeParser { self: Parser with TemporalParser => - def datetime_add + lazy val datetime_add : PackratParser[DateTimeFunction with FunctionWithIdentifier with DateMathScript] = DateTimeAdd.regex ~ start ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) ~ separator ~ interval ~ end ^^ { case _ ~ _ ~ i ~ _ ~ t ~ _ => DateTimeAdd(i, t) } - def datetime_add_transact_sql + lazy val datetime_add_transact_sql : PackratParser[DateTimeFunction with FunctionWithIdentifier with DateMathScript] = DateTimeAdd.regex ~ start ~> time_unit ~ separator ~ long ~ separator ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) <~ end ^^ { @@ -150,21 +151,21 @@ package object time { DateTimeAdd(i, TimeInterval(l.value.toInt, u), transactSql = true) } - def datetime_sub + lazy val datetime_sub : PackratParser[DateTimeFunction with FunctionWithIdentifier with DateMathScript] = DateTimeSub.regex ~ start ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) ~ separator ~ interval ~ end ^^ { case _ ~ _ ~ i ~ _ ~ t ~ _ => DateTimeSub(i, t) } - def datetime_sub_transact_sql + lazy val datetime_sub_transact_sql : PackratParser[DateTimeFunction with FunctionWithIdentifier with DateMathScript] = DateTimeSub.regex ~ start ~> time_unit ~ separator ~ long ~ separator ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) <~ end ^^ { case u ~ _ ~ l ~ _ ~ i => DateTimeSub(i, TimeInterval(l.value.toInt, u), transactSql = true) } - def datetime_parse: PackratParser[DateTimeFunction with FunctionWithIdentifier] = + lazy val datetime_parse: PackratParser[DateTimeFunction with FunctionWithIdentifier] = DateTimeParse.regex ~ start ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | literal | identifier) ~ separator ~ literal ~ end ^^ { case _ ~ _ ~ li ~ _ ~ f ~ _ => li match { @@ -175,13 +176,13 @@ package object time { } } - def datetime_format: PackratParser[DateTimeFunction with FunctionWithIdentifier] = + lazy val datetime_format: PackratParser[DateTimeFunction with FunctionWithIdentifier] = DateTimeFormat.regex ~ start ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) ~ separator ~ literal ~ end ^^ { case _ ~ _ ~ i ~ _ ~ f ~ _ => DateTimeFormat(i, f.value) } - def datetime_function: PackratParser[DateTimeFunction with FunctionWithIdentifier] = + lazy val datetime_function: PackratParser[DateTimeFunction with FunctionWithIdentifier] = datetime_add | datetime_add_transact_sql | datetime_sub | @@ -189,7 +190,7 @@ package object time { datetime_parse | datetime_format - def dateTimeFunctionWithIdentifier: PackratParser[Identifier] = + lazy val dateTimeFunctionWithIdentifier: PackratParser[Identifier] = datetime_function ^^ { t => t.identifier.withFunctions(t +: t.identifier.functions) } @@ -199,7 +200,7 @@ package object time { trait TemporalParser extends CurrentParser with TimeParser with DateParser with DateTimeParser { self: Parser => - def date_diff: PackratParser[BinaryFunction[_, _, _]] = + lazy val date_diff: PackratParser[BinaryFunction[_, _, _]] = DateDiff.regex ~ start ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) ~ separator ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) ~ (separator ~ time_unit).? ~ end ^^ { case _ ~ _ ~ d1 ~ _ ~ d2 ~ u ~ _ => DateDiff( @@ -212,35 +213,35 @@ package object time { ) } - def date_diff_transact_sql: PackratParser[BinaryFunction[_, _, _]] = + lazy val date_diff_transact_sql: PackratParser[BinaryFunction[_, _, _]] = DateDiff.regex ~ start ~> time_unit ~ separator ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) ~ separator ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) <~ end ^^ { case u ~ _ ~ d1 ~ _ ~ d2 => DateDiff(d1, d2, u, transactSql = true) } - def date_diff_identifier: PackratParser[Identifier] = (date_diff | date_diff_transact_sql) ^^ { - dd => + lazy val date_diff_identifier: PackratParser[Identifier] = + (date_diff | date_diff_transact_sql) ^^ { dd => Identifier(dd) - } + } - def date_trunc: PackratParser[FunctionWithIdentifier] = + lazy val date_trunc: PackratParser[FunctionWithIdentifier] = DateTrunc.regex ~ start ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) ~ separator ~ time_unit ~ end ^^ { case _ ~ _ ~ i ~ _ ~ u ~ _ => DateTrunc(i, u) } - def date_trunc_transact_sql: PackratParser[FunctionWithIdentifier] = + lazy val date_trunc_transact_sql: PackratParser[FunctionWithIdentifier] = DateTrunc.regex ~ start ~> time_unit ~ separator ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) <~ end ^^ { case u ~ _ ~ i => DateTrunc(i, u, transactSql = true) } - def date_trunc_identifier: PackratParser[Identifier] = + lazy val date_trunc_identifier: PackratParser[Identifier] = (date_trunc | date_trunc_transact_sql) ^^ { dt => dt.identifier.withFunctions(dt +: dt.identifier.functions) } - def extract_identifier: PackratParser[Identifier] = + lazy val extract_identifier: PackratParser[Identifier] = Extract.regex ~ start ~ time_field ~ "(?i)from".r ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) ~ end ^^ { case _ ~ _ ~ u ~ _ ~ i ~ _ => i.withFunctions(Extract(u) +: i.functions) @@ -248,47 +249,47 @@ package object time { import TimeField._ - def day_of_week_tr: PackratParser[FunctionWithIdentifier] = + lazy val day_of_week_tr: PackratParser[FunctionWithIdentifier] = DAY_OF_WEEK.regex ~ start ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) ~ end ^^ { case _ ~ _ ~ i ~ _ => new DayOfWeek(i) } - def day_of_week_identifier: PackratParser[Identifier] = day_of_week_tr ^^ { dw => + lazy val day_of_week_identifier: PackratParser[Identifier] = day_of_week_tr ^^ { dw => dw.identifier.withFunctions(dw +: dw.identifier.functions) } - def year_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = + lazy val year_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = YEAR.regex ^^ (_ => new Year) - def month_of_year_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = + lazy val month_of_year_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = MONTH_OF_YEAR.regex ^^ (_ => new MonthOfYear) - def day_of_month_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = + lazy val day_of_month_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = DAY_OF_MONTH.regex ^^ (_ => new DayOfMonth) - def day_of_year_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = + lazy val day_of_year_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = DAY_OF_YEAR.regex ^^ (_ => new DayOfYear) - def hour_of_day_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = + lazy val hour_of_day_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = HOUR_OF_DAY.regex ^^ (_ => new HourOfDay) - def minute_of_hour_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = + lazy val minute_of_hour_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = MINUTE_OF_HOUR.regex ^^ (_ => new MinuteOfHour) - def second_of_minute_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = + lazy val second_of_minute_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = SECOND_OF_MINUTE.regex ^^ (_ => new SecondOfMinute) - def nano_of_second_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = + lazy val nano_of_second_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = NANO_OF_SECOND.regex ^^ (_ => new NanoOfSecond) - def micro_of_second_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = + lazy val micro_of_second_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = MICRO_OF_SECOND.regex ^^ (_ => new MicroOfSecond) - def milli_of_second_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = + lazy val milli_of_second_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = MILLI_OF_SECOND.regex ^^ (_ => new MilliOfSecond) - def epoch_day_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = + lazy val epoch_day_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = EPOCH_DAY.regex ^^ (_ => new EpochDay) - def offset_seconds_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = + lazy val offset_seconds_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = OFFSET_SECONDS.regex ^^ (_ => new OffsetSeconds) - def quarter_of_year_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = + lazy val quarter_of_year_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = IsoField.QUARTER_OF_YEAR.regex ^^ (_ => new QuarterOfYear) - def week_of_week_based_year_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = + lazy val week_of_week_based_year_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = IsoField.WEEK_OF_WEEK_BASED_YEAR.regex ^^ (_ => new WeekOfWeekBasedYear) - def extractor_function: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = + lazy val extractor_function: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = year_tr | month_of_year_tr | day_of_month_tr | diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/http/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/http/package.scala index a74176470..5d05b92bc 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/http/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/http/package.scala @@ -39,82 +39,83 @@ package object http { trait HttpParser { self: Parser => // URL parser - def url: PackratParser[Url] = literal ^^ { urlStr => + lazy val url: PackratParser[Url] = literal ^^ { urlStr => Url(urlStr.value) } // url protocol parser - def http: PackratParser[Protocol.Http.type] = + lazy val http: PackratParser[Protocol.Http.type] = "(?i)(HTTP)\\b".r ^^ { _ => Protocol.Http } - def https: PackratParser[Protocol.Https.type] = + lazy val https: PackratParser[Protocol.Https.type] = "(?i)(HTTPS)\\b".r ^^ { _ => Protocol.Https } - def urlProtocol: PackratParser[Protocol] = + lazy val urlProtocol: PackratParser[Protocol] = "PROTOCOL" ~> (https | http) // url host parser - def urlHost: PackratParser[Host] = + lazy val urlHost: PackratParser[Host] = "HOST" ~> literal ^^ { hostStr => Host(hostStr) } // url port parser - def urlPort: PackratParser[Port] = + lazy val urlPort: PackratParser[Port] = "PORT" ~> long ^^ { l => Port.CustomPort(IntValue(l.value.toInt)) } // url path parser - def urlPath: PackratParser[Path] = + lazy val urlPath: PackratParser[Path] = "PATH" ~> literal ^^ { pathStr => Path(pathStr) } // url query parameters parser - def urlQueryParams: PackratParser[QueryParams] = + lazy val urlQueryParams: PackratParser[QueryParams] = "PARAMS" ~> start ~ repsep(option, separator) ~ end ^^ { case _ ~ opts ~ _ => QueryParams(ListMap(opts: _*)) } // url part parser - def urlPart: PackratParser[UrlPart] = urlProtocol | urlHost | urlPort | urlPath | urlQueryParams + lazy val urlPart: PackratParser[UrlPart] = + urlProtocol | urlHost | urlPort | urlPath | urlQueryParams // combined url parts parser - def urlParts: PackratParser[Url] = + lazy val urlParts: PackratParser[Url] = rep(urlPart) ^^ { parts => Url(parts) } // method parser - def get: PackratParser[Method.Get.type] = + lazy val get: PackratParser[Method.Get.type] = "(?i)(GET)\\b".r ^^ { _ => Method.Get } - def post: PackratParser[Method.Post.type] = + lazy val post: PackratParser[Method.Post.type] = "(?i)(POST)\\b".r ^^ { _ => Method.Post } - def put: PackratParser[Method.Put.type] = + lazy val put: PackratParser[Method.Put.type] = "(?i)(PUT)\\b".r ^^ { _ => Method.Put } - def del: PackratParser[Method.Delete.type] = + lazy val del: PackratParser[Method.Delete.type] = "(?i)(DELETE)\\b".r ^^ { _ => Method.Delete } - def httpMethod: PackratParser[Method] = get | post | put | del + lazy val httpMethod: PackratParser[Method] = get | post | put | del // headers parser - def headers: PackratParser[Headers] = + lazy val headers: PackratParser[Headers] = "HEADERS" ~> start ~ repsep(option, separator) ~ end ^^ { case _ ~ opts ~ _ => Headers(ListMap(opts: _*)) } // body parser - def body: PackratParser[Body] = + lazy val body: PackratParser[Body] = "BODY" ~> literal ^^ { body => Body(body) } - def timeout: PackratParser[Option[Timeout]] = + lazy val timeout: PackratParser[Option[Timeout]] = "TIMEOUT" ~> start ~ repsep(option, separator) <~ end ^^ { case _ ~ t => Timeout(t.toMap) } - def httpRequest: PackratParser[HttpRequest] = + lazy val httpRequest: PackratParser[HttpRequest] = httpMethod ~ (url | urlParts) ~ opt(headers) ~ opt(body) ~ opt(timeout) ^^ { case method ~ url ~ headersOpt ~ bodyOpt ~ timeoutOpt => HttpRequest( diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/operator/math/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/operator/math/package.scala index 4db78dc4d..39ee73076 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/operator/math/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/operator/math/package.scala @@ -32,17 +32,17 @@ import app.softnetwork.elastic.sql.parser.Parser package object math { trait ArithmeticParser { self: Parser => - def add: PackratParser[ArithmeticOperator] = ADD.sql ^^ (_ => ADD) + lazy val add: PackratParser[ArithmeticOperator] = ADD.sql ^^ (_ => ADD) - def subtract: PackratParser[ArithmeticOperator] = SUBTRACT.sql ^^ (_ => SUBTRACT) + lazy val subtract: PackratParser[ArithmeticOperator] = SUBTRACT.sql ^^ (_ => SUBTRACT) - def multiply: PackratParser[ArithmeticOperator] = MULTIPLY.sql ^^ (_ => MULTIPLY) + lazy val multiply: PackratParser[ArithmeticOperator] = MULTIPLY.sql ^^ (_ => MULTIPLY) - def divide: PackratParser[ArithmeticOperator] = DIVIDE.sql ^^ (_ => DIVIDE) + lazy val divide: PackratParser[ArithmeticOperator] = DIVIDE.sql ^^ (_ => DIVIDE) - def modulo: PackratParser[ArithmeticOperator] = MODULO.sql ^^ (_ => MODULO) + lazy val modulo: PackratParser[ArithmeticOperator] = MODULO.sql ^^ (_ => MODULO) - def factor: PackratParser[PainlessScript] = + lazy val factor: PackratParser[PainlessScript] = "(" ~> arithmeticExpressionLevel2 <~ ")" ^^ { case expr: ArithmeticExpression => expr.copy(group = true) diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/time/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/time/package.scala index 3e750a6f3..9b2af070e 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/time/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/time/package.scala @@ -28,38 +28,39 @@ package object time { import TimeField._ - def year: PackratParser[TimeField] = YEAR.regex ^^ (_ => YEAR) - def month_of_year: PackratParser[TimeField] = MONTH_OF_YEAR.regex ^^ (_ => MONTH_OF_YEAR) - def day_of_month: PackratParser[TimeField] = + lazy val year: PackratParser[TimeField] = YEAR.regex ^^ (_ => YEAR) + lazy val month_of_year: PackratParser[TimeField] = MONTH_OF_YEAR.regex ^^ (_ => MONTH_OF_YEAR) + lazy val day_of_month: PackratParser[TimeField] = DAY_OF_MONTH.regex ^^ (_ => DAY_OF_MONTH) - def day_of_week: PackratParser[TimeField] = + lazy val day_of_week: PackratParser[TimeField] = DAY_OF_WEEK.regex ^^ (_ => DAY_OF_WEEK) - def day_of_year: PackratParser[TimeField] = + lazy val day_of_year: PackratParser[TimeField] = DAY_OF_YEAR.regex ^^ (_ => DAY_OF_YEAR) - def hour_of_day: PackratParser[TimeField] = HOUR_OF_DAY.regex ^^ (_ => HOUR_OF_DAY) - def minute_of_hour: PackratParser[TimeField] = MINUTE_OF_HOUR.regex ^^ (_ => MINUTE_OF_HOUR) - def second_of_minute: PackratParser[TimeField] = + lazy val hour_of_day: PackratParser[TimeField] = HOUR_OF_DAY.regex ^^ (_ => HOUR_OF_DAY) + lazy val minute_of_hour: PackratParser[TimeField] = + MINUTE_OF_HOUR.regex ^^ (_ => MINUTE_OF_HOUR) + lazy val second_of_minute: PackratParser[TimeField] = SECOND_OF_MINUTE.regex ^^ (_ => SECOND_OF_MINUTE) - def nano_of_second: PackratParser[TimeField] = + lazy val nano_of_second: PackratParser[TimeField] = NANO_OF_SECOND.regex ^^ (_ => NANO_OF_SECOND) - def micro_of_second: PackratParser[TimeField] = + lazy val micro_of_second: PackratParser[TimeField] = MICRO_OF_SECOND.regex ^^ (_ => MICRO_OF_SECOND) - def milli_of_second: PackratParser[TimeField] = + lazy val milli_of_second: PackratParser[TimeField] = MILLI_OF_SECOND.regex ^^ (_ => MILLI_OF_SECOND) - def epoch_day: PackratParser[TimeField] = + lazy val epoch_day: PackratParser[TimeField] = EPOCH_DAY.regex ^^ (_ => EPOCH_DAY) - def offset_seconds: PackratParser[TimeField] = + lazy val offset_seconds: PackratParser[TimeField] = OFFSET_SECONDS.regex ^^ (_ => OFFSET_SECONDS) import IsoField._ - def quarter_of_year: PackratParser[TimeField] = + lazy val quarter_of_year: PackratParser[TimeField] = QUARTER_OF_YEAR.regex ^^ (_ => QUARTER_OF_YEAR) - def week_of_week_based_year: PackratParser[TimeField] = + lazy val week_of_week_based_year: PackratParser[TimeField] = WEEK_OF_WEEK_BASED_YEAR.regex ^^ (_ => WEEK_OF_WEEK_BASED_YEAR) - def time_field: PackratParser[TimeField] = + lazy val time_field: PackratParser[TimeField] = year | month_of_year | day_of_month | @@ -78,34 +79,34 @@ package object time { import TimeUnit._ - def years: PackratParser[TimeUnit] = YEARS.regex ^^ (_ => YEARS) - def months: PackratParser[TimeUnit] = MONTHS.regex ^^ (_ => MONTHS) - def quarters: PackratParser[TimeUnit] = QUARTERS.regex ^^ (_ => QUARTERS) - def weeks: PackratParser[TimeUnit] = WEEKS.regex ^^ (_ => WEEKS) - def days: PackratParser[TimeUnit] = DAYS.regex ^^ (_ => DAYS) - def hours: PackratParser[TimeUnit] = HOURS.regex ^^ (_ => HOURS) - def minutes: PackratParser[TimeUnit] = MINUTES.regex ^^ (_ => MINUTES) - def seconds: PackratParser[TimeUnit] = SECONDS.regex ^^ (_ => SECONDS) + lazy val years: PackratParser[TimeUnit] = YEARS.regex ^^ (_ => YEARS) + lazy val months: PackratParser[TimeUnit] = MONTHS.regex ^^ (_ => MONTHS) + lazy val quarters: PackratParser[TimeUnit] = QUARTERS.regex ^^ (_ => QUARTERS) + lazy val weeks: PackratParser[TimeUnit] = WEEKS.regex ^^ (_ => WEEKS) + lazy val days: PackratParser[TimeUnit] = DAYS.regex ^^ (_ => DAYS) + lazy val hours: PackratParser[TimeUnit] = HOURS.regex ^^ (_ => HOURS) + lazy val minutes: PackratParser[TimeUnit] = MINUTES.regex ^^ (_ => MINUTES) + lazy val seconds: PackratParser[TimeUnit] = SECONDS.regex ^^ (_ => SECONDS) - def time_unit: PackratParser[TimeUnit] = + lazy val time_unit: PackratParser[TimeUnit] = years | months | quarters | weeks | days | hours | minutes | seconds - def interval: PackratParser[TimeInterval] = + lazy val interval: PackratParser[TimeInterval] = Interval.regex ~ long ~ time_unit ^^ { case _ ~ l ~ u => TimeInterval(l.value.toInt, u) } - def add_interval: PackratParser[SQLAddInterval] = + lazy val add_interval: PackratParser[SQLAddInterval] = add ~ interval ^^ { case _ ~ it => SQLAddInterval(it) } - def substract_interval: PackratParser[SQLSubtractInterval] = + lazy val substract_interval: PackratParser[SQLSubtractInterval] = subtract ~ interval ^^ { case _ ~ it => SQLSubtractInterval(it) } - def intervalFunction: PackratParser[TransformFunction[SQLTemporal, SQLTemporal]] = + lazy val intervalFunction: PackratParser[TransformFunction[SQLTemporal, SQLTemporal]] = add_interval | substract_interval /** `quotedIdentifier` MUST precede `identifierWithValue` (story 21.1 AD-13). @@ -138,7 +139,7 @@ package object time { * * No alternation ORDER moves anywhere; this adds an alternative. */ - def identifierWithIntervalFunction: PackratParser[Identifier] = + lazy val identifierWithIntervalFunction: PackratParser[Identifier] = ((identifierWithTransformation | identifierWithFunction | quotedIdentifier | diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/type/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/type/package.scala index a5ae9a277..74208f478 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/type/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/type/package.scala @@ -63,52 +63,53 @@ package object `type` { * a bare `"x"`. An EMPTY `""` stays the empty STRING — `quotedNameRegex`'s content quantifier * is `+` on purpose. */ - def literal: PackratParser[StringValue] = + lazy val literal: PackratParser[StringValue] = (""""([^"\\]|\\.|"")*"""".r ^^ { str => StringValue(unescapeStringLiteral(str.substring(1, str.length - 1), '"')) }) | ("""'([^'\\]|\\.|'')*'""".r ^^ { str => StringValue(unescapeStringLiteral(str.substring(1, str.length - 1), '\'')) }) - def long: PackratParser[LongValue] = + lazy val long: PackratParser[LongValue] = """(-)?(0|[1-9]\d*)""".r ^^ (str => LongValue(str.toLong)) - def double: PackratParser[DoubleValue] = + lazy val double: PackratParser[DoubleValue] = """(-)?(\d+\.\d+)""".r ^^ (str => DoubleValue(str.toDouble)) - def pi: PackratParser[Value[Double]] = + lazy val pi: PackratParser[Value[Double]] = PiValue.regex ^^ (_ => PiValue) - def random: PackratParser[Value[Double]] = """(?i)random\b""".r ^^ (_ => RandomValue) + lazy val random: PackratParser[Value[Double]] = """(?i)random\b""".r ^^ (_ => RandomValue) - def boolean: PackratParser[BooleanValue] = + lazy val boolean: PackratParser[BooleanValue] = """(?i)(true|false)\b""".r ^^ (bool => BooleanValue(bool.toBoolean)) - def param: PackratParser[ParamValue.type] = + lazy val param: PackratParser[ParamValue.type] = "?" ^^ (_ => ParamValue) - def nullValue: PackratParser[Null.type] = + lazy val nullValue: PackratParser[Null.type] = "(?i)NULL\\b".r ^^ (_ => Null) - def literals: PackratParser[Value[_]] = "[" ~> repsep(literal, ",") <~ "]" ^^ { list => + lazy val literals: PackratParser[Value[_]] = "[" ~> repsep(literal, ",") <~ "]" ^^ { list => StringValues(list) } - def longs: PackratParser[Value[_]] = "[" ~> repsep(long, ",") <~ "]" ^^ { list => + lazy val longs: PackratParser[Value[_]] = "[" ~> repsep(long, ",") <~ "]" ^^ { list => LongValues(list) } - def doubles: PackratParser[Value[_]] = "[" ~> repsep(double, ",") <~ "]" ^^ { list => + lazy val doubles: PackratParser[Value[_]] = "[" ~> repsep(double, ",") <~ "]" ^^ { list => DoubleValues(list) } - def booleans: PackratParser[BooleanValues] = "[" ~> repsep(boolean, ",") <~ "]" ^^ { list => - BooleanValues(list) + lazy val booleans: PackratParser[BooleanValues] = "[" ~> repsep(boolean, ",") <~ "]" ^^ { + list => + BooleanValues(list) } - def array: PackratParser[Value[_]] = literals | longs | doubles | booleans + lazy val array: PackratParser[Value[_]] = literals | longs | doubles | booleans - def value: PackratParser[Value[_]] = + lazy val value: PackratParser[Value[_]] = literal | pi | random | double | long | boolean | nullValue | param | array /** The two ingest-time placeholders a column DEFAULT may carry. They live beside `value` so @@ -116,9 +117,9 @@ package object `type` { * `option` both need them, and only the former could see them while they were declared in the * object. */ - def ingest_id: PackratParser[Value[_]] = "_id" ^^ (_ => IdValue) + lazy val ingest_id: PackratParser[Value[_]] = "_id" ^^ (_ => IdValue) - def ingest_timestamp: PackratParser[Value[_]] = + lazy val ingest_timestamp: PackratParser[Value[_]] = "_ingest.timestamp" ^^ (_ => IngestTimestampValue) def identifierWithValue: Parser[Identifier] = (value ^^ functionAsIdentifier) >> cast @@ -126,7 +127,7 @@ package object `type` { /** An UNSIGNED decimal integer — deliberately NOT `long`, whose regex carries an optional sign * and would accept `CHAR(-1)`. */ - private def typeParamValue: PackratParser[String] = """\d+""".r ^^ (v => v) + private lazy val typeParamValue: PackratParser[String] = """\d+""".r ^^ (v => v) /** An ANSI/MySQL type parameter list — `CHAR(10)`, `DECIMAL(10,2)`, `INT(11)`, `TIMESTAMP(3)`. * @@ -147,13 +148,13 @@ package object `type` { * yields a `Failure` (never an `Error` — none of `start`/`separator`/`end` is `~!`/`err`), * which `|` recovers, restoring the input position. */ - def typeParams: PackratParser[Unit] = + lazy val typeParams: PackratParser[Unit] = opt(start ~ typeParamValue ~ opt(separator ~ typeParamValue) ~ end) ^^ (_ => ()) - def char_type: PackratParser[SQLTypes.Char.type] = + lazy val char_type: PackratParser[SQLTypes.Char.type] = "(?i)char".r ~ typeParams ^^ (_ => SQLTypes.Char) - def string_type: PackratParser[SQLTypes.Varchar.type] = + lazy val string_type: PackratParser[SQLTypes.Varchar.type] = "(?i)varchar|string".r ~ typeParams ^^ (_ => SQLTypes.Varchar) /** `DECIMAL` / `NUMERIC` / `DEC` map to DOUBLE, **not** to `SQLTypes.Numeric`. @@ -163,7 +164,7 @@ package object `type` { * identity fallback and would emit the operand UNCONVERTED — the #205 silent-wrong-answer * shape. DOUBLE is also what Elasticsearch actually stores for these. */ - def decimal_type: PackratParser[SQLTypes.Double.type] = + lazy val decimal_type: PackratParser[SQLTypes.Double.type] = """(?i)(decimal|numeric|dec)\b""".r ~ typeParams ^^ (_ => SQLTypes.Double) /** MySQL's `CAST(x AS SIGNED)` / `AS UNSIGNED` — what Tableau's MySQL dialect emits. Both are @@ -171,53 +172,53 @@ package object `type` { * UNSIGNED is an alias of BIGINT and the documentation says so rather than pretending. Neither * spelling takes a parameter list in any dialect, so neither gets one. */ - def signed_type: PackratParser[SQLTypes.BigInt.type] = + lazy val signed_type: PackratParser[SQLTypes.BigInt.type] = """(?i)(signed|unsigned)(\s+(integer|int))?\b""".r ^^ (_ => SQLTypes.BigInt) /** No dialect gives `DATE` a precision — a fractional-seconds precision belongs to `TIME`, * `TIMESTAMP` and `DATETIME`. */ - def date_type: PackratParser[SQLTypes.Date.type] = "(?i)date".r ^^ (_ => SQLTypes.Date) + lazy val date_type: PackratParser[SQLTypes.Date.type] = "(?i)date".r ^^ (_ => SQLTypes.Date) - def time_type: PackratParser[SQLTypes.Time.type] = + lazy val time_type: PackratParser[SQLTypes.Time.type] = "(?i)time".r ~ typeParams ^^ (_ => SQLTypes.Time) - def datetime_type: PackratParser[SQLTypes.DateTime.type] = + lazy val datetime_type: PackratParser[SQLTypes.DateTime.type] = "(?i)(datetime)".r ~ typeParams ^^ (_ => SQLTypes.DateTime) - def timestamp_type: PackratParser[SQLTypes.Timestamp.type] = + lazy val timestamp_type: PackratParser[SQLTypes.Timestamp.type] = "(?i)(timestamp)".r ~ typeParams ^^ (_ => SQLTypes.Timestamp) - def boolean_type: PackratParser[SQLTypes.Boolean.type] = + lazy val boolean_type: PackratParser[SQLTypes.Boolean.type] = "(?i)boolean".r ^^ (_ => SQLTypes.Boolean) - def byte_type: PackratParser[SQLTypes.TinyInt.type] = + lazy val byte_type: PackratParser[SQLTypes.TinyInt.type] = "(?i)(byte|tinyint)".r ~ typeParams ^^ (_ => SQLTypes.TinyInt) - def short_type: PackratParser[SQLTypes.SmallInt.type] = + lazy val short_type: PackratParser[SQLTypes.SmallInt.type] = "(?i)(short|smallint)".r ~ typeParams ^^ (_ => SQLTypes.SmallInt) - def int_type: PackratParser[SQLTypes.Int.type] = + lazy val int_type: PackratParser[SQLTypes.Int.type] = "(?i)(integer|int)".r ~ typeParams ^^ (_ => SQLTypes.Int) - def long_type: PackratParser[SQLTypes.BigInt.type] = + lazy val long_type: PackratParser[SQLTypes.BigInt.type] = "(?i)long|bigint".r ~ typeParams ^^ (_ => SQLTypes.BigInt) - def double_type: PackratParser[SQLTypes.Double.type] = + lazy val double_type: PackratParser[SQLTypes.Double.type] = "(?i)double".r ~ typeParams ^^ (_ => SQLTypes.Double) - def float_type: PackratParser[SQLTypes.Real.type] = + lazy val float_type: PackratParser[SQLTypes.Real.type] = "(?i)float|real".r ~ typeParams ^^ (_ => SQLTypes.Real) - def struct_type: PackratParser[SQLTypes.Struct.type] = + lazy val struct_type: PackratParser[SQLTypes.Struct.type] = "(?i)struct".r ^^ (_ => SQLTypes.Struct) - def array_type: PackratParser[SQLTypes.Array] = + lazy val array_type: PackratParser[SQLTypes.Array] = "(?i)array<".r ~> sql_type <~ ">" ^^ { elementType => SQLTypes.Array(elementType) } - def binary_type: PackratParser[SQLTypes.VarBinary.type] = + lazy val binary_type: PackratParser[SQLTypes.VarBinary.type] = "(?i)(binary|varbinary)".r ~ typeParams ^^ (_ => SQLTypes.VarBinary) /** 🔴 The ORDER is load-bearing: none of these regexes carries a `\b`, so a shorter name that @@ -230,7 +231,7 @@ package object `type` { * only) — which is precisely why a DQL cast to `TEXT` or `KEYWORD` was rejected while `CREATE * TABLE t (c TEXT)` worked. */ - def sql_type: PackratParser[SQLType] = + lazy val sql_type: PackratParser[SQLType] = char_type | string_type | decimal_type | @@ -252,17 +253,17 @@ package object `type` { array_type | binary_type - def text_type: PackratParser[SQLTypes.Text.type] = + lazy val text_type: PackratParser[SQLTypes.Text.type] = "(?i)text".r ~ typeParams ^^ (_ => SQLTypes.Text) /** Elasticsearch's `keyword` has no length, in SQL or in ES. */ - def keyword_type: PackratParser[SQLTypes.Keyword.type] = + lazy val keyword_type: PackratParser[SQLTypes.Keyword.type] = "(?i)keyword".r ^^ (_ => SQLTypes.Keyword) - def geo_point_type: PackratParser[SQLTypes.GeoPoint.type] = + lazy val geo_point_type: PackratParser[SQLTypes.GeoPoint.type] = "(?i)(geo_point|geopoint)".r ^^ (_ => SQLTypes.GeoPoint) - def extension_type: PackratParser[SQLType] = + lazy val extension_type: PackratParser[SQLType] = sql_type | geo_point_type } } diff --git a/sql/src/test/scala/app/softnetwork/elastic/sql/census/DialectCensus.scala b/sql/src/test/scala/app/softnetwork/elastic/sql/census/DialectCensus.scala index b441d302c..9b7534976 100644 --- a/sql/src/test/scala/app/softnetwork/elastic/sql/census/DialectCensus.scala +++ b/sql/src/test/scala/app/softnetwork/elastic/sql/census/DialectCensus.scala @@ -265,7 +265,7 @@ object DialectCensus { "PERCENTILE_CONT", "PERCENTILE_CONT", PFa, - "private[this] def percentile_args: PackratParser[(Option[Identifier], Double)] =", + "private[this] lazy val percentile_args: PackratParser[(Option[Identifier], Double)] =", "SELECT PERCENTILE_CONT(salary, 0.5) AS med FROM emp", "2", EsSpecific, @@ -279,7 +279,7 @@ object DialectCensus { "PERCENTILE_CONT", "PERCENTILE_CONT", PFa, - "private[this] def percentile_within_group: PackratParser[Seq[Identifier]] =", + "private[this] lazy val percentile_within_group: PackratParser[Seq[Identifier]] =", "SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) AS med FROM emp", "2", EsSpecific, @@ -293,7 +293,7 @@ object DialectCensus { "PERCENTILE_CONT", "PERCENTILE_CONT", PFa, - "def percentile_agg: PackratParser[WindowFunction] =", + "lazy val percentile_agg: PackratParser[WindowFunction] =", "SELECT PERCENTILE_CONT(0.5) OVER (ORDER BY salary) AS med FROM emp", "2", EsSpecific, @@ -307,7 +307,7 @@ object DialectCensus { "PERCENTILE_CONT", "PERCENTILE_CONT", PFa, - "def percentile_agg: PackratParser[WindowFunction] =", + "lazy val percentile_agg: PackratParser[WindowFunction] =", "SELECT PERCENTILE_CONT(salary, 0.5) OVER (PARTITION BY dept) AS med FROM emp", "2", EsSpecific, @@ -321,7 +321,7 @@ object DialectCensus { "PERCENTILE_CONT", "PERCENTILE_CONT", PFa, - "def percentile_agg: PackratParser[WindowFunction] =", + "lazy val percentile_agg: PackratParser[WindowFunction] =", "SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) " + "OVER (PARTITION BY dept) AS med FROM emp", "2", @@ -336,7 +336,7 @@ object DialectCensus { "PERCENTILE_DISC", "PERCENTILE_DISC", PFa, - "private[this] def percentile_args: PackratParser[(Option[Identifier], Double)] =", + "private[this] lazy val percentile_args: PackratParser[(Option[Identifier], Double)] =", "SELECT PERCENTILE_DISC(salary, 0.9) AS p90 FROM emp", "2", EsSpecific, @@ -350,7 +350,7 @@ object DialectCensus { "PERCENTILE_DISC", "PERCENTILE_DISC", PFa, - "private[this] def percentile_within_group: PackratParser[Seq[Identifier]] =", + "private[this] lazy val percentile_within_group: PackratParser[Seq[Identifier]] =", "SELECT PERCENTILE_DISC(0.9) WITHIN GROUP (ORDER BY salary) AS p90 FROM emp", "2", EsSpecific, @@ -364,7 +364,7 @@ object DialectCensus { "PERCENTILE_DISC", "PERCENTILE_DISC", PFa, - "def percentile_agg: PackratParser[WindowFunction] =", + "lazy val percentile_agg: PackratParser[WindowFunction] =", "SELECT PERCENTILE_DISC(0.9) OVER (ORDER BY salary) AS p90 FROM emp", "2", EsSpecific, @@ -378,7 +378,7 @@ object DialectCensus { "PERCENTILE_DISC", "PERCENTILE_DISC", PFa, - "def percentile_agg: PackratParser[WindowFunction] =", + "lazy val percentile_agg: PackratParser[WindowFunction] =", "SELECT PERCENTILE_DISC(salary, 0.9) OVER (PARTITION BY dept) AS p90 FROM emp", "2", EsSpecific, @@ -392,7 +392,7 @@ object DialectCensus { "PERCENTILE_DISC", "PERCENTILE_DISC", PFa, - "def percentile_agg: PackratParser[WindowFunction] =", + "lazy val percentile_agg: PackratParser[WindowFunction] =", "SELECT PERCENTILE_DISC(0.9) WITHIN GROUP (ORDER BY salary) " + "OVER (PARTITION BY dept) AS p90 FROM emp", "2", @@ -688,7 +688,7 @@ object DialectCensus { "CONVERT", "CONVERT", PFv, - "def convert_transact_sql_identifier: PackratParser[Identifier] =", + "lazy val convert_transact_sql_identifier: PackratParser[Identifier] =", "SELECT CONVERT(BIGINT, age) AS a FROM emp", "2", EsSpecific, @@ -1097,7 +1097,7 @@ object DialectCensus { "SUBSTRING", "SUBSTRING", PFs, - "def substr: PackratParser[StringFunction[SQLVarchar]] =", + "lazy val substr: PackratParser[StringFunction[SQLVarchar]] =", "SELECT SUBSTRING(name FROM 2 FOR 3) AS c FROM emp", "2..3", Ansi, @@ -1113,7 +1113,7 @@ object DialectCensus { "SUBSTRING", "SUBSTRING", PFs, - "def substr: PackratParser[StringFunction[SQLVarchar]] =", + "lazy val substr: PackratParser[StringFunction[SQLVarchar]] =", "SELECT SUBSTRING(name, 2, 3) AS c FROM emp", "2..3", AnsiAdjacent, @@ -1143,7 +1143,7 @@ object DialectCensus { "LEFT", "LEFT", PFs, - "def left: PackratParser[StringFunction[SQLVarchar]] =", + "lazy val left: PackratParser[StringFunction[SQLVarchar]] =", "SELECT LEFT(name, 3) AS l FROM emp", "2", AnsiAdjacent, @@ -1156,7 +1156,7 @@ object DialectCensus { "LEFT", "LEFT", PFs, - "def left: PackratParser[StringFunction[SQLVarchar]] =", + "lazy val left: PackratParser[StringFunction[SQLVarchar]] =", "SELECT LEFT(name FOR 3) AS l FROM emp", "2", EsSpecific, @@ -1171,7 +1171,7 @@ object DialectCensus { "RIGHT", "RIGHT", PFs, - "def right: PackratParser[StringFunction[SQLVarchar]] =", + "lazy val right: PackratParser[StringFunction[SQLVarchar]] =", "SELECT RIGHT(name, 3) AS r FROM emp", "2", AnsiAdjacent, @@ -1184,7 +1184,7 @@ object DialectCensus { "RIGHT", "RIGHT", PFs, - "def right: PackratParser[StringFunction[SQLVarchar]] =", + "lazy val right: PackratParser[StringFunction[SQLVarchar]] =", "SELECT RIGHT(name FOR 3) AS r FROM emp", "2", EsSpecific, @@ -1283,7 +1283,7 @@ object DialectCensus { "POSITION", "POSITION", PFs, - "def position: PackratParser[StringFunction[SQLBigInt]] =", + "lazy val position: PackratParser[StringFunction[SQLBigInt]] =", "SELECT POSITION('a', name, 2) AS p FROM emp", "2..3", EsSpecific, @@ -1374,7 +1374,7 @@ object DialectCensus { "CURRENT_DATE", "CURRENT_DATE", PFt, - "def parens: PackratParser[List[Delimiter]] =", + "lazy val parens: PackratParser[List[Delimiter]] =", "SELECT CURRENT_DATE() AS d FROM emp", "0", EsSpecific, @@ -1418,7 +1418,7 @@ object DialectCensus { "CURRENT_TIME", "CURRENT_TIME", PFt, - "def parens: PackratParser[List[Delimiter]] =", + "lazy val parens: PackratParser[List[Delimiter]] =", "SELECT CURRENT_TIME() AS t FROM emp", "0", EsSpecific, @@ -1457,7 +1457,7 @@ object DialectCensus { "CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP", PFt, - "def parens: PackratParser[List[Delimiter]] =", + "lazy val parens: PackratParser[List[Delimiter]] =", "SELECT CURRENT_TIMESTAMP() AS ts FROM emp", "0", EsSpecific, @@ -1514,7 +1514,7 @@ object DialectCensus { "DATE_TRUNC", "DATE_TRUNC", PFt, - "def date_trunc_transact_sql: PackratParser[FunctionWithIdentifier] =", + "lazy val date_trunc_transact_sql: PackratParser[FunctionWithIdentifier] =", "SELECT DATE_TRUNC(MONTH, created_at) AS m FROM events", "2", AnsiAdjacent, @@ -1605,7 +1605,7 @@ object DialectCensus { "DATE_DIFF", "DATE_DIFF", PFt, - "def date_diff_transact_sql: PackratParser[BinaryFunction[_, _, _]] =", + "lazy val date_diff_transact_sql: PackratParser[BinaryFunction[_, _, _]] =", "SELECT DATE_DIFF(DAY, start_date, end_date) AS d FROM projects", "3", EsSpecific, @@ -1649,7 +1649,7 @@ object DialectCensus { "DATE_ADD", "DATE_ADD", PFt, - "def date_add_transact_sql : PackratParser[DateFunction with FunctionWithIdentifier " + + "lazy val date_add_transact_sql : PackratParser[DateFunction with FunctionWithIdentifier " + "with DateMathScript]", "SELECT DATE_ADD(DAY, 7, created_at) AS d FROM events", "3", @@ -1690,7 +1690,7 @@ object DialectCensus { "DATE_SUB", "DATE_SUB", PFt, - "def date_sub_transact_sql : PackratParser[DateFunction with FunctionWithIdentifier " + + "lazy val date_sub_transact_sql : PackratParser[DateFunction with FunctionWithIdentifier " + "with DateMathScript]", "SELECT DATE_SUB(DAY, 7, created_at) AS d FROM events", "3", @@ -1829,7 +1829,7 @@ object DialectCensus { "DATETIME_ADD", "DATETIME_ADD", PFt, - "def datetime_add_transact_sql : PackratParser[DateTimeFunction with " + + "lazy val datetime_add_transact_sql : PackratParser[DateTimeFunction with " + "FunctionWithIdentifier with DateMathScript]", "SELECT DATETIME_ADD(HOUR, 2, updated_at) AS d FROM events", "3", @@ -1871,7 +1871,7 @@ object DialectCensus { "DATETIME_SUB", "DATETIME_SUB", PFt, - "def datetime_sub_transact_sql : PackratParser[DateTimeFunction with " + + "lazy val datetime_sub_transact_sql : PackratParser[DateTimeFunction with " + "FunctionWithIdentifier with DateMathScript]", "SELECT DATETIME_SUB(HOUR, 2, updated_at) AS d FROM events", "3", @@ -3258,7 +3258,7 @@ object DialectCensus { "CASE", "CASE", s"$S/parser/function/cond/package.scala", - "def case_when: PackratParser[Case] =", + "lazy val case_when: PackratParser[Case] =", "SELECT CASE status WHEN 'A' THEN 1 ELSE 0 END AS s FROM emp", "1..n", Ansi, @@ -3330,7 +3330,7 @@ object DialectCensus { "BIGINT", "BIGINT", s"$S/parser/type/package.scala", - "def sql_type: PackratParser[SQLType] =", + "lazy val sql_type: PackratParser[SQLType] =", "SELECT CAST(age AS BIGINT) AS a FROM emp", "1", EsSpecific, @@ -3411,7 +3411,7 @@ object DialectCensus { "NULL", "NULL", s"$S/parser/type/package.scala", - "def nullValue: PackratParser[Null.type] =", + "lazy val nullValue: PackratParser[Null.type] =", "SELECT COALESCE(nickname, NULL) AS n FROM emp", "0", EsSpecific, @@ -3425,7 +3425,7 @@ object DialectCensus { "TRUE", "TRUE", s"$S/parser/type/package.scala", - "def boolean: PackratParser[BooleanValue] =", + "lazy val boolean: PackratParser[BooleanValue] =", "SELECT id FROM emp WHERE active = TRUE", "0", AnsiAdjacent, @@ -3439,7 +3439,7 @@ object DialectCensus { "FALSE", "FALSE", s"$S/parser/type/package.scala", - "def boolean: PackratParser[BooleanValue] =", + "lazy val boolean: PackratParser[BooleanValue] =", "SELECT id FROM emp WHERE active = FALSE", "0", AnsiAdjacent, diff --git a/sql/src/test/scala/app/softnetwork/elastic/sql/parser/ParserConcurrencySpec.scala b/sql/src/test/scala/app/softnetwork/elastic/sql/parser/ParserConcurrencySpec.scala new file mode 100644 index 000000000..0afeed7ee --- /dev/null +++ b/sql/src/test/scala/app/softnetwork/elastic/sql/parser/ParserConcurrencySpec.scala @@ -0,0 +1,242 @@ +/* + * Copyright 2025 SOFTNETWORK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app.softnetwork.elastic.sql.parser + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +import java.net.{URL, URLClassLoader} +import java.util.concurrent.{CountDownLatch, TimeUnit} +import scala.collection.mutable + +/** The grammar's productions are `lazy val`s on one singleton (`object Parser`), which is the + * declaration form `PackratParsers` requires for its memo cache to key on a STABLE parser + * instance. That swaps per-call construction for shared, lazily-initialised state, and this suite + * is the guard on the one hazard that swap introduces: several threads reaching an + * as-yet-uninitialised production at the same time. + * + * Two legs, because they fail in different ways: + * + * 1. '''COLD''' - `object Parser` is loaded afresh in a child-first classloader, so EVERY + * production is uninitialised when N threads hit it simultaneously. This is the leg that + * would catch an initialisation deadlock or a half-built parser. It cannot be done against + * the ambient `Parser`: sbt runs `sql` tests unforked in one JVM, so by the time any suite + * runs the singleton may already be warm. 2. '''WARM''' - the ordinary `Parser`, N threads, + * results compared against the single-threaded answers. + * + * Both legs bound their wait: a deadlock must fail the suite, not hang the build forever. + * + * The structural argument this checks, for the record: a `lazy val` on an object initialises under + * that object's own monitor, so every production shares ONE lock and no lock-ordering inversion is + * possible between them; and because `PackratParsers.parser2packrat` takes its argument BY NAME + * (`p: => Parser[T]`, held in a local `lazy val`), a production's body is not evaluated when the + * production initialises - only when it is first APPLIED. Initialisation therefore never re-enters + * the grammar, which is what keeps the mutually recursive productions (`criteria` -> `predicate` + * -> `criteria`) safe as `lazy val`s. + */ +class ParserConcurrencySpec extends AnyFlatSpec with Matchers { + + /** Deliberately spread across the grammar - DQL, aggregation, window, JOIN, derived table, + * subquery, DDL, DML, geo, watcher, and a rejection - so that as many distinct productions as + * possible are initialised concurrently rather than one hot path being warmed by thread 1. + */ + private val statements: List[String] = List( + "SELECT a FROM t", + "SELECT * FROM orders WHERE status = 'OPEN' AND amount BETWEEN 1 AND 10", + "SELECT country, COUNT(customer_id) AS ct FROM orders GROUP BY country HAVING COUNT(customer_id) > 1", + "SELECT name, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn FROM emp", + "SELECT o.id, c.name FROM orders o INNER JOIN customers c ON o.customer_id = c.id", + "SELECT d.cid FROM (SELECT cid FROM customers WHERE region = 'EU') d", + "SELECT id FROM orders WHERE customer_id IN (SELECT id FROM customers WHERE region = 'EU')", + "CREATE TABLE IF NOT EXISTS users (id INT NOT NULL, name VARCHAR, PRIMARY KEY (id))", + "UPDATE orders SET status = 'CLOSED' WHERE id = 1", + "DELETE FROM orders WHERE id = 1", + "SELECT * FROM places WHERE distance(location, POINT(48.8, 2.3)) <= 10 km", + "SELECT department, STDDEV(salary) AS sd FROM emp GROUP BY department", + "SELECT MAX(DATE_PARSE(createdAt, '%Y-%m-%d')) AS m FROM t GROUP BY k", + "SHOW TABLES", + "SELECT a, FROM WHERE t GROUP 'x' HAVING" + ) + + private val Threads = 8 + private val Rounds = 40 + private val TimeoutSeconds = 120L + + /** Runs `body` on `Threads` threads released together by a latch, bounded by [[TimeoutSeconds]]. + * Returns each thread's per-statement answers. A thread that throws records the throwable's + * class and message in place of its answer, so an exception is a visible diff rather than a + * swallowed one. + */ + private def raced(body: String => String): (List[List[String]], List[Throwable]) = { + val start = new CountDownLatch(1) + val done = new CountDownLatch(Threads) + val answers = Array.fill(Threads)(List.empty[String]) + val errors = mutable.ListBuffer.empty[Throwable] + val threads = (0 until Threads).map { t => + val th = new Thread( + () => { + try { + start.await() + val acc = mutable.ListBuffer.empty[String] + for (round <- 0 until Rounds) { + // Each thread starts at a different offset so the threads are not walking the + // statement list in lockstep - that is what makes them race on DIFFERENT + // productions rather than queue on the same one. + val sql = statements((t + round) % statements.size) + acc += body(sql) + } + answers(t) = acc.toList + } catch { + case e: Throwable => errors.synchronized(errors += e) + } finally done.countDown() + }, + s"parser-race-$t" + ) + th.setDaemon(true) + th.start() + th + } + start.countDown() + val finished = done.await(TimeoutSeconds, TimeUnit.SECONDS) + threads.foreach(_.interrupt()) + if (!finished) { + fail( + s"$Threads threads did not finish within $TimeoutSeconds s - the grammar's lazy vals " + + "deadlocked or livelocked under concurrent initialisation" + ) + } + (answers.toList, errors.toList) + } + + private def expectedPerThread(body: String => String): List[List[String]] = + (0 until Threads).toList.map { t => + (0 until Rounds).toList.map(round => body(statements((t + round) % statements.size))) + } + + "the grammar" should "parse identically on 8 threads racing a COLD object Parser" in { + val loader = ColdParserLoader.build() + try { + val cold = ColdParserLoader.parseFunction(loader) + val (answers, errors) = raced(cold) + errors shouldBe empty + // The single-threaded expectation is computed on the SAME cold loader, after the race, so + // any answer the race corrupted shows up as a diff rather than being re-derived from it. + val expected = expectedPerThread(cold) + answers.zipWithIndex.foreach { case (got, t) => + withClue(s"thread $t: ") { got shouldBe expected(t) } + } + answers.flatten.count(_.startsWith("Left(")) should be > 0 + answers.flatten.count(_.startsWith("Right(")) should be > 0 + } finally loader.close() + } + + it should "parse identically on 8 threads racing the ambient object Parser" in { + val warm: String => String = sql => Parser(sql).toString + val expected = expectedPerThread(warm) + val (answers, errors) = raced(warm) + errors shouldBe empty + answers.zipWithIndex.foreach { case (got, t) => + withClue(s"thread $t: ") { got shouldBe expected(t) } + } + } + + it should "agree between the cold and the warm parser" in { + val loader = ColdParserLoader.build() + try { + val cold = ColdParserLoader.parseFunction(loader) + statements.foreach { sql => + withClue(s"[$sql] ") { cold(sql) shouldBe Parser(sql).toString } + } + } finally loader.close() + } +} + +/** Loads `app.softnetwork.elastic.sql.**` from a classloader with NO parent, so `object Parser` and + * every production it holds are initialised from scratch inside this suite. + * + * The URL set is derived from the code-source location of a handful of marker classes rather than + * from `java.class.path`: sbt runs `sql` tests unforked through its own layered classloader, so + * `java.class.path` is the sbt LAUNCHER's classpath and does not contain the project at all. A + * marker whose code source cannot be located fails the build loudly, naming the class - this + * harness must never degrade into "loaded the ambient classes after all", which would make the + * cold leg pass vacuously. + */ +private[parser] object ColdParserLoader { + + /** Markers whose code source pins one classpath entry each. They are only a FLOOR: the URL set is + * the union of these and every `URLClassLoader` in the ambient loader chain, because sbt's + * layered test loaders hold the bulk of the dependency classpath. + */ + private val markerNames: List[String] = List( + "app.softnetwork.elastic.sql.parser.Parser", + "scala.collection.immutable.List", + "scala.util.parsing.combinator.Parsers", + "com.typesafe.config.ConfigFactory", + "org.slf4j.Logger", + "com.fasterxml.jackson.databind.ObjectMapper", + "com.fasterxml.jackson.core.JsonFactory", + "com.fasterxml.jackson.annotation.JsonInclude", + "com.fasterxml.jackson.module.scala.DefaultScalaModule$" + ) + + def build(): URLClassLoader = { + val here = getClass.getClassLoader + val fromChain = Iterator + .iterate(here)(l => if (l == null) null else l.getParent) + .takeWhile(_ != null) + .collect { case u: URLClassLoader => u.getURLs.toList } + .flatten + .toList + val fromMarkers = markerNames.map { n => + val c = Class.forName(n, false, here) + Option(c.getProtectionDomain) + .flatMap(pd => Option(pd.getCodeSource)) + .flatMap(cs => Option(cs.getLocation)) + .getOrElse( + throw new IllegalStateException( + s"ColdParserLoader: cannot locate the code source of $n - the cold leg would " + + "silently fall back to the ambient classes" + ) + ) + } + val urls = (fromMarkers ++ fromChain).distinct + // parent = null => bootstrap only. Nothing resolves through the ambient app classloader, so + // `object Parser` here is a genuinely separate, uninitialised singleton. + new URLClassLoader(urls.toArray, null) + } + + /** `Parser.apply(String): Either[ParserError, Statement]`, reached reflectively because the cold + * `Either` is a different `Class` than ours. `toString` is the comparison surface: it renders + * the whole AST for a success and the parser reason for a rejection, so a verdict change and a + * render change are both visible. + */ + def parseFunction(loader: URLClassLoader): String => String = { + val cls = Class.forName("app.softnetwork.elastic.sql.parser.Parser$", false, loader) + // Gate integrity: if the class came back from the ambient loader (a parent that was not + // `null`, a marker that resolved through delegation) the "cold" leg would be re-running the + // WARM one and could never fail. Assert the separation before anything is parsed. + if (cls.getClassLoader ne loader) { + throw new IllegalStateException( + s"ColdParserLoader: object Parser was loaded by ${cls.getClassLoader} , not by the " + + "child-first loader - the cold leg would pass vacuously" + ) + } + val module = cls.getField("MODULE$").get(null) + val apply = cls.getMethod("apply", classOf[String]) + sql => String.valueOf(apply.invoke(module, sql)) + } +} diff --git a/sql/src/test/scala/app/softnetwork/elastic/sql/perf/GrammarDiffProbe.scala b/sql/src/test/scala/app/softnetwork/elastic/sql/perf/GrammarDiffProbe.scala new file mode 100644 index 000000000..a9099a38f --- /dev/null +++ b/sql/src/test/scala/app/softnetwork/elastic/sql/perf/GrammarDiffProbe.scala @@ -0,0 +1,316 @@ +/* + * Copyright 2025 SOFTNETWORK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app.softnetwork.elastic.sql.perf + +import app.softnetwork.elastic.sql.parser.Parser +import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} + +import java.nio.charset.StandardCharsets +import java.nio.file.{Files, Path, Paths} +import scala.collection.mutable +import scala.jdk.CollectionConverters._ + +/** Differential probe: feed ONE frozen corpus to the parser built from two different git trees and + * compare, per input, the VERDICT and the AST RENDER. + * + * Why it exists: a branch-only test suite is structurally blind to a narrowing, because the inputs + * a grammar change breaks are by construction the ones nobody wrote a test for - story 21.7 + * shipped a real narrowing past 912 green tests. The only instrument that can establish "no + * behaviour change" is a parser built from the OTHER tree, fed the same bytes. + * + * ==Two modes, and the corpus is FROZEN on purpose== + * + * {{{ + * # 1. once, on either tree - derive the corpus from disk and freeze it + * sbt "sql/Test/runMain app.softnetwork.elastic.sql.perf.GrammarDiffProbe --emit-corpus corpus.txt" + * + * # 2. once per tree - replay the frozen corpus + * sbt "sql/Test/runMain app.softnetwork.elastic.sql.perf.GrammarDiffProbe corpus.txt a.tsv" + * git checkout -- sql/src/main + * sbt "sql/Test/runMain app.softnetwork.elastic.sql.perf.GrammarDiffProbe corpus.txt b.tsv" + * git checkout HEAD -- sql/src/main + * diff a.tsv b.tsv + * }}} + * + * Freezing matters: part of the corpus is scraped from the test sources themselves, and those + * differ between the two trees. A corpus re-derived per run would silently compare two different + * input sets and report a difference that is an artefact of the harness. + * + * 🔴 `git checkout -- ` DESTROYS uncommitted work at that path. Commit first. 🔴 + * Never point this at a `sql/target//classes` produced by `clean` + `compile` in ONE sbt + * invocation: that skips `copyResources`, `softnetwork-sql.conf` is then absent and EVERY `CREATE + * TABLE` fails to parse - which looks exactly like the narrowing the probe exists to rule out. A + * plain `sql/Test/runMain` depends on `Test/compile` and is safe. + * + * ==Corpus== + * Five layers, deliberately weighted towards inputs nobody would write a test for: + * 1. every SQL-shaped cell of every the CSVs under `sql/src/test/resources/corpus` (the BI + * census, the Tableau live capture, the pre-epic-21 baseline, the attribution set); 2. every + * `examples[].sql` and every `syntax[]` line of the help corpus (under + * `core/src/main/resources/help`); 3. every string literal in the `sql` test sources that + * starts with a SQL verb - malformed extractions included, since for a DIFFERENTIAL probe any + * byte string is a valid input; 4. every whitespace-boundary PREFIX of layers 1-3 (an + * unterminated statement is where a changed alternation shows first); 5. targeted mutations - + * stray `(` / `)`, dangling `AND` / `OR`, a misspelt keyword, a trailing comma, a doubled + * quote. + * + * ==Output== + * One TSV row per input: index, verdict (`right` | `left` | `throw`), and the detail - the AST + * render (`Statement.sql`) plus its `toString` for a success, the `ParserError` message for a + * rejection, the throwable class and message for a throw. Tabs, CRs and LFs are escaped so a row + * is one line. Comparing the two files with `diff` is the whole verdict: any differing line is a + * behaviour change. + */ +object GrammarDiffProbe { + + private val SqlVerb = + "(?is)^\\s*(SELECT|WITH|CREATE|ALTER|DROP|INSERT|UPDATE|DELETE|SHOW|TRUNCATE|COPY|REFRESH|EXPLAIN|DESCRIBE|SET|GRANT|REVOKE|CALL|USE)\\b.*" + + private def looksLikeSql(s: String): Boolean = + s.length > 5 && s.matches(SqlVerb) + + // ---------------------------------------------------------------- corpus layers + + /** RFC-4180: a quoted field may hold commas, CRs and LFs; `""` is a literal quote. */ + private[perf] def parseCsv(content: String): List[List[String]] = { + val rows = mutable.ListBuffer.empty[List[String]] + val row = mutable.ListBuffer.empty[String] + val cell = new StringBuilder + var inQuotes = false + var i = 0 + while (i < content.length) { + val c = content.charAt(i) + if (inQuotes) { + if (c == '"') { + if (i + 1 < content.length && content.charAt(i + 1) == '"') { cell.append('"'); i += 1 } + else inQuotes = false + } else cell.append(c) + } else { + c match { + case '"' => inQuotes = true + case ',' => row += cell.toString; cell.setLength(0) + case '\r' => () + case '\n' => row += cell.toString; cell.setLength(0); rows += row.toList; row.clear() + case other => cell.append(other) + } + } + i += 1 + } + if (cell.nonEmpty || row.nonEmpty) { row += cell.toString; rows += row.toList } + rows.toList + } + + private def fromCsvCorpora(root: Path): List[String] = { + val dir = root.resolve("sql/src/test/resources/corpus") + require(Files.isDirectory(dir), s"GrammarDiffProbe: corpus directory not found at $dir") + val files = Files + .list(dir) + .iterator() + .asScala + .filter(_.toString.endsWith(".csv")) + .toList + .sortBy(_.toString) + require(files.nonEmpty, s"GrammarDiffProbe: no CSV under $dir") + files.flatMap { f => + parseCsv(new String(Files.readAllBytes(f), StandardCharsets.UTF_8)).flatten + .filter(looksLikeSql) + } + } + + private def fromHelpCorpus(root: Path): List[String] = { + val dir = root.resolve("core/src/main/resources/help") + require(Files.isDirectory(dir), s"GrammarDiffProbe: help corpus not found at $dir") + val mapper = new ObjectMapper() + val files = Files + .walk(dir) + .iterator() + .asScala + .filter(p => Files.isRegularFile(p) && p.toString.endsWith(".json")) + .toList + .sortBy(_.toString) + require(files.nonEmpty, s"GrammarDiffProbe: no help JSON under $dir") + files.flatMap { f => + val node: JsonNode = mapper.readTree(Files.readAllBytes(f)) + val examples = + Option(node.get("examples")).toList.flatMap(_.iterator().asScala.toList).flatMap { e => + Option(e.get("sql")).map(_.asText()).toList + } + val syntax = + Option(node.get("syntax")).toList.flatMap(_.iterator().asScala.toList).map(_.asText()) + (examples ++ syntax).filter(looksLikeSql) + } + } + + /** Every double-quoted (or triple-quoted) literal in the sql test sources that starts with a SQL + * verb. The scanner is deliberately simple; a mis-split literal is still a legitimate probe + * input, it just is not the statement its author wrote. + */ + private def fromTestSources(root: Path): List[String] = { + val dir = root.resolve("sql/src/test/scala") + require(Files.isDirectory(dir), s"GrammarDiffProbe: test sources not found at $dir") + val out = mutable.ListBuffer.empty[String] + Files + .walk(dir) + .iterator() + .asScala + .filter(p => Files.isRegularFile(p) && p.toString.endsWith(".scala")) + .toList + .sortBy(_.toString) + .foreach { f => + val src = new String(Files.readAllBytes(f), StandardCharsets.UTF_8) + var i = 0 + while (i < src.length) { + if (src.startsWith("\"\"\"", i)) { + val end = src.indexOf("\"\"\"", i + 3) + if (end < 0) i = src.length + else { out += src.substring(i + 3, end); i = end + 3 } + } else if (src.charAt(i) == '"') { + val sb = new StringBuilder + var j = i + 1 + var closed = false + while (j < src.length && !closed) { + val c = src.charAt(j) + if (c == '\\' && j + 1 < src.length) { + src.charAt(j + 1) match { + case 'n' => sb.append('\n') + case 't' => sb.append('\t') + case 'r' => sb.append('\r') + case other => sb.append(other) + } + j += 2 + } else if (c == '"') { closed = true; j += 1 } + else if (c == '\n') { j = src.length } + else { sb.append(c); j += 1 } + } + out += sb.toString + i = j + } else i += 1 + } + } + out.toList.map(_.replace("|", "")).filter(looksLikeSql) + } + + private def prefixes(s: String): List[String] = { + val out = mutable.ListBuffer.empty[String] + var i = 1 + while (i < s.length) { + if (s.charAt(i).isWhitespace && !s.charAt(i - 1).isWhitespace) out += s.substring(0, i) + i += 1 + } + out.toList + } + + private def mutations(s: String): List[String] = List( + s + " (", + s + ")", + s + " AND", + s + " OR", + s + ",", + s + " '", + "(" + s, + s.replaceFirst("(?i)\\bWHERE\\b", "WHEREE"), + s.replaceFirst("(?i)\\bFROM\\b", "FROMM"), + s.replaceFirst("(?i)\\bSELECT\\b", "SELEC") + ) + + private[perf] def buildCorpus(root: Path): List[String] = { + val base = (fromCsvCorpora(root) ++ fromHelpCorpus(root) ++ fromTestSources(root)) + .map(_.trim) + .filter(_.nonEmpty) + .distinct + val withPrefixes = base.flatMap(prefixes) + // Mutating every base statement would be ~10x the base for very little extra discrimination; + // a deterministic stride keeps the set large without making the run unbounded. + val mutated = base.zipWithIndex.collect { case (s, i) if i % 3 == 0 => mutations(s) }.flatten + (base ++ withPrefixes ++ mutated).map(_.trim).filter(_.nonEmpty).distinct + } + + // ---------------------------------------------------------------- replay + + private def escape(s: String): String = + s.replace("\\", "\\\\").replace("\t", "\\t").replace("\r", "\\r").replace("\n", "\\n") + + private[perf] def verdictOf(sql: String): String = + try { + Parser(sql) match { + case Right(stmt) => + val rendered = + try escape(stmt.sql) + catch { case t: Throwable => s"render-threw:${t.getClass.getName}:${t.getMessage}" } + s"right\t$rendered\t${escape(String.valueOf(stmt))}" + case Left(err) => s"left\t${escape(String.valueOf(err.msg))}\t" + } + } catch { + case t: Throwable => s"throw\t${t.getClass.getName}\t${escape(String.valueOf(t.getMessage))}" + } + + def main(args: Array[String]): Unit = { + val root = Paths.get("").toAbsolutePath + if (args.length == 2 && args(0) == "--emit-corpus") { + val corpus = buildCorpus(root) + Files.write( + Paths.get(args(1)), + corpus.map(escape).mkString("\n").getBytes(StandardCharsets.UTF_8) + ) + println(s"GrammarDiffProbe: wrote ${corpus.size} inputs to ${args(1)}") + return + } + if (args.length != 2) { + throw new RuntimeException( + "usage: GrammarDiffProbe --emit-corpus | GrammarDiffProbe " + ) + } + val corpusPath = Paths.get(args(0)) + if (!Files.isRegularFile(corpusPath)) { + throw new RuntimeException( + s"GrammarDiffProbe: corpus not found at ${corpusPath.toAbsolutePath}" + ) + } + val inputs = new String(Files.readAllBytes(corpusPath), StandardCharsets.UTF_8) + .split("\n") + .toList + .filter(_.nonEmpty) + .map(unescape) + if (inputs.isEmpty) { + throw new RuntimeException(s"GrammarDiffProbe: corpus at $corpusPath has 0 inputs") + } + val sb = new StringBuilder + inputs.zipWithIndex.foreach { case (sql, i) => + sb.append(s"$i\t${escape(sql)}\t${verdictOf(sql)}\n") + } + Files.write(Paths.get(args(1)), sb.toString.getBytes(StandardCharsets.UTF_8)) + println(s"GrammarDiffProbe: replayed ${inputs.size} inputs -> ${args(1)}") + } + + private[perf] def unescape(s: String): String = { + val sb = new StringBuilder + var i = 0 + while (i < s.length) { + if (s.charAt(i) == '\\' && i + 1 < s.length) { + s.charAt(i + 1) match { + case 'n' => sb.append('\n') + case 't' => sb.append('\t') + case 'r' => sb.append('\r') + case '\\' => sb.append('\\') + case other => sb.append('\\').append(other) + } + i += 2 + } else { sb.append(s.charAt(i)); i += 1 } + } + sb.toString + } +} diff --git a/sql/src/test/scala/app/softnetwork/elastic/sql/perf/ParseCostProbe.scala b/sql/src/test/scala/app/softnetwork/elastic/sql/perf/ParseCostProbe.scala new file mode 100644 index 000000000..43f1e6534 --- /dev/null +++ b/sql/src/test/scala/app/softnetwork/elastic/sql/perf/ParseCostProbe.scala @@ -0,0 +1,186 @@ +/* + * Copyright 2025 SOFTNETWORK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app.softnetwork.elastic.sql.perf + +import app.softnetwork.elastic.sql.parser.Parser + +/** Single-statement parse-latency probe for the SQL grammar. + * + * Every query the engine answers is parsed first, so per-statement parse latency is a cost the + * user pays on every single request - which is what this probe measures. Suite wall time + * (`sql/testOnly *ParserSpec`) is a useful secondary instrument but it aggregates thousands of + * parses behind fixture construction and ScalaTest overhead. + * + * Run it with: + * + * {{{ + * sbt "sql/Test/runMain app.softnetwork.elastic.sql.perf.ParseCostProbe" + * sbt "sql/Test/runMain app.softnetwork.elastic.sql.perf.ParseCostProbe 300 2000 out.tsv" + * }}} + * + * Arguments, all optional and positional: `warmup` (default [[DefaultWarmup]] = 300 discarded + * iterations per statement), `runs` (default [[DefaultRuns]] = 1000 timed iterations per + * statement) and an output path - when given, the same table is also written there as TSV so two + * runs (e.g. one per git tree) can be diffed mechanically. + * + * ==Warm-up== + * The JIT needs a few hundred iterations to compile the combinator hot path; an unwarmed first + * parse of `SELECT a FROM t` measures the interpreter and the `Parser` object's own lazy + * initialisation, not the grammar. The probe therefore runs a **global** warm-up over the whole + * statement set (so `object Parser` is fully initialised before any timing starts) and then + * [[DefaultWarmup]] further **discarded** iterations per statement immediately before that + * statement's timed loop. Those warm-up iterations are never included in any statistic. + * + * ==This probe NEVER asserts== + * It prints and exits. A timing assertion in the test suite is a CI-flake liability - the older + * `ParseCostProbeSpec` had to have its 1 ms ceiling relaxed to 10 ms (issues #269/#270) after it + * reddened two PRs whose diffs touched no parse path, on loaded GitHub runners. Nothing here is + * collected by `sbt test`: this is a `main`-bearing object, not a ScalaTest suite. + * + * A failure (bad argument, unparseable probe statement that was expected to parse) is raised as a + * thrown exception, never `sys.exit`: this build sets no `fork` key on `sql`, so `runMain` + * executes in the sbt JVM where `sys.exit` would go through sbt's `TrapExit` SecurityManager - + * deprecated for removal from JDK 17. + */ +object ParseCostProbe { + + /** Discarded iterations per statement, before its timed loop. */ + val DefaultWarmup: Int = 300 + + /** Timed iterations per statement. */ + val DefaultRuns: Int = 1000 + + /** label -> statement. One entry per grammar shape a user actually sends, plus a deliberate + * rejection: refusing a malformed statement is a cost too, and it is the one shape where the + * parser explores every alternative before giving up. + */ + val Statements: List[(String, String)] = List( + "bare SELECT" -> + "SELECT a FROM t", + "SELECT list + LIMIT" -> + "SELECT id, name, category, amount, created_at FROM orders LIMIT 100", + "WHERE heavy" -> + ("SELECT * FROM orders WHERE (status = 'OPEN' AND amount > 10.5) OR " + + "(country <> 'USA' AND city LIKE '%berlin%') AND id IN (1,2,3) AND " + + "created_at BETWEEN '2024-01-01' AND '2024-12-31' AND label IS NOT NULL"), + "GROUP BY + HAVING" -> + ("SELECT country, city, COUNT(customer_id) AS ct, MAX(amount) AS mx FROM orders " + + "WHERE amount > 0 GROUP BY country, city " + + "HAVING Country <> 'USA' AND COUNT(customer_id) > 1 ORDER BY ct DESC LIMIT 10"), + "window function" -> + ("SELECT name, salary, ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) AS rn " + + "FROM emp LIMIT 100"), + "JOIN" -> + "SELECT o.id, c.name FROM orders o INNER JOIN customers c ON o.customer_id = c.id LIMIT 50", + "derived table" -> + "SELECT d.cid FROM (SELECT cid FROM customers WHERE region = 'EU') d WHERE d.cid > 10", + "WHERE subquery" -> + ("SELECT id FROM orders WHERE customer_id IN " + + "(SELECT id FROM customers WHERE region = 'EU') AND amount > 100"), + "DDL CREATE TABLE" -> + ("CREATE TABLE IF NOT EXISTS users (id INT NOT NULL, name VARCHAR " + + "FIELDS(raw Keyword) DEFAULT 'anonymous', birthdate DATE, " + + "age INT SCRIPT AS (DATEDIFF(birthdate, CURRENT_DATE, YEAR)), PRIMARY KEY (id))"), + "rejection (malformed)" -> + "SELECT a, FROM WHERE t GROUP 'x' HAVING" + ) + + private def median(sorted: Array[Long]): Long = { + val n = sorted.length + if (n % 2 == 1) sorted(n / 2) else (sorted(n / 2 - 1) + sorted(n / 2)) / 2 + } + + /** Locale-independent on purpose: this machine's default locale renders a decimal comma, which + * makes the printed table unparseable by anything downstream. + */ + private def micros(nanos: Long): String = + String.format(java.util.Locale.ROOT, "%9.1f", java.lang.Double.valueOf(nanos / 1000.0)) + + def main(args: Array[String]): Unit = { + val warmup = if (args.length > 0) args(0).toInt else DefaultWarmup + val runs = if (args.length > 1) args(1).toInt else DefaultRuns + val out = if (args.length > 2) Some(args(2)) else None + if (warmup < 0 || runs < 1) { + throw new RuntimeException( + s"ParseCostProbe: warmup must be >= 0 and runs >= 1 (got warmup=$warmup runs=$runs)" + ) + } + + // Global warm-up: forces `object Parser` initialisation and lets the JIT compile the + // combinator hot path before ANY statement is timed. Without it the first statement in the + // list absorbs the whole initialisation cost and looks pathological. + var sink = 0 + for (_ <- 1 to warmup; (_, sql) <- Statements) sink ^= Parser(sql).hashCode() + + val lines = Statements.map { case (label, sql) => + for (_ <- 1 to warmup) sink ^= Parser(sql).hashCode() + val samples = new Array[Long](runs) + var i = 0 + while (i < runs) { + val t0 = System.nanoTime() + val r = Parser(sql) + val t1 = System.nanoTime() + sink ^= r.hashCode() + samples(i) = t1 - t0 + i += 1 + } + java.util.Arrays.sort(samples) + val verdict = if (Parser(sql).isRight) "parses" else "rejected" + ( + label, + verdict, + runs, + median(samples), + samples(0), + samples(runs - 1), + samples(math.min(runs - 1, (runs.toLong * 95L / 100L).toInt)) + ) + } + + val header = + f"${"statement"}%-24s ${"verdict"}%-9s ${"runs"}%6s ${"median us"}%10s ${"min us"}%10s ${"p95 us"}%10s ${"max us"}%10s" + println() + println(s"ParseCostProbe - warmup=$warmup runs=$runs (warm-up iterations are discarded)") + println(header) + println("-" * header.length) + lines.foreach { case (label, verdict, n, med, min, max, p95) => + println( + f"$label%-24s $verdict%-9s $n%6d ${micros(med)}%10s ${micros(min)}%10s ${micros(p95)}%10s ${micros(max)}%10s" + ) + } + val total = lines.map(_._4).sum + println("-" * header.length) + println(f"sum of medians: ${micros(total)}%s us") + println() + + out.foreach { path => + val sb = new StringBuilder + sb.append("statement\tverdict\truns\tmedian_ns\tmin_ns\tp95_ns\tmax_ns\n") + lines.foreach { case (label, verdict, n, med, min, max, p95) => + sb.append(s"$label\t$verdict\t$n\t$med\t$min\t$p95\t$max\n") + } + java.nio.file.Files.write( + java.nio.file.Paths.get(path), + sb.toString.getBytes(java.nio.charset.StandardCharsets.UTF_8) + ) + println(s"ParseCostProbe: wrote $path") + } + + // Keep `sink` observable so the JIT cannot eliminate the parses as dead code. + if (sink == Int.MinValue) println("unreachable sink guard") + } +}