diff --git a/build.sbt b/build.sbt index 3bf483e7..3712b2b9 100644 --- a/build.sbt +++ b/build.sbt @@ -19,10 +19,14 @@ ThisBuild / organization := "app.softnetwork" name := "softclient4es" -ThisBuild / version := "0.9.2" +ThisBuild / version := "0.9.3" ThisBuild / scalaVersion := scala213 +ThisBuild / organizationName := "SOFTNETWORK" +ThisBuild / startYear := Some(2025) +ThisBuild / licenses += ("Apache-2.0", url("https://www.apache.org/licenses/LICENSE-2.0.txt")) + ThisBuild / dependencyOverrides ++= Seq( "com.fasterxml.jackson.module" %% "jackson-module-scala" % Versions.jackson, "com.github.jnr" % "jnr-ffi" % "2.2.17", diff --git a/core/src/main/scala-2.13/app/softnetwork/elastic/client/ElasticConfig.scala b/core/src/main/scala-2.13/app/softnetwork/elastic/client/ElasticConfig.scala index 9c230d39..93c325d9 100644 --- a/core/src/main/scala-2.13/app/softnetwork/elastic/client/ElasticConfig.scala +++ b/core/src/main/scala-2.13/app/softnetwork/elastic/client/ElasticConfig.scala @@ -1,3 +1,19 @@ +/* + * 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.client import com.typesafe.config.{Config, ConfigFactory} diff --git a/core/src/main/scala/app/softnetwork/elastic/client/AggregateResult.scala b/core/src/main/scala/app/softnetwork/elastic/client/AggregateResult.scala index f5544489..4b6468cf 100644 --- a/core/src/main/scala/app/softnetwork/elastic/client/AggregateResult.scala +++ b/core/src/main/scala/app/softnetwork/elastic/client/AggregateResult.scala @@ -1,3 +1,19 @@ +/* + * 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.client import app.softnetwork.elastic.sql.function.aggregate.AggregateFunction diff --git a/core/src/main/scala/app/softnetwork/elastic/client/ElasticClientApi.scala b/core/src/main/scala/app/softnetwork/elastic/client/ElasticClientApi.scala index 418eca87..dc8206cf 100644 --- a/core/src/main/scala/app/softnetwork/elastic/client/ElasticClientApi.scala +++ b/core/src/main/scala/app/softnetwork/elastic/client/ElasticClientApi.scala @@ -1,3 +1,19 @@ +/* + * 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.client import java.time.LocalDate diff --git a/core/src/main/scala/app/softnetwork/elastic/client/MappingComparator.scala b/core/src/main/scala/app/softnetwork/elastic/client/MappingComparator.scala index dfa1ca37..f9b9d1b3 100644 --- a/core/src/main/scala/app/softnetwork/elastic/client/MappingComparator.scala +++ b/core/src/main/scala/app/softnetwork/elastic/client/MappingComparator.scala @@ -1,3 +1,19 @@ +/* + * 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.client import com.google.gson._ diff --git a/core/src/main/scala/app/softnetwork/elastic/client/package.scala b/core/src/main/scala/app/softnetwork/elastic/client/package.scala index a1883f77..c6174aa7 100644 --- a/core/src/main/scala/app/softnetwork/elastic/client/package.scala +++ b/core/src/main/scala/app/softnetwork/elastic/client/package.scala @@ -1,3 +1,19 @@ +/* + * 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 import akka.stream.{Attributes, FlowShape, Inlet, Outlet} diff --git a/core/src/main/scala/app/softnetwork/elastic/persistence/query/ElasticProvider.scala b/core/src/main/scala/app/softnetwork/elastic/persistence/query/ElasticProvider.scala index bf45187b..cdeed9e8 100644 --- a/core/src/main/scala/app/softnetwork/elastic/persistence/query/ElasticProvider.scala +++ b/core/src/main/scala/app/softnetwork/elastic/persistence/query/ElasticProvider.scala @@ -1,3 +1,19 @@ +/* + * 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.persistence.query import app.softnetwork.elastic.client.ElasticClientApi diff --git a/core/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStream.scala b/core/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStream.scala index 88af3f3b..55df2cea 100644 --- a/core/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStream.scala +++ b/core/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStream.scala @@ -1,3 +1,19 @@ +/* + * 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.persistence.query import app.softnetwork.persistence.ManifestWrapper diff --git a/core/src/main/scala/app/softnetwork/elastic/persistence/typed/Elastic.scala b/core/src/main/scala/app/softnetwork/elastic/persistence/typed/Elastic.scala index 1efe4d59..8f4cdc67 100644 --- a/core/src/main/scala/app/softnetwork/elastic/persistence/typed/Elastic.scala +++ b/core/src/main/scala/app/softnetwork/elastic/persistence/typed/Elastic.scala @@ -1,3 +1,19 @@ +/* + * 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.persistence.typed import app.softnetwork.persistence._ diff --git a/documentation/request_structure.md b/documentation/request_structure.md index 1ad37f1d..f942df23 100644 --- a/documentation/request_structure.md +++ b/documentation/request_structure.md @@ -45,7 +45,7 @@ Expand an array / nested field into rows. Mapped to Elasticsearch `nested` and i ```sql SELECT id, phone FROM customers -JOIN UNNEST(phones) AS phone; +JOIN UNNEST(customers.phones) AS phone; ``` --- diff --git a/es6/jest/src/main/scala/app/softnetwork/elastic/client/jest/JestClientApi.scala b/es6/jest/src/main/scala/app/softnetwork/elastic/client/jest/JestClientApi.scala index 2af5369f..f913170c 100644 --- a/es6/jest/src/main/scala/app/softnetwork/elastic/client/jest/JestClientApi.scala +++ b/es6/jest/src/main/scala/app/softnetwork/elastic/client/jest/JestClientApi.scala @@ -1,3 +1,19 @@ +/* + * 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.client.jest import akka.NotUsed diff --git a/es6/jest/src/main/scala/app/softnetwork/elastic/client/jest/JestClientCompanion.scala b/es6/jest/src/main/scala/app/softnetwork/elastic/client/jest/JestClientCompanion.scala index 0ed8655e..497d1afe 100644 --- a/es6/jest/src/main/scala/app/softnetwork/elastic/client/jest/JestClientCompanion.scala +++ b/es6/jest/src/main/scala/app/softnetwork/elastic/client/jest/JestClientCompanion.scala @@ -1,3 +1,19 @@ +/* + * 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.client.jest import app.softnetwork.elastic.client.{ElasticConfig, ElasticCredentials} diff --git a/es6/jest/src/main/scala/app/softnetwork/elastic/client/jest/JestClientResultHandler.scala b/es6/jest/src/main/scala/app/softnetwork/elastic/client/jest/JestClientResultHandler.scala index fc05e2e2..6c2e22ae 100644 --- a/es6/jest/src/main/scala/app/softnetwork/elastic/client/jest/JestClientResultHandler.scala +++ b/es6/jest/src/main/scala/app/softnetwork/elastic/client/jest/JestClientResultHandler.scala @@ -1,3 +1,19 @@ +/* + * 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.client.jest import io.searchbox.action.Action diff --git a/es6/jest/src/main/scala/app/softnetwork/elastic/persistence/query/JestProvider.scala b/es6/jest/src/main/scala/app/softnetwork/elastic/persistence/query/JestProvider.scala index 1cdb739d..c806e401 100644 --- a/es6/jest/src/main/scala/app/softnetwork/elastic/persistence/query/JestProvider.scala +++ b/es6/jest/src/main/scala/app/softnetwork/elastic/persistence/query/JestProvider.scala @@ -1,3 +1,19 @@ +/* + * 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.persistence.query import app.softnetwork.elastic.client.jest.JestClientApi diff --git a/es6/jest/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithJestProvider.scala b/es6/jest/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithJestProvider.scala index 673457b9..5501a386 100644 --- a/es6/jest/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithJestProvider.scala +++ b/es6/jest/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithJestProvider.scala @@ -1,3 +1,19 @@ +/* + * 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.persistence.query import app.softnetwork.persistence.message.CrudEvent diff --git a/es6/rest/src/main/scala/app/softnetwork/elastic/client/rest/RestHighLevelClientApi.scala b/es6/rest/src/main/scala/app/softnetwork/elastic/client/rest/RestHighLevelClientApi.scala index 2b93ffa4..4e56975c 100644 --- a/es6/rest/src/main/scala/app/softnetwork/elastic/client/rest/RestHighLevelClientApi.scala +++ b/es6/rest/src/main/scala/app/softnetwork/elastic/client/rest/RestHighLevelClientApi.scala @@ -1,3 +1,19 @@ +/* + * 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.client.rest import akka.NotUsed diff --git a/es6/rest/src/main/scala/app/softnetwork/elastic/client/rest/RestHighLevelClientCompanion.scala b/es6/rest/src/main/scala/app/softnetwork/elastic/client/rest/RestHighLevelClientCompanion.scala index 2379923d..cb3098d7 100644 --- a/es6/rest/src/main/scala/app/softnetwork/elastic/client/rest/RestHighLevelClientCompanion.scala +++ b/es6/rest/src/main/scala/app/softnetwork/elastic/client/rest/RestHighLevelClientCompanion.scala @@ -1,3 +1,19 @@ +/* + * 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.client.rest import app.softnetwork.elastic.client.ElasticConfig diff --git a/es6/rest/src/main/scala/app/softnetwork/elastic/persistence/query/RestHighLevelClientProvider.scala b/es6/rest/src/main/scala/app/softnetwork/elastic/persistence/query/RestHighLevelClientProvider.scala index b76c13b4..7a93fcf9 100644 --- a/es6/rest/src/main/scala/app/softnetwork/elastic/persistence/query/RestHighLevelClientProvider.scala +++ b/es6/rest/src/main/scala/app/softnetwork/elastic/persistence/query/RestHighLevelClientProvider.scala @@ -1,3 +1,19 @@ +/* + * 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.persistence.query import app.softnetwork.elastic.client.rest.RestHighLevelClientApi diff --git a/es6/rest/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithRestProvider.scala b/es6/rest/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithRestProvider.scala index 3f79bb07..2e6eee32 100644 --- a/es6/rest/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithRestProvider.scala +++ b/es6/rest/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithRestProvider.scala @@ -1,3 +1,19 @@ +/* + * 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.persistence.query import app.softnetwork.persistence.message.CrudEvent diff --git a/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticAggregation.scala b/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticAggregation.scala index 991621a5..2e427256 100644 --- a/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticAggregation.scala +++ b/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticAggregation.scala @@ -1,5 +1,22 @@ +/* + * 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.bridge +import app.softnetwork.elastic.sql.PainlessContext import app.softnetwork.elastic.sql.query.{ Asc, Bucket, @@ -105,8 +122,9 @@ object ElasticAggregation { buildScript: (String, Script) => Aggregation ): Aggregation = { if (transformFuncs.nonEmpty) { - val scriptSrc = identifier.painless() - val script = Script(scriptSrc).lang("painless") + val context = PainlessContext() + val scriptSrc = identifier.painless(Some(context)) + val script = Script(s"$context$scriptSrc").lang("painless") buildScript(aggName, script) } else { buildField(aggName, sourceField) @@ -145,7 +163,7 @@ object ElasticAggregation { .copy( scripts = th.fields .filter(_.isScriptField) - .map(f => f.sourceField -> Script(f.painless()).lang("painless")) + .map(f => f.sourceField -> Script(f.painless(None)).lang("painless")) .toMap ) .size(limit) sortBy th.orderBy.sorts.map(sort => diff --git a/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticCriteria.scala b/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticCriteria.scala index f117d0a9..caa12dd0 100644 --- a/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticCriteria.scala +++ b/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticCriteria.scala @@ -1,3 +1,19 @@ +/* + * 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.bridge import app.softnetwork.elastic.sql.query.Criteria diff --git a/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticMultiSearchRequest.scala b/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticMultiSearchRequest.scala index 61925b7b..0a904a6d 100644 --- a/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticMultiSearchRequest.scala +++ b/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticMultiSearchRequest.scala @@ -1,3 +1,19 @@ +/* + * 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.bridge import com.sksamuel.elastic4s.http.search.MultiSearchBuilderFn diff --git a/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticQuery.scala b/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticQuery.scala index c99899f4..2b3c9d6b 100644 --- a/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticQuery.scala +++ b/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticQuery.scala @@ -1,3 +1,19 @@ +/* + * 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.bridge import app.softnetwork.elastic.sql.operator.AND @@ -6,7 +22,6 @@ import app.softnetwork.elastic.sql.query.{ ElasticBoolQuery, ElasticChild, ElasticFilter, - ElasticMatch, ElasticNested, ElasticParent, GenericExpression, @@ -15,6 +30,7 @@ import app.softnetwork.elastic.sql.query.{ IsNotNullExpr, IsNullCriteria, IsNullExpr, + MatchCriteria, NestedElement, NestedElements, Predicate @@ -152,9 +168,9 @@ case class ElasticQuery(filter: ElasticFilter) { case in: InExpr[_, _] => in case between: BetweenExpr => between // case geoDistance: DistanceCriteria => geoDistance - case matchExpression: ElasticMatch => matchExpression - case isNull: IsNullCriteria => isNull - case isNotNull: IsNotNullCriteria => isNotNull + case matchExpression: MatchCriteria => matchExpression + case isNull: IsNullCriteria => isNull + case isNotNull: IsNotNullCriteria => isNotNull case other => throw new IllegalArgumentException(s"Unsupported filter type: ${other.getClass.getName}") } diff --git a/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticSearchRequest.scala b/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticSearchRequest.scala index bac7afb9..3afcdc6e 100644 --- a/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticSearchRequest.scala +++ b/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticSearchRequest.scala @@ -1,3 +1,19 @@ +/* + * 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.bridge import app.softnetwork.elastic.sql.query.{Bucket, Criteria, Except, Field} diff --git a/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/package.scala b/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/package.scala index d0a8c4f6..c13ee796 100644 --- a/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/package.scala +++ b/es6/sql-bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/package.scala @@ -1,3 +1,19 @@ +/* + * 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 import app.softnetwork.elastic.sql.`type`.{SQLBigInt, SQLDouble, SQLTemporal, SQLVarchar} @@ -23,27 +39,69 @@ package object bridge { implicit def requestToNestedFilterAggregation( request: SQLSearchRequest, innerHitsName: String - ): Option[FilterAggregation] = - request.where.flatMap(_.criteria) match { - case Some(f) => - f.nestedCriteria(innerHitsName) match { - case Nil => None - case cs => - val boolQuery = ElasticBoolQuery(group = true) - cs.map(c => boolQuery.filter(c.asFilter(Option(boolQuery)))) - Some( - filterAgg( - s"filtered_$innerHitsName", + ): Option[FilterAggregation] = { + val having: Option[Query] = + request.having.flatMap(_.criteria) match { + case Some(f) => + f.nestedCriteria(innerHitsName) match { + case Nil => None + case cs => + val boolQuery = ElasticBoolQuery(group = true) + cs.map(c => boolQuery.filter(c.asFilter(Option(boolQuery)))) + Some( boolQuery.query( request.aggregates.flatMap(_.identifier.innerHitsName).toSet, Option(boolQuery) ) ) - ) - } + } + case _ => + None + } + val where: Option[Query] = + request.where.flatMap(_.criteria) match { + case Some(f) => + f.nestedCriteria(innerHitsName) match { + case Nil => None + case cs => + val boolQuery = ElasticBoolQuery(group = true) + cs.map(c => boolQuery.filter(c.asFilter(Option(boolQuery)))) + Some( + boolQuery.query( + request.aggregates.flatMap(_.identifier.innerHitsName).toSet, + Option(boolQuery) + ) + ) + } + case _ => + None + } + (having, where) match { + case (Some(h), Some(w)) => + Some( + filterAgg( + s"filtered_$innerHitsName", + boolQuery().filter(h, w) + ) + ) + case (Some(h), None) => + Some( + filterAgg( + s"filtered_$innerHitsName", + h + ) + ) + case (None, Some(w)) => + Some( + filterAgg( + s"filtered_$innerHitsName", + w + ) + ) case _ => None } + } implicit def requestToFilterAggregation( request: SQLSearchRequest @@ -295,9 +353,11 @@ package object bridge { case Nil => _search case _ => _search scriptfields scriptFields.map { field => + val context = PainlessContext() + val script = field.painless(Some(context)) scriptField( field.scriptName, - Script(script = field.painless()) + Script(script = s"$context$script") .lang("painless") .scriptType("source") .params(field.identifier.functions.headOption match { @@ -352,7 +412,11 @@ package object bridge { case _ => true })) ) { - return scriptQuery(Script(script = painless()).lang("painless").scriptType("source")) + val context = PainlessContext() + val script = painless(Some(context)) + return scriptQuery( + Script(script = s"$context$script").lang("painless").scriptType("source") + ) } // Geo distance special case identifier.functions.headOption match { @@ -566,10 +630,22 @@ package object bridge { case NE | DIFF => not(rangeQuery(identifier.name) gte script lte script) } case _ => - scriptQuery(Script(script = painless()).lang("painless").scriptType("source")) + val context = PainlessContext() + val script = painless(Some(context)) + scriptQuery( + Script(script = s"$context$script") + .lang("painless") + .scriptType("source") + ) } case _ => - scriptQuery(Script(script = painless()).lang("painless").scriptType("source")) + val context = PainlessContext() + val script = painless(Some(context)) + scriptQuery( + Script(script = s"$context$script") + .lang("painless") + .scriptType("source") + ) } case _ => matchAllQuery() } @@ -723,7 +799,7 @@ package object bridge { case _ => scriptQuery( Script( - script = distanceCriteria.painless(), + script = distanceCriteria.painless(None), lang = Some("painless"), scriptType = Source, params = distance.params @@ -733,7 +809,7 @@ package object bridge { } implicit def matchToQuery( - matchExpression: ElasticMatch + matchExpression: MatchCriteria ): Query = { import matchExpression._ matchQuery(identifier.name, value.value) diff --git a/es6/sql-bridge/src/test/scala/app/softnetwork/elastic/sql/SQLQuerySpec.scala b/es6/sql-bridge/src/test/scala/app/softnetwork/elastic/sql/SQLQuerySpec.scala index 8a5cef5e..d1a4a4ee 100644 --- a/es6/sql-bridge/src/test/scala/app/softnetwork/elastic/sql/SQLQuerySpec.scala +++ b/es6/sql-bridge/src/test/scala/app/softnetwork/elastic/sql/SQLQuerySpec.scala @@ -28,7 +28,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { "SQLQuery" should "perform native count" in { val results: Seq[ElasticAggregation] = - SQLQuery("select count(t.id) c2 from Table t where t.nom = \"Nom\"") + SQLQuery("select count(t.id) c2 from Table t where t.nom = 'Nom'") results.size shouldBe 1 val result = results.head result.nested shouldBe false @@ -64,7 +64,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { it should "perform count distinct" in { val results: Seq[ElasticAggregation] = - SQLQuery("select count(distinct t.id) as c2 from Table as t where nom = \"Nom\"") + SQLQuery("select count(distinct t.id) as c2 from Table as t where nom = 'Nom'") results.size shouldBe 1 val result = results.head result.nested shouldBe false @@ -101,7 +101,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { it should "perform nested count" in { val results: Seq[ElasticAggregation] = SQLQuery( - "select count(inner_emails.value) as email from index i join unnest(i.emails) as inner_emails where i.nom = \"Nom\"" + "select count(inner_emails.value) as email from index i join unnest(i.emails) as inner_emails where i.nom = 'Nom'" ) results.size shouldBe 1 val result = results.head @@ -719,29 +719,106 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "path": "products" | }, | "aggs": { - | "cat": { - | "terms": { - | "field": "products.category.keyword" + | "filtered_inner_products": { + | "filter": { + | "bool": { + | "filter": [ + | { + | "bool": { + | "must_not": [ + | { + | "term": { + | "products.category": { + | "value": "coffee" + | } + | } + | } + | ] + | } + | }, + | { + | "match_all": {} + | }, + | { + | "match_all": {} + | }, + | { + | "bool": { + | "should": [ + | { + | "match": { + | "products.name": { + | "query": "lasagnes" + | } + | } + | }, + | { + | "match": { + | "products.description": { + | "query": "lasagnes" + | } + | } + | }, + | { + | "match": { + | "products.ingredients": { + | "query": "lasagnes" + | } + | } + | } + | ] + | } + | }, + | { + | "range": { + | "products.stock": { + | "gt": 0 + | } + | } + | }, + | { + | "term": { + | "products.upForSale": { + | "value": true + | } + | } + | }, + | { + | "term": { + | "products.deleted": { + | "value": false + | } + | } + | } + | ] + | } | }, | "aggs": { - | "min_price": { - | "min": { - | "field": "products.price" - | } - | }, - | "max_price": { - | "max": { - | "field": "products.price" - | } - | }, - | "having_filter": { - | "bucket_selector": { - | "buckets_path": { - | "min_price": "inner_products>min_price", - | "max_price": "inner_products>max_price" + | "cat": { + | "terms": { + | "field": "products.category.keyword" + | }, + | "aggs": { + | "min_price": { + | "min": { + | "field": "products.price" + | } | }, - | "script": { - | "source": "params.min_price > 5.0 && params.max_price < 50.0" + | "max_price": { + | "max": { + | "field": "products.price" + | } + | }, + | "having_filter": { + | "bucket_selector": { + | "buckets_path": { + | "min_price": "inner_products>min_price", + | "max_price": "inner_products>max_price" + | }, + | "script": { + | "source": "params.min_price > 5.0 && params.max_price < 50.0" + | } + | } | } | } | } @@ -773,7 +850,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "ct": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.minus(35, ChronoUnit.MINUTES) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.minus(35, ChronoUnit.MINUTES)); param1" | } | } | }, @@ -784,7 +861,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("if\\(", "if (") .replaceAll("!=null", " != null") @@ -965,7 +1042,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { it should "handle group by with having and date time functions" in { val select: ElasticSearchRequest = - SQLQuery(groupByWithHavingAndDateTimeFunctions.replace("GROUP BY 3, 2", "GROUP BY 3, 2")) + SQLQuery(groupByWithHavingAndDateTimeFunctions) val query = select.query println(query) query shouldBe @@ -1026,7 +1103,72 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll(">", " > ") } - it should "handle parse_date function" in { + it should "handle group by index" in { + val select: ElasticSearchRequest = + SQLQuery( + groupByWithHavingAndDateTimeFunctions.replace("GROUP BY Country, City", "GROUP BY 3, 2") + ) + val query = select.query + println(query) + query shouldBe + """{ + | "query": { + | "match_all": {} + | }, + | "size": 0, + | "_source": true, + | "aggs": { + | "Country": { + | "terms": { + | "field": "Country.keyword", + | "exclude": "USA", + | "order": { + | "_key": "asc" + | } + | }, + | "aggs": { + | "City": { + | "terms": { + | "field": "City.keyword", + | "exclude": "Berlin" + | }, + | "aggs": { + | "cnt": { + | "value_count": { + | "field": "CustomerID" + | } + | }, + | "lastSeen": { + | "max": { + | "field": "createdAt" + | } + | }, + | "having_filter": { + | "bucket_selector": { + | "buckets_path": { + | "cnt": "cnt", + | "lastSeen": "lastSeen" + | }, + | "script": { + | "source": "params.cnt > 1 && params.lastSeen > ZonedDateTime.now(ZoneId.of('Z')).minus(7, ChronoUnit.DAYS).toInstant().toEpochMilli()" + | } + | } + | } + | } + | } + | } + | } + | } + |}""".stripMargin + .replaceAll("\\s", "") + .replaceAll("ChronoUnit", " ChronoUnit") + .replaceAll("==", " == ") + .replaceAll("!=", " != ") + .replaceAll("&&", " && ") + .replaceAll(">", " > ") + } + + it should "handle date_parse function" in { val select: ElasticSearchRequest = SQLQuery(dateParse) val query = select.query @@ -1065,7 +1207,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "field": "createdAt", | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? DateTimeFormatter.ofPattern('yyyy-MM-dd').parse(e0, LocalDate::from) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); (param1 == null) ? null : LocalDate.parse(param1, DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"))" | } | } | } @@ -1074,7 +1216,102 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") + .replaceAll("defe", "def e") + .replaceAll("if\\(", "if (") + .replaceAll("=\\(", " = (") + .replaceAll("\\?", " ? ") + .replaceAll(":null", " : null") + .replaceAll("null:", "null : ") + .replaceAll("return", " return ") + .replaceAll(";", "; ") + .replaceAll(",ChronoUnit", ", ChronoUnit") + .replaceAll("=DateTimeFormatter", " = DateTimeFormatter") + .replaceAll(",DateTimeFormatter", ", DateTimeFormatter") + .replaceAll("==", " == ") + .replaceAll("!=", " != ") + .replaceAll("&&", " && ") + .replaceAll("\\|\\|", " || ") + .replaceAll(">", " > ") + .replaceAll(",LocalDate", ", LocalDate") + } + + it should "handle date_format function" in { + val select: ElasticSearchRequest = + SQLQuery(dateFormat) + val query = select.query + println(query) + query shouldBe + """{ + | "query": { + | "bool": { + | "filter": [ + | { + | "exists": { + | "field": "identifier2" + | } + | } + | ] + | } + | }, + | "script_fields": { + | "y": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.withDayOfYear(1).truncatedTo(ChronoUnit.DAYS)); def param2 = DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"); (param1 == null) ? null : param2.format(param1)" + | } + | }, + | "q": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); def param2 = param1 != null ? param1.withMonth((((param1.getMonthValue() - 1) / 3) * 3) + 1).withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS) : null; def param3 = DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"); (param1 == null) ? null : param3.format(param2)" + | } + | }, + | "m": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS)); def param2 = DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"); (param1 == null) ? null : param2.format(param1)" + | } + | }, + | "w": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.with(DayOfWeek.SUNDAY).truncatedTo(ChronoUnit.DAYS)); def param2 = DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"); (param1 == null) ? null : param2.format(param1)" + | } + | }, + | "d": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.truncatedTo(ChronoUnit.DAYS)); def param2 = DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"); (param1 == null) ? null : param2.format(param1)" + | } + | }, + | "h": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.truncatedTo(ChronoUnit.HOURS)); def param2 = DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"); (param1 == null) ? null : param2.format(param1)" + | } + | }, + | "m2": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.truncatedTo(ChronoUnit.MINUTES)); def param2 = DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"); (param1 == null) ? null : param2.format(param1)" + | } + | }, + | "lastSeen": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.truncatedTo(ChronoUnit.SECONDS)); def param2 = DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"); (param1 == null) ? null : param2.format(param1)" + | } + | } + | }, + | "_source": { + | "includes": [ + | "identifier" + | ] + | } + |}""".stripMargin + .replaceAll("\\s", "") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("if\\(", "if (") .replaceAll("=\\(", " = (") @@ -1084,7 +1321,13 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("return", " return ") .replaceAll(";", "; ") .replaceAll(",ChronoUnit", ", ChronoUnit") + .replaceAll("=DateTimeFormatter", " = DateTimeFormatter") .replaceAll("==", " == ") + .replaceAll("-(\\d)", " - $1") + .replaceAll("\\+", " + ") + .replaceAll("/", " / ") + .replaceAll("\\*", " * ") + .replaceAll("=p", " = p") .replaceAll("!=", " != ") .replaceAll("&&", " && ") .replaceAll("\\|\\|", " || ") @@ -1092,7 +1335,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll(",LocalDate", ", LocalDate") } - it should "handle parse_datetime function" in { + it should "handle datetime_parse function" in { // #25 val select: ElasticSearchRequest = SQLQuery(dateTimeParse) val query = select.query @@ -1131,7 +1374,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "field": "createdAt", | "script": { | "lang": "painless", - | "source": "(def e2 = (def e1 = (def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? DateTimeFormatter.ofPattern('yyyy-MM-dd HH:mm:ss.SSS XXX').parse(e0, ZonedDateTime::from) : null); e1 != null ? e1.truncatedTo(ChronoUnit.MINUTES) : null); e2 != null ? e2.get(ChronoField.YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); (param1 == null) ? null : ZonedDateTime.parse(param1, DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss.SSS XXX\")).truncatedTo(ChronoUnit.MINUTES).get(ChronoField.YEAR)" | } | } | } @@ -1140,7 +1383,61 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") + .replaceAll("defe", "def e") + .replaceAll("if\\(", "if (") + .replaceAll("=\\(", " = (") + .replaceAll("\\?", " ? ") + .replaceAll(":null", " : null") + .replaceAll("null:", "null : ") + .replaceAll("return", " return ") + .replaceAll(";", "; ") + .replaceAll("==", " == ") + .replaceAll("!=", " != ") + .replaceAll("&&", " && ") + .replaceAll("\\|\\|", " || ") + .replaceAll(">", " > ") + .replaceAll(",ZonedDateTime", ", ZonedDateTime") + .replaceAll("=DateTimeFormatter", " = DateTimeFormatter") + .replaceAll(",DateTimeFormatter", ", DateTimeFormatter") + .replaceAll("SSSXXX", "SSS XXX") + .replaceAll("ddHH", "dd HH") + } + + it should "handle datetime_format function" in { + val select: ElasticSearchRequest = + SQLQuery(dateTimeFormat) + val query = select.query + println(query) + query shouldBe + """{ + | "query": { + | "bool": { + | "filter": [ + | { + | "exists": { + | "field": "identifier2" + | } + | } + | ] + | } + | }, + | "script_fields": { + | "lastSeen": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS)); def param2 = DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss XXX\"); (param1 == null) ? null : param2.format(param1)" + | } + | } + | }, + | "_source": { + | "includes": [ + | "identifier" + | ] + | } + |}""".stripMargin + .replaceAll("\\s", "") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("if\\(", "if (") .replaceAll("=\\(", " = (") @@ -1155,8 +1452,10 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("\\|\\|", " || ") .replaceAll(">", " > ") .replaceAll(",ZonedDateTime", ", ZonedDateTime") + .replaceAll("=DateTimeFormatter", " = DateTimeFormatter") .replaceAll("SSSXXX", "SSS XXX") .replaceAll("ddHH", "dd HH") + .replaceAll("XXX", " XXX") } it should "handle date_diff function as script field" in { @@ -1173,7 +1472,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "diff": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('updatedAt') || doc['updatedAt'].empty ? null : doc['updatedAt'].value); def arg1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); (arg0 == null || arg1 == null) ? null : ChronoUnit.DAYS.between(arg0, arg1))" + | "source": "def param1 = (!doc.containsKey('updatedAt') || doc['updatedAt'].empty ? null : doc['updatedAt'].value); def param2 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); (param1 == null || param2 == null) ? null : ChronoUnit.DAYS.between(param1, param2)" | } | } | }, @@ -1184,7 +1483,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("defa", "def a") .replaceAll("if\\(", "if (") @@ -1193,7 +1492,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll(":null", " : null") .replaceAll("null:", "null : ") .replaceAll("return", " return ") - .replaceAll(",a", ", a") + .replaceAll(",p", ", p") .replaceAll(";", "; ") .replaceAll("==", " == ") .replaceAll("!=", " != ") @@ -1223,7 +1522,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "max": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('updatedAt') || doc['updatedAt'].empty ? null : doc['updatedAt'].value); def arg1 = (def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? DateTimeFormatter.ofPattern('yyyy-MM-dd HH:mm:ss.SSS XXX').parse(e0, ZonedDateTime::from) : null); (arg0 == null || arg1 == null) ? null : ChronoUnit.DAYS.between(arg0, arg1))" + | "source": "def param1 = (!doc.containsKey('updatedAt') || doc['updatedAt'].empty ? null : doc['updatedAt'].value); def param2 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); def param3 = (param2 == null) ? null : ZonedDateTime.parse(param2, DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss.SSS XXX\")); (param1 == null || param2 == null) ? null : ChronoUnit.DAYS.between(param1, param3)" | } | } | } @@ -1232,7 +1531,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("defa", "def a") .replaceAll("if\\(", "if (") @@ -1241,13 +1540,14 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll(":null", " : null") .replaceAll("null:", "null : ") .replaceAll("return", " return ") - .replaceAll(",a", ", a") + .replaceAll(",p", ", p") .replaceAll(";", "; ") .replaceAll("==", " == ") .replaceAll("!=", " != ") .replaceAll("&&", " && ") .replaceAll("\\|\\|", " || ") - .replaceAll("ZonedDateTime", " ZonedDateTime") + .replaceAll("=DateTimeFormatter", " = DateTimeFormatter") + .replaceAll(",DateTimeFormatter", ", DateTimeFormatter") .replaceAll("SSSXXX", "SSS XXX") .replaceAll("ddHH", "dd HH") } @@ -1274,7 +1574,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "lastSeen": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); e0 != null ? e0.plus(10, ChronoUnit.DAYS) : null)" + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.plus(10, ChronoUnit.DAYS)); param1" | } | } | }, @@ -1285,7 +1585,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("defs", "def s") .replaceAll("if\\(", "if (") @@ -1303,7 +1603,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("ChronoUnit", " ChronoUnit") } - it should "handle date_sub function as script field" in { + it should "handle date_sub function as script field" in { // 30 val select: ElasticSearchRequest = SQLQuery(dateSub) val query = select.query @@ -1325,7 +1625,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "lastSeen": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); e0 != null ? e0.minus(10, ChronoUnit.DAYS) : null)" + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.minus(10, ChronoUnit.DAYS)); param1" | } | } | }, @@ -1336,7 +1636,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("defs", "def s") .replaceAll("if\\(", "if (") @@ -1376,7 +1676,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "lastSeen": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); e0 != null ? e0.plus(10, ChronoUnit.DAYS) : null)" + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.plus(10, ChronoUnit.DAYS)); param1" | } | } | }, @@ -1387,7 +1687,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("defs", "def s") .replaceAll("if\\(", "if (") @@ -1427,7 +1727,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "lastSeen": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); e0 != null ? e0.minus(10, ChronoUnit.DAYS) : null)" + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.minus(10, ChronoUnit.DAYS)); param1" | } | } | }, @@ -1438,7 +1738,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("defs", "def s") .replaceAll("if\\(", "if (") @@ -1470,20 +1770,20 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "flag": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); e0 == null)" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); param1 == null" | } | } | }, | "_source": true |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("defs", "def s") .replaceAll("if\\(", "if (") .replaceAll("=\\(", " = (") .replaceAll("\\?", " ? ") - .replaceAll(":null", " : null") + .replaceAll(":false", " : false") .replaceAll("null:", "null : ") .replaceAll("return", " return ") .replaceAll("between\\(s,", "between(s, ") @@ -1508,7 +1808,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "flag": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null)" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); param1 != null" | } | } | }, @@ -1519,13 +1819,13 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("defs", "def s") .replaceAll("if\\(", "if (") .replaceAll("=\\(", " = (") .replaceAll("\\?", " ? ") - .replaceAll(":null", " : null") + .replaceAll(":true", " : true") .replaceAll("null:", "null : ") .replaceAll("return", " return ") .replaceAll("between\\(s,", "between(s, ") @@ -1608,7 +1908,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "c": { | "script": { | "lang": "painless", - | "source": "{ def v0 = (def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.minus(35, ChronoUnit.MINUTES) : null);if (v0 != null) return v0; return ZonedDateTime.now(ZoneId.of('Z')).toLocalDate().atStartOfDay(ZoneId.of('Z')); }" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.minus(35, ChronoUnit.MINUTES)); def param2 = ZonedDateTime.now(ZoneId.of('Z')).toLocalDate(); param1 != null ? param1 : param2" | } | } | }, @@ -1619,6 +1919,8 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") + .replaceAll(";defp", "; defp") + .replaceAll("defp", "def p") .replaceAll("defv", " def v") .replaceAll("defe", "def e") .replaceAll("if\\(", "if (") @@ -1627,6 +1929,9 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll(":null", " : null") .replaceAll(";}", "; }") .replaceAll(";e", "; e") + .replaceAll(";v", "; v") + .replaceAll(":p", " : p") + .replaceAll(";p", "; p") .replaceAll(":null", " : null") .replaceAll("null:", "null : ") .replaceAll("return", " return ") @@ -1635,6 +1940,8 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("&&", " && ") .replaceAll("\\|\\|", " || ") .replaceAll("ChronoUnit", " ChronoUnit") + .replaceAll("=ZonedDateTime", " = ZonedDateTime") + .replaceAll(":ZonedDateTime", " : ZonedDateTime") } it should "handle nullif function as script field" in { @@ -1651,7 +1958,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "c": { | "script": { | "lang": "painless", - | "source": "{ def v0 = ((def arg0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); (arg0 == null) ? null : arg0 == DateTimeFormatter.ofPattern('yyyy-MM-dd').parse(\"2025-09-11\", LocalDate::from).minus(2, ChronoUnit.DAYS) ? null : arg0));if (v0 != null) return v0; return ZonedDateTime.now(ZoneId.of('Z')).toLocalDate(); }" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.toLocalDate()); def param2 = LocalDate.parse(\"2025-09-11\", DateTimeFormatter.ofPattern(\"yyyy-MM-dd\")).minus(2, ChronoUnit.DAYS); def param3 = param1 == null || param1.isEqual(param2) ? null : param1; def param4 = ZonedDateTime.now(ZoneId.of('Z')).toLocalDate(); param3 != null ? param3 : param4" | } | } | }, @@ -1662,6 +1969,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") + .replaceAll("defp", "def p") .replaceAll("defv", " def v") .replaceAll("defa", "def a") .replaceAll("if\\(", "if (") @@ -1672,6 +1980,10 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("between\\(s,", "between(s, ") .replaceAll(";def", "; def") .replaceAll(";return", "; return") + .replaceAll(";v", "; v") + .replaceAll("(\\d)=p", "$1 = p") + .replaceAll(";p", "; p") + .replaceAll(":p", " : p") .replaceAll("returnv", " return v") .replaceAll("returne", " return e") .replaceAll(";}", "; }") @@ -1683,8 +1995,11 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll(";\\s\\s", "; ") .replaceAll("ChronoUnit", " ChronoUnit") .replaceAll(",LocalDate", ", LocalDate") + .replaceAll("=LocalDate", " = LocalDate") .replaceAll("=DateTimeFormatter", " = DateTimeFormatter") - .replaceAll("ZonedDateTime", " ZonedDateTime") + .replaceAll(",DateTimeFormatter", ", DateTimeFormatter") + .replaceAll("=ZonedDateTime", " = ZonedDateTime") + .replaceAll(":ZonedDateTime", " : ZonedDateTime") } it should "handle cast function as script field" in { @@ -1701,19 +2016,19 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "c": { | "script": { | "lang": "painless", - | "source": "try { def v0 = ((def arg0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); (arg0 == null) ? null : arg0 == DateTimeFormatter.ofPattern('yyyy-MM-dd').parse(\"2025-09-11\", LocalDate::from) ? null : arg0));if (v0 != null) return v0; return ZonedDateTime.now(ZoneId.of('Z')).toLocalDate().atStartOfDay(ZoneId.of('Z')).minus(2, ChronoUnit.HOURS).toInstant().toEpochMilli(); } catch (Exception e) { return null; }" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.toLocalDate()); def param2 = LocalDate.parse(\"2025-09-11\", DateTimeFormatter.ofPattern(\"yyyy-MM-dd\")); def param3 = param1 == null || param1.isEqual(param2) ? null : param1; def param4 = ZonedDateTime.now(ZoneId.of('Z')).toLocalDate().minus(2, ChronoUnit.HOURS); try { param3 != null ? param3 : param4 } catch (Exception e) { return null; }" | } | }, | "c2": { | "script": { | "lang": "painless", - | "source": "ZonedDateTime.now(ZoneId.of('Z')).toInstant().toEpochMilli()" + | "source": "def param1 = ZonedDateTime.now(ZoneId.of('Z')); param1.toInstant().toEpochMilli()" | } | }, | "c3": { | "script": { | "lang": "painless", - | "source": "ZonedDateTime.now(ZoneId.of('Z')).toLocalDate()" + | "source": "def param1 = ZonedDateTime.now(ZoneId.of('Z')); param1.toLocalDate()" | } | }, | "c4": { @@ -1725,7 +2040,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "c5": { | "script": { | "lang": "painless", - | "source": "LocalDate.parse(\"2025-09-11\", DateTimeFormatter.ofPattern('yyyy-MM-dd'))" + | "source": "def param1 = LocalDate.parse(\"2025-09-11\", DateTimeFormatter.ofPattern(\"yyyy-MM-dd\")); param1" | } | } | }, @@ -1736,6 +2051,8 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") + .replaceAll(";defp", "; defp") + .replaceAll("defp", "def p") .replaceAll("defv", " def v") .replaceAll("defa", "def a") .replaceAll("if\\(", "if (") @@ -1759,9 +2076,17 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("\\}catch", "} catch ") .replaceAll("Exceptione\\)", "Exception e) ") .replaceAll(",DateTimeFormatter", ", DateTimeFormatter") + .replaceAll("(\\d)=p", "$1 = p") + .replaceAll(";p", "; p") + .replaceAll(":p", " : p") + .replaceAll("=ZonedDateTime", " = ZonedDateTime") + .replaceAll("=LocalDate", " = LocalDate") + .replaceAll(":ZonedDateTime", " : ZonedDateTime") + .replaceAll("try \\{", "try { ") + .replaceAll("} catch", " } catch") } - it should "handle case function as script field" in { + it should "handle case function as script field" in { // 40 val select: ElasticSearchRequest = SQLQuery(caseWhen) val query = select.query @@ -1775,7 +2100,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "c": { | "script": { | "lang": "painless", - | "source": "{ if (def left = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); left == null ? false : left > ZonedDateTime.now(ZoneId.of('Z')).minus(7, ChronoUnit.DAYS)) return left; if (def left = (!doc.containsKey('lastSeen') || doc['lastSeen'].empty ? null : doc['lastSeen'].value); left != null) return left.plus(2, ChronoUnit.DAYS); def dval = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); return dval; }" + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); def param2 = ZonedDateTime.now(ZoneId.of('Z')).minus(7, ChronoUnit.DAYS); def param3 = param1 == null ? false : (param1.isAfter(param2)); def param4 = (!doc.containsKey('lastSeen') || doc['lastSeen'].empty ? null : doc['lastSeen'].value.plus(2, ChronoUnit.DAYS)); def param5 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); param3 ? param1 : param4 != null ? param4 : param5" | } | } | }, @@ -1786,7 +2111,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defd", " def d") .replaceAll("defe", " def e") .replaceAll("defl", " def l") @@ -1809,6 +2134,9 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll(">", " > ") .replaceAll("if \\(\\s*def", "if (def") .replaceAll("ChronoUnit", " ChronoUnit") + .replaceAll("=ZonedDateTime", " = ZonedDateTime") + .replaceAll("=p", " = p") + .replaceAll(":p", " : p") } it should "handle case with expression function as script field" in { @@ -1825,7 +2153,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "c": { | "script": { | "lang": "painless", - | "source": "{ def expr = ZonedDateTime.now(ZoneId.of('Z')).toLocalDate().minus(7, ChronoUnit.DAYS); def e0 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); def val0 = e0 != null ? e0.minus(3, ChronoUnit.DAYS).atStartOfDay(ZoneId.of('Z')) : null; if (expr == val0) return e0; def val1 = (!doc.containsKey('lastSeen') || doc['lastSeen'].empty ? null : doc['lastSeen'].value); if (expr == val1) return val1.plus(2, ChronoUnit.DAYS); def dval = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); return dval; }" + | "source": "def param1 = ZonedDateTime.now(ZoneId.of('Z')).toLocalDate().minus(7, ChronoUnit.DAYS); def param2 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.toLocalDate().minus(3, ChronoUnit.DAYS)); def param3 = (!doc.containsKey('lastSeen') || doc['lastSeen'].empty ? null : doc['lastSeen'].value.toLocalDate().plus(2, ChronoUnit.DAYS)); def param4 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.toLocalDate()); param1 != null && param1.isEqual(param2) ? param2 : param1 != null && param1.isEqual(param3) ? param3 : param4" | } | } | }, @@ -1836,7 +2164,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defd", " def d") .replaceAll("defe", " def e") .replaceAll("defl", " def l") @@ -1861,6 +2189,9 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("ChronoUnit", " ChronoUnit") .replaceAll("=ZonedDateTime", " = ZonedDateTime") .replaceAll("=e", " = e") + .replaceAll("=ZonedDateTime", " = ZonedDateTime") + .replaceAll("=p", " = p") + .replaceAll(":p", " : p") } it should "handle extract function as script field" in { @@ -1877,98 +2208,98 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "dom": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.DAY_OF_MONTH) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.DAY_OF_MONTH)); param1" | } | }, | "dow": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.DAY_OF_WEEK) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.DAY_OF_WEEK)); param1" | } | }, | "doy": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.DAY_OF_YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.DAY_OF_YEAR)); param1" | } | }, | "m": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.MONTH_OF_YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.MONTH_OF_YEAR)); param1" | } | }, | "y": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.YEAR)); param1" | } | }, | "h": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.HOUR_OF_DAY) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.HOUR_OF_DAY)); param1" | } | }, | "minutes": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.MINUTE_OF_HOUR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.MINUTE_OF_HOUR)); param1" | } | }, | "s": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.SECOND_OF_MINUTE) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.SECOND_OF_MINUTE)); param1" | } | }, | "nano": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.NANO_OF_SECOND) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.NANO_OF_SECOND)); param1" | } | }, | "micro": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.MICRO_OF_SECOND) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.MICRO_OF_SECOND)); param1" | } | }, | "milli": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.MILLI_OF_SECOND) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.MILLI_OF_SECOND)); param1" | } | }, | "epoch": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.EPOCH_DAY) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.EPOCH_DAY)); param1" | } | }, | "off": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.OFFSET_SECONDS) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.OFFSET_SECONDS)); param1" | } | }, | "w": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(java.time.temporal.IsoFields.WEEK_OF_WEEK_BASED_YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(java.time.temporal.IsoFields.WEEK_OF_WEEK_BASED_YEAR)); param1" | } | }, | "q": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(java.time.temporal.IsoFields.QUARTER_OF_YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(java.time.temporal.IsoFields.QUARTER_OF_YEAR)); param1" | } | } | }, | "_source": true |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defe", "def e") + .replaceAll("defp", "def p") .replaceAll("if\\(", "if (") .replaceAll("=\\(", " = (") .replaceAll("\\?", " ? ") @@ -2001,7 +2332,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "script": { | "script": { | "lang": "painless", - | "source": "def lv0 = ((!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value)); ( lv0 == null ) ? null : (lv0 * (ZonedDateTime.now(ZoneId.of('Z')).toLocalDate().get(ChronoField.YEAR) - 10)) > 10000" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); def param2 = ZonedDateTime.now(ZoneId.of('Z')).toLocalDate().get(ChronoField.YEAR); (param1 == null) ? null : (param1 * (param2 - 10)) > 10000" | } | } | } @@ -2012,37 +2343,37 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "add": { | "script": { | "lang": "painless", - | "source": "def lv0 = ((!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value)); ( lv0 == null ) ? null : (lv0 + 1)" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : (param1 + 1)" | } | }, | "sub": { | "script": { | "lang": "painless", - | "source": "def lv0 = ((!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value)); ( lv0 == null ) ? null : (lv0 - 1)" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : (param1 - 1)" | } | }, | "mul": { | "script": { | "lang": "painless", - | "source": "def lv0 = ((!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value)); ( lv0 == null ) ? null : (lv0 * 2)" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : (param1 * 2)" | } | }, | "div": { | "script": { | "lang": "painless", - | "source": "def lv0 = ((!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value)); ( lv0 == null ) ? null : (lv0 / 2)" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : (param1 / 2)" | } | }, | "mod": { | "script": { | "lang": "painless", - | "source": "def lv0 = ((!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value)); ( lv0 == null ) ? null : (lv0 % 2)" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : (param1 % 2)" | } | }, | "identifier_mul_identifier2_minus_10": { | "script": { | "lang": "painless", - | "source": "def lv0 = ((def lv1 = ((!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value)); def rv1 = ((!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value)); ( lv1 == null || rv1 == null ) ? null : (lv1 * rv1))); ( lv0 == null ) ? null : (lv0 - 10)" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); def param2 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); def lv0 = ((param1 == null || param2 == null) ? null : (param1 * param2)); (lv0 == null) ? null : (lv0 - 10)" | } | } | }, @@ -2053,12 +2384,14 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("defl", "def l") .replaceAll("defr", "def r") .replaceAll("if\\(", "if (") .replaceAll("=\\(", " = (") + // .replaceAll("(\\d)=", "$1 =") + .replaceAll("=ZonedDateTime", " = ZonedDateTime") .replaceAll("\\?", " ? ") .replaceAll(":null", " : null") .replaceAll("null:", "null : ") @@ -2088,7 +2421,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "script": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.sqrt(arg0)) > 100.0" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.sqrt(param1) > 100.0" | } | } | } @@ -2099,109 +2432,109 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "abs_identifier_plus_1_0_mul_2": { | "script": { | "lang": "painless", - | "source": "((def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.abs(arg0)) + 1.0) * ((double) 2)" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); ((param1 == null) ? null : Math.abs(param1) + 1.0) * ((double) 2)" | } | }, | "ceil_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.ceil(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.ceil(param1)" | } | }, | "floor_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.floor(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.floor(param1)" | } | }, | "sqrt_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.sqrt(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.sqrt(param1)" | } | }, | "exp_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.exp(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.exp(param1)" | } | }, | "log_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.log(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.log(param1)" | } | }, | "log10_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.log10(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.log10(param1)" | } | }, | "pow_identifier_3": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.pow(arg0, 3))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.pow(param1, 3)" | } | }, | "round_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : (def p = Math.pow(10, 0); Math.round((arg0 * p) / p)))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); def param2 = Math.pow(10, 0); (param1 == null || param2 == null) ? null : Math.round((param1 * param2) / param2)" | } | }, | "round_identifier_2": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : (def p = Math.pow(10, 2); Math.round((arg0 * p) / p)))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); def param2 = Math.pow(10, 2); (param1 == null || param2 == null) ? null : Math.round((param1 * param2) / param2)" | } | }, | "sign_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); arg0 != null ? (arg0 > 0 ? 1 : (arg0 < 0 ? -1 : 0)) : null)" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : (param1 > 0 ? 1 : (param1 < 0 ? -1 : 0))" | } | }, | "cos_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.cos(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.cos(param1)" | } | }, | "acos_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.acos(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.acos(param1)" | } | }, | "sin_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.sin(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.sin(param1)" | } | }, | "asin_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.asin(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.asin(param1)" | } | }, | "tan_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.tan(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.tan(param1)" | } | }, | "atan_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.atan(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.atan(param1)" | } | }, | "atan2_identifier_3_0": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.atan2(arg0, 3.0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.atan2(param1, 3.0)" | } | } | }, @@ -2212,7 +2545,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defa", "def a") .replaceAll("defp", "def p") .replaceAll("if\\(", "if (") @@ -2244,7 +2577,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("\\(double\\)(\\d)", "(double) $1") } - it should "handle string function as script field and condition" in { + it should "handle string function as script field and condition" in { // 45 val select: ElasticSearchRequest = SQLQuery(string) val query = select.query @@ -2258,7 +2591,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "script": { | "script": { | "lang": "painless", - | "source": "def left = (def e1 = (def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.trim() : null); e1 != null ? e1.length() : null); left == null ? false : left > 10" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.trim().length() > 10" | } | } | } @@ -2269,85 +2602,85 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "len": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.length() : null)" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.length()" | } | }, | "low": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.lower() : null)" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.toLowerCase()" | } | }, | "upp": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.upper() : null)" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.toUpperCase()" | } | }, | "sub": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : arg0.substring(1 - 1, Math.min(1 - 1 + 3, arg0.length())))" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.substring(0, Math.min(3, param1.length()))" | } | }, | "tr": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.trim() : null)" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.trim()" | } | }, | "ltr": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.replaceAll(\"^\\\\s+\",\"\") : null)" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.replaceAll(\"^\\\\s+\",\"\")" | } | }, | "rtr": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.replaceAll(\"\\\\s+$\",\"\") : null)" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.replaceAll(\"\\\\s+$\",\"\")" | } | }, | "con": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : String.valueOf(arg0) + \"_test\" + String.valueOf(1))" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : String.valueOf(param1) + \"_test\" + String.valueOf(1)" | } | }, | "l": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : arg0.substring(0, Math.min(5, arg0.length())))" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.substring(0, Math.min(5, param1.length()))" | } | }, | "r": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : 3 == 0 ? \"\" : arg0.substring(arg0.length() - Math.min(3, arg0.length())))" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.substring(param1.length() - Math.min(3, param1.length()))" | } | }, | "rep": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : arg0.replace(\"el\", \"le\"))" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.replace(\"el\", \"le\")" | } | }, | "rev": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : new StringBuilder(arg0).reverse().toString())" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : new StringBuilder(param1).reverse().toString()" | } | }, | "pos": { | "script": { | "lang": "painless", - | "source": "(def arg1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg1 == null) ? null : arg1.indexOf(\"soft\", 1 - 1) + 1)" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.indexOf(\"soft\", 0) + 1" | } | }, | "reg": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : java.util.regex.Pattern.compile(\"soft\", java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.MULTILINE).matcher(arg0).find())" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : java.util.regex.Pattern.compile(\"soft\", java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.MULTILINE).matcher(param1).find()" | } | } | }, @@ -2358,7 +2691,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defa", "def a") .replaceAll("defe", "def e") .replaceAll("defl", "def l") @@ -2415,7 +2748,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "hire_date": { | "script": { | "lang": "painless", - | "source": "(!doc.containsKey('hire_date') || doc['hire_date'].empty ? null : doc['hire_date'].value)" + | "source": "def param1 = (!doc.containsKey('hire_date') || doc['hire_date'].empty ? null : doc['hire_date'].value.toLocalDate()); param1" | } | } | }, @@ -2494,7 +2827,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defa", "def a") .replaceAll("defe", "def e") .replaceAll("defl", "def l") @@ -2531,36 +2864,36 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { println(query) query shouldBe """{ - | "query": { - | "bool": { - | "filter": [ - | { - | "script": { - | "script": { - | "lang": "painless", - | "source": "(def e1 = ZonedDateTime.now(ZoneId.of('Z')).toLocalDate(); e1.withDayOfMonth(e1.lengthOfMonth())).get(ChronoField.DAY_OF_MONTH) > 28" - | } - | } - | } - | ] - | } - | }, - | "script_fields": { - | "ld": { - | "script": { - | "lang": "painless", - | "source": "(def e1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e1 != null ? e1.withDayOfMonth(e1.lengthOfMonth()) : null)" - | } - | } - | }, - | "_source": { - | "includes": [ - | "identifier" - | ] - | } - |}""".stripMargin + | "query": { + | "bool": { + | "filter": [ + | { + | "script": { + | "script": { + | "lang": "painless", + | "source": "def param1 = ZonedDateTime.now(ZoneId.of('Z')); param1.toLocalDate().withDayOfMonth(param1.toLocalDate().lengthOfMonth()).get(ChronoField.DAY_OF_MONTH) > 28" + | } + | } + | } + | ] + | } + | }, + | "script_fields": { + | "ld": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.toLocalDate()); (param1 == null) ? null : param1.withDayOfMonth(param1.lengthOfMonth())" + | } + | } + | }, + | "_source": { + | "includes": [ + | "identifier" + | ] + | } + |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defa", "def a") .replaceAll("defe", "def e") .replaceAll("defl", "def l") @@ -2605,98 +2938,98 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "y": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.YEAR)); param1" | } | }, | "m": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.MONTH_OF_YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.MONTH_OF_YEAR)); param1" | } | }, | "wd": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.DAY_OF_WEEK) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); (param1 == null) ? null : (param1.get(ChronoField.DAY_OF_WEEK) + 6) % 7" | } | }, | "yd": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.DAY_OF_YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.DAY_OF_YEAR)); param1" | } | }, | "d": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.DAY_OF_MONTH) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.DAY_OF_MONTH)); param1" | } | }, | "h": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.HOUR_OF_DAY) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.HOUR_OF_DAY)); param1" | } | }, | "minutes": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.MINUTE_OF_HOUR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.MINUTE_OF_HOUR)); param1" | } | }, | "s": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.SECOND_OF_MINUTE) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.SECOND_OF_MINUTE)); param1" | } | }, | "nano": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.NANO_OF_SECOND) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.NANO_OF_SECOND)); param1" | } | }, | "micro": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.MICRO_OF_SECOND) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.MICRO_OF_SECOND)); param1" | } | }, | "milli": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.MILLI_OF_SECOND) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.MILLI_OF_SECOND)); param1" | } | }, | "epoch": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.EPOCH_DAY) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.EPOCH_DAY)); param1" | } | }, | "off": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.OFFSET_SECONDS) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.OFFSET_SECONDS)); param1" | } | }, | "w": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(java.time.temporal.IsoFields.WEEK_OF_WEEK_BASED_YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(java.time.temporal.IsoFields.WEEK_OF_WEEK_BASED_YEAR)); param1" | } | }, | "q": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(java.time.temporal.IsoFields.QUARTER_OF_YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(java.time.temporal.IsoFields.QUARTER_OF_YEAR)); param1" | } | } | }, | "_source": true |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defa", "def a") .replaceAll("defe", "def e") .replaceAll("defl", "def l") @@ -2719,6 +3052,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("-", " - ") .replaceAll("\\*", " * ") .replaceAll("/", " / ") + .replaceAll("%", " % ") .replaceAll(">", " > ") .replaceAll("<", " < ") .replaceAll("!=", " != ") @@ -2849,7 +3183,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("lat,arg", "lat, arg") } - it should "handle between with temporal" in { + it should "handle between with temporal" in { // 50 val select: ElasticSearchRequest = SQLQuery(betweenTemporal) val query = select.query @@ -2874,7 +3208,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "script": { | "script": { | "lang": "painless", - | "source": "def left = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); left == null ? false : left >= (def e2 = LocalDate.parse(\"2025-09-11\", DateTimeFormatter.ofPattern('yyyy-MM-dd')); e2.withDayOfMonth(e2.lengthOfMonth()))" + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.toLocalDate()); def param2 = LocalDate.parse(\"2025-09-11\", DateTimeFormatter.ofPattern(\"yyyy-MM-dd\")); param1 == null ? false : (param1.isBefore(param2.withDayOfMonth(param2.lengthOfMonth())) == false)" | } | } | }, @@ -2899,7 +3233,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { |}""".stripMargin .replaceAll("\\s+", "") .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defa", "def a") .replaceAll("defe", "def e") .replaceAll("defl", "def l") @@ -2964,7 +3298,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "script": { | "script": { | "lang": "painless", - | "source": "def left = (!doc.containsKey('comments.replies.lastUpdated') || doc['comments.replies.lastUpdated'].empty ? null : doc['comments.replies.lastUpdated'].value); left == null ? false : left < (def e2 = LocalDate.parse(\"2025-09-10\", DateTimeFormatter.ofPattern('yyyy-MM-dd')); e2.withDayOfMonth(e2.lengthOfMonth()))" + | "source": "def param1 = (!doc.containsKey('comments.replies.lastUpdated') || doc['comments.replies.lastUpdated'].empty ? null : doc['comments.replies.lastUpdated'].value.toLocalDate()); def param2 = LocalDate.parse(\"2025-09-10\", DateTimeFormatter.ofPattern(\"yyyy-MM-dd\")); param1 == null ? false : (param1.isBefore(param2.withDayOfMonth(param2.lengthOfMonth())))" | } | } | } @@ -3007,7 +3341,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("\\s+", "") .replaceAll("\\s+", "") .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defa", "def a") .replaceAll("defe", "def e") .replaceAll("defl", "def l") @@ -3062,7 +3396,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "script": { | "script": { | "lang": "painless", - | "source": "def left = (!doc.containsKey('replies.lastUpdated') || doc['replies.lastUpdated'].empty ? null : doc['replies.lastUpdated'].value); left == null ? false : left < (def e2 = LocalDate.parse(\"2025-09-10\", DateTimeFormatter.ofPattern('yyyy-MM-dd')); e2.withDayOfMonth(e2.lengthOfMonth()))" + | "source": "def param1 = (!doc.containsKey('replies.lastUpdated') || doc['replies.lastUpdated'].empty ? null : doc['replies.lastUpdated'].value.toLocalDate()); def param2 = LocalDate.parse(\"2025-09-10\", DateTimeFormatter.ofPattern(\"yyyy-MM-dd\")); param1 == null ? false : (param1.isBefore(param2.withDayOfMonth(param2.lengthOfMonth())))" | } | } | }, @@ -3117,7 +3451,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("\\s+", "") .replaceAll("\\s+", "") .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defa", "def a") .replaceAll("defe", "def e") .replaceAll("defl", "def l") @@ -3169,7 +3503,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "script": { | "script": { | "lang": "painless", - | "source": "def left = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); left == null ? false : left < ZonedDateTime.now(ZoneId.of('Z')).toLocalDate()" + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.toLocalDate()); def param2 = ZonedDateTime.now(ZoneId.of('Z')).toLocalDate(); param1 == null ? false : (param1.isBefore(param2))" | } | } | } @@ -3221,7 +3555,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("\\s+", "") .replaceAll("\\s+", "") .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defa", "def a") .replaceAll("defe", "def e") .replaceAll("defl", "def l") diff --git a/es6/testkit/src/main/scala/app/softnetwork/elastic/scalatest/EmbeddedElasticTestKit.scala b/es6/testkit/src/main/scala/app/softnetwork/elastic/scalatest/EmbeddedElasticTestKit.scala index 3fe39859..8f84a3cc 100644 --- a/es6/testkit/src/main/scala/app/softnetwork/elastic/scalatest/EmbeddedElasticTestKit.scala +++ b/es6/testkit/src/main/scala/app/softnetwork/elastic/scalatest/EmbeddedElasticTestKit.scala @@ -1,3 +1,19 @@ +/* + * 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.scalatest import org.scalatest.Suite diff --git a/es7/rest/src/main/scala/app/softnetwork/elastic/client/rest/RestHighLevelClientApi.scala b/es7/rest/src/main/scala/app/softnetwork/elastic/client/rest/RestHighLevelClientApi.scala index 862d3d64..76b317dc 100644 --- a/es7/rest/src/main/scala/app/softnetwork/elastic/client/rest/RestHighLevelClientApi.scala +++ b/es7/rest/src/main/scala/app/softnetwork/elastic/client/rest/RestHighLevelClientApi.scala @@ -1,3 +1,19 @@ +/* + * 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.client.rest import akka.NotUsed diff --git a/es7/rest/src/main/scala/app/softnetwork/elastic/client/rest/RestHighLevelClientCompanion.scala b/es7/rest/src/main/scala/app/softnetwork/elastic/client/rest/RestHighLevelClientCompanion.scala index 1b490bb0..a2bff72e 100644 --- a/es7/rest/src/main/scala/app/softnetwork/elastic/client/rest/RestHighLevelClientCompanion.scala +++ b/es7/rest/src/main/scala/app/softnetwork/elastic/client/rest/RestHighLevelClientCompanion.scala @@ -1,3 +1,19 @@ +/* + * 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.client.rest import app.softnetwork.elastic.client.ElasticConfig diff --git a/es7/rest/src/main/scala/app/softnetwork/elastic/persistence/query/RestHighLevelClientProvider.scala b/es7/rest/src/main/scala/app/softnetwork/elastic/persistence/query/RestHighLevelClientProvider.scala index b76c13b4..7a93fcf9 100644 --- a/es7/rest/src/main/scala/app/softnetwork/elastic/persistence/query/RestHighLevelClientProvider.scala +++ b/es7/rest/src/main/scala/app/softnetwork/elastic/persistence/query/RestHighLevelClientProvider.scala @@ -1,3 +1,19 @@ +/* + * 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.persistence.query import app.softnetwork.elastic.client.rest.RestHighLevelClientApi diff --git a/es7/rest/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithRestProvider.scala b/es7/rest/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithRestProvider.scala index 3f79bb07..2e6eee32 100644 --- a/es7/rest/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithRestProvider.scala +++ b/es7/rest/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithRestProvider.scala @@ -1,3 +1,19 @@ +/* + * 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.persistence.query import app.softnetwork.persistence.message.CrudEvent diff --git a/es8/java/src/main/scala/app/softnetwork/elastic/client/java/ElasticsearchClientApi.scala b/es8/java/src/main/scala/app/softnetwork/elastic/client/java/ElasticsearchClientApi.scala index f2e01392..c13f2ee7 100644 --- a/es8/java/src/main/scala/app/softnetwork/elastic/client/java/ElasticsearchClientApi.scala +++ b/es8/java/src/main/scala/app/softnetwork/elastic/client/java/ElasticsearchClientApi.scala @@ -1,3 +1,19 @@ +/* + * 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.client.java import akka.NotUsed diff --git a/es8/java/src/main/scala/app/softnetwork/elastic/client/java/ElasticsearchClientCompanion.scala b/es8/java/src/main/scala/app/softnetwork/elastic/client/java/ElasticsearchClientCompanion.scala index ead36f44..0fcd0123 100644 --- a/es8/java/src/main/scala/app/softnetwork/elastic/client/java/ElasticsearchClientCompanion.scala +++ b/es8/java/src/main/scala/app/softnetwork/elastic/client/java/ElasticsearchClientCompanion.scala @@ -1,3 +1,19 @@ +/* + * 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.client.java import app.softnetwork.elastic.client.ElasticConfig diff --git a/es8/java/src/main/scala/app/softnetwork/elastic/persistence/query/ElasticsearchClientProvider.scala b/es8/java/src/main/scala/app/softnetwork/elastic/persistence/query/ElasticsearchClientProvider.scala index d83d9408..33b2bd05 100644 --- a/es8/java/src/main/scala/app/softnetwork/elastic/persistence/query/ElasticsearchClientProvider.scala +++ b/es8/java/src/main/scala/app/softnetwork/elastic/persistence/query/ElasticsearchClientProvider.scala @@ -1,3 +1,19 @@ +/* + * 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.persistence.query import app.softnetwork.elastic.client.java.ElasticsearchClientApi diff --git a/es8/java/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithJavaProvider.scala b/es8/java/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithJavaProvider.scala index eaf53713..2e2add10 100644 --- a/es8/java/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithJavaProvider.scala +++ b/es8/java/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithJavaProvider.scala @@ -1,3 +1,19 @@ +/* + * 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.persistence.query import app.softnetwork.persistence.message.CrudEvent diff --git a/es9/java/src/main/scala/app/softnetwork/elastic/client/java/ElasticsearchClientApi.scala b/es9/java/src/main/scala/app/softnetwork/elastic/client/java/ElasticsearchClientApi.scala index 5fbf9652..c0ad301c 100644 --- a/es9/java/src/main/scala/app/softnetwork/elastic/client/java/ElasticsearchClientApi.scala +++ b/es9/java/src/main/scala/app/softnetwork/elastic/client/java/ElasticsearchClientApi.scala @@ -1,3 +1,19 @@ +/* + * 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.client.java import akka.NotUsed diff --git a/es9/java/src/main/scala/app/softnetwork/elastic/client/java/ElasticsearchClientCompanion.scala b/es9/java/src/main/scala/app/softnetwork/elastic/client/java/ElasticsearchClientCompanion.scala index ead36f44..0fcd0123 100644 --- a/es9/java/src/main/scala/app/softnetwork/elastic/client/java/ElasticsearchClientCompanion.scala +++ b/es9/java/src/main/scala/app/softnetwork/elastic/client/java/ElasticsearchClientCompanion.scala @@ -1,3 +1,19 @@ +/* + * 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.client.java import app.softnetwork.elastic.client.ElasticConfig diff --git a/es9/java/src/main/scala/app/softnetwork/elastic/persistence/query/ElasticsearchClientProvider.scala b/es9/java/src/main/scala/app/softnetwork/elastic/persistence/query/ElasticsearchClientProvider.scala index d83d9408..33b2bd05 100644 --- a/es9/java/src/main/scala/app/softnetwork/elastic/persistence/query/ElasticsearchClientProvider.scala +++ b/es9/java/src/main/scala/app/softnetwork/elastic/persistence/query/ElasticsearchClientProvider.scala @@ -1,3 +1,19 @@ +/* + * 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.persistence.query import app.softnetwork.elastic.client.java.ElasticsearchClientApi diff --git a/es9/java/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithJavaProvider.scala b/es9/java/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithJavaProvider.scala index eaf53713..2e2add10 100644 --- a/es9/java/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithJavaProvider.scala +++ b/es9/java/src/main/scala/app/softnetwork/elastic/persistence/query/State2ElasticProcessorStreamWithJavaProvider.scala @@ -1,3 +1,19 @@ +/* + * 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.persistence.query import app.softnetwork.persistence.message.CrudEvent diff --git a/project/plugins.sbt b/project/plugins.sbt index ab6031fc..e19c083c 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -15,3 +15,5 @@ addDependencyTreePlugin addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.2") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.0") + +addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") diff --git a/sql/bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticAggregation.scala b/sql/bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticAggregation.scala index 1b8950c3..01160dc6 100644 --- a/sql/bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticAggregation.scala +++ b/sql/bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticAggregation.scala @@ -1,5 +1,6 @@ package app.softnetwork.elastic.sql.bridge +import app.softnetwork.elastic.sql.PainlessContext import app.softnetwork.elastic.sql.query.{ Asc, Bucket, @@ -104,8 +105,9 @@ object ElasticAggregation { buildScript: (String, Script) => Aggregation ): Aggregation = { if (transformFuncs.nonEmpty) { - val scriptSrc = identifier.painless() - val script = Script(scriptSrc).lang("painless") + val context = PainlessContext() + val scriptSrc = identifier.painless(Some(context)) + val script = Script(s"$context$scriptSrc").lang("painless") buildScript(aggName, script) } else { buildField(aggName, sourceField) @@ -142,7 +144,7 @@ object ElasticAggregation { Array.empty ).copy( scripts = th.fields.filter(_.isScriptField).map(f => - f.sourceField -> Script(f.painless()).lang("painless") + f.sourceField -> Script(f.painless(None)).lang("painless") ).toMap ) .size(limit) sortBy th.orderBy.sorts.map(sort => diff --git a/sql/bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticQuery.scala b/sql/bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticQuery.scala index 8cc8150a..300b7376 100644 --- a/sql/bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticQuery.scala +++ b/sql/bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/ElasticQuery.scala @@ -6,7 +6,7 @@ import app.softnetwork.elastic.sql.query.{ ElasticBoolQuery, ElasticChild, ElasticFilter, - ElasticMatch, + MatchCriteria, ElasticNested, ElasticParent, GenericExpression, @@ -157,7 +157,7 @@ case class ElasticQuery(filter: ElasticFilter) { case in: InExpr[_, _] => in case between: BetweenExpr => between // case geoDistance: DistanceCriteria => geoDistance - case matchExpression: ElasticMatch => matchExpression + case matchExpression: MatchCriteria => matchExpression case isNull: IsNullCriteria => isNull case isNotNull: IsNotNullCriteria => isNotNull case other => diff --git a/sql/bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/package.scala b/sql/bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/package.scala index 343da652..6ccc2488 100644 --- a/sql/bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/package.scala +++ b/sql/bridge/src/main/scala/app/softnetwork/elastic/sql/bridge/package.scala @@ -27,24 +27,69 @@ package object bridge { implicit def requestToNestedFilterAggregation( request: SQLSearchRequest, innerHitsName: String - ): Option[FilterAggregation] = - request.where.flatMap(_.criteria) match { - case Some(f) => - f.nestedCriteria(innerHitsName) match { - case Nil => None - case cs => - val boolQuery = ElasticBoolQuery(group = true) - cs.map(c => boolQuery.filter(c.asFilter(Option(boolQuery)))) - Some( - filterAgg( - s"filtered_$innerHitsName", - boolQuery.query(request.aggregates.flatMap(_.identifier.innerHitsName).toSet, Option(boolQuery)) + ): Option[FilterAggregation] = { + val having: Option[Query] = + request.having.flatMap(_.criteria) match { + case Some(f) => + f.nestedCriteria(innerHitsName) match { + case Nil => None + case cs => + val boolQuery = ElasticBoolQuery(group = true) + cs.map(c => boolQuery.filter(c.asFilter(Option(boolQuery)))) + Some( + boolQuery.query( + request.aggregates.flatMap(_.identifier.innerHitsName).toSet, + Option(boolQuery) + ) ) - ) - } + } + case _ => + None + } + val where: Option[Query] = + request.where.flatMap(_.criteria) match { + case Some(f) => + f.nestedCriteria(innerHitsName) match { + case Nil => None + case cs => + val boolQuery = ElasticBoolQuery(group = true) + cs.map(c => boolQuery.filter(c.asFilter(Option(boolQuery)))) + Some( + boolQuery.query( + request.aggregates.flatMap(_.identifier.innerHitsName).toSet, + Option(boolQuery) + ) + ) + } + case _ => + None + } + (having, where) match { + case (Some(h), Some(w)) => + Some( + filterAgg( + s"filtered_$innerHitsName", + boolQuery().filter(h, w) + ) + ) + case (Some(h), None) => + Some( + filterAgg( + s"filtered_$innerHitsName", + h + ) + ) + case (None, Some(w)) => + Some( + filterAgg( + s"filtered_$innerHitsName", + w + ) + ) case _ => None } + } implicit def requestToFilterAggregation( request: SQLSearchRequest @@ -296,9 +341,11 @@ package object bridge { case Nil => _search case _ => _search scriptfields scriptFields.map { field => + val context = PainlessContext() + val script = field.painless(Some(context)) scriptField( field.scriptName, - Script(script = field.painless()) + Script(script = s"$context$script") .lang("painless") .scriptType("source") .params(field.identifier.functions.headOption match { @@ -353,7 +400,11 @@ package object bridge { case _ => true })) ) { - return scriptQuery(Script(script = painless()).lang("painless").scriptType("source")) + val context = PainlessContext() + val script = painless(Some(context)) + return scriptQuery( + Script(script = s"$context$script").lang("painless").scriptType("source") + ) } // Geo distance special case identifier.functions.headOption match { @@ -567,10 +618,22 @@ package object bridge { case NE | DIFF => not(rangeQuery(identifier.name) gte script lte script) } case _ => - scriptQuery(Script(script = painless()).lang("painless").scriptType("source")) + val context = PainlessContext() + val script = painless(Some(context)) + scriptQuery( + Script(script = s"$context$script") + .lang("painless") + .scriptType("source") + ) } case _ => - scriptQuery(Script(script = painless()).lang("painless").scriptType("source")) + val context = PainlessContext() + val script = painless(Some(context)) + scriptQuery( + Script(script = s"$context$script") + .lang("painless") + .scriptType("source") + ) } case _ => matchAllQuery() } @@ -724,7 +787,7 @@ package object bridge { case _ => scriptQuery( Script( - script = distanceCriteria.painless(), + script = distanceCriteria.painless(None), lang = Some("painless"), scriptType = Source, params = distance.params @@ -734,7 +797,7 @@ package object bridge { } implicit def matchToQuery( - matchExpression: ElasticMatch + matchExpression: MatchCriteria ): Query = { import matchExpression._ matchQuery(identifier.name, value.value) diff --git a/sql/bridge/src/test/scala/app/softnetwork/elastic/sql/SQLQuerySpec.scala b/sql/bridge/src/test/scala/app/softnetwork/elastic/sql/SQLQuerySpec.scala index 86617c50..3462d3a4 100644 --- a/sql/bridge/src/test/scala/app/softnetwork/elastic/sql/SQLQuerySpec.scala +++ b/sql/bridge/src/test/scala/app/softnetwork/elastic/sql/SQLQuerySpec.scala @@ -28,7 +28,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { "SQLQuery" should "perform native count" in { val results: Seq[ElasticAggregation] = - SQLQuery("select count(t.id) c2 from Table t where t.nom = \"Nom\"") + SQLQuery("select count(t.id) c2 from Table t where t.nom = 'Nom'") results.size shouldBe 1 val result = results.head result.nested shouldBe false @@ -64,7 +64,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { it should "perform count distinct" in { val results: Seq[ElasticAggregation] = - SQLQuery("select count(distinct t.id) as c2 from Table as t where nom = \"Nom\"") + SQLQuery("select count(distinct t.id) as c2 from Table as t where nom = 'Nom'") results.size shouldBe 1 val result = results.head result.nested shouldBe false @@ -101,7 +101,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { it should "perform nested count" in { val results: Seq[ElasticAggregation] = SQLQuery( - "select count(inner_emails.value) as email from index i join unnest(i.emails) as inner_emails where i.nom = \"Nom\"" + "select count(inner_emails.value) as email from index i join unnest(i.emails) as inner_emails where i.nom = 'Nom'" ) results.size shouldBe 1 val result = results.head @@ -719,29 +719,106 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "path": "products" | }, | "aggs": { - | "cat": { - | "terms": { - | "field": "products.category.keyword" + | "filtered_inner_products": { + | "filter": { + | "bool": { + | "filter": [ + | { + | "bool": { + | "must_not": [ + | { + | "term": { + | "products.category": { + | "value": "coffee" + | } + | } + | } + | ] + | } + | }, + | { + | "match_all": {} + | }, + | { + | "match_all": {} + | }, + | { + | "bool": { + | "should": [ + | { + | "match": { + | "products.name": { + | "query": "lasagnes" + | } + | } + | }, + | { + | "match": { + | "products.description": { + | "query": "lasagnes" + | } + | } + | }, + | { + | "match": { + | "products.ingredients": { + | "query": "lasagnes" + | } + | } + | } + | ] + | } + | }, + | { + | "range": { + | "products.stock": { + | "gt": 0 + | } + | } + | }, + | { + | "term": { + | "products.upForSale": { + | "value": true + | } + | } + | }, + | { + | "term": { + | "products.deleted": { + | "value": false + | } + | } + | } + | ] + | } | }, | "aggs": { - | "min_price": { - | "min": { - | "field": "products.price" - | } - | }, - | "max_price": { - | "max": { - | "field": "products.price" - | } - | }, - | "having_filter": { - | "bucket_selector": { - | "buckets_path": { - | "min_price": "inner_products>min_price", - | "max_price": "inner_products>max_price" + | "cat": { + | "terms": { + | "field": "products.category.keyword" + | }, + | "aggs": { + | "min_price": { + | "min": { + | "field": "products.price" + | } | }, - | "script": { - | "source": "params.min_price > 5.0 && params.max_price < 50.0" + | "max_price": { + | "max": { + | "field": "products.price" + | } + | }, + | "having_filter": { + | "bucket_selector": { + | "buckets_path": { + | "min_price": "inner_products>min_price", + | "max_price": "inner_products>max_price" + | }, + | "script": { + | "source": "params.min_price > 5.0 && params.max_price < 50.0" + | } + | } | } | } | } @@ -773,7 +850,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "ct": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.minus(35, ChronoUnit.MINUTES) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.minus(35, ChronoUnit.MINUTES)); param1" | } | } | }, @@ -784,7 +861,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("if\\(", "if (") .replaceAll("!=null", " != null") @@ -965,7 +1042,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { it should "handle group by with having and date time functions" in { val select: ElasticSearchRequest = - SQLQuery(groupByWithHavingAndDateTimeFunctions.replace("GROUP BY 3, 2", "GROUP BY 3, 2")) + SQLQuery(groupByWithHavingAndDateTimeFunctions) val query = select.query println(query) query shouldBe @@ -1026,7 +1103,76 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll(">", " > ") } - it should "handle parse_date function" in { + it should "handle group by index" in { + val select: ElasticSearchRequest = + SQLQuery( + groupByWithHavingAndDateTimeFunctions.replace("GROUP BY Country, City", "GROUP BY 3, 2") + ) + val query = select.query + println(query) + query shouldBe + """{ + | "query": { + | "match_all": {} + | }, + | "size": 0, + | "_source": true, + | "aggs": { + | "Country": { + | "terms": { + | "field": "Country.keyword", + | "exclude": [ + | "USA" + | ], + | "order": { + | "_key": "asc" + | } + | }, + | "aggs": { + | "City": { + | "terms": { + | "field": "City.keyword", + | "exclude": [ + | "Berlin" + | ] + | }, + | "aggs": { + | "cnt": { + | "value_count": { + | "field": "CustomerID" + | } + | }, + | "lastSeen": { + | "max": { + | "field": "createdAt" + | } + | }, + | "having_filter": { + | "bucket_selector": { + | "buckets_path": { + | "cnt": "cnt", + | "lastSeen": "lastSeen" + | }, + | "script": { + | "source": "params.cnt > 1 && params.lastSeen > ZonedDateTime.now(ZoneId.of('Z')).minus(7, ChronoUnit.DAYS).toInstant().toEpochMilli()" + | } + | } + | } + | } + | } + | } + | } + | } + |}""".stripMargin + .replaceAll("\\s", "") + .replaceAll("ChronoUnit", " ChronoUnit") + .replaceAll("==", " == ") + .replaceAll("!=", " != ") + .replaceAll("&&", " && ") + .replaceAll(">", " > ") + } + + it should "handle date_parse function" in { val select: ElasticSearchRequest = SQLQuery(dateParse) val query = select.query @@ -1065,7 +1211,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "field": "createdAt", | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? DateTimeFormatter.ofPattern('yyyy-MM-dd').parse(e0, LocalDate::from) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); (param1 == null) ? null : LocalDate.parse(param1, DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"))" | } | } | } @@ -1074,7 +1220,102 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") + .replaceAll("defe", "def e") + .replaceAll("if\\(", "if (") + .replaceAll("=\\(", " = (") + .replaceAll("\\?", " ? ") + .replaceAll(":null", " : null") + .replaceAll("null:", "null : ") + .replaceAll("return", " return ") + .replaceAll(";", "; ") + .replaceAll(",ChronoUnit", ", ChronoUnit") + .replaceAll("=DateTimeFormatter", " = DateTimeFormatter") + .replaceAll(",DateTimeFormatter", ", DateTimeFormatter") + .replaceAll("==", " == ") + .replaceAll("!=", " != ") + .replaceAll("&&", " && ") + .replaceAll("\\|\\|", " || ") + .replaceAll(">", " > ") + .replaceAll(",LocalDate", ", LocalDate") + } + + it should "handle date_format function" in { + val select: ElasticSearchRequest = + SQLQuery(dateFormat) + val query = select.query + println(query) + query shouldBe + """{ + | "query": { + | "bool": { + | "filter": [ + | { + | "exists": { + | "field": "identifier2" + | } + | } + | ] + | } + | }, + | "script_fields": { + | "y": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.withDayOfYear(1).truncatedTo(ChronoUnit.DAYS)); def param2 = DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"); (param1 == null) ? null : param2.format(param1)" + | } + | }, + | "q": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); def param2 = param1 != null ? param1.withMonth((((param1.getMonthValue() - 1) / 3) * 3) + 1).withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS) : null; def param3 = DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"); (param1 == null) ? null : param3.format(param2)" + | } + | }, + | "m": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS)); def param2 = DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"); (param1 == null) ? null : param2.format(param1)" + | } + | }, + | "w": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.with(DayOfWeek.SUNDAY).truncatedTo(ChronoUnit.DAYS)); def param2 = DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"); (param1 == null) ? null : param2.format(param1)" + | } + | }, + | "d": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.truncatedTo(ChronoUnit.DAYS)); def param2 = DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"); (param1 == null) ? null : param2.format(param1)" + | } + | }, + | "h": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.truncatedTo(ChronoUnit.HOURS)); def param2 = DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"); (param1 == null) ? null : param2.format(param1)" + | } + | }, + | "m2": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.truncatedTo(ChronoUnit.MINUTES)); def param2 = DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"); (param1 == null) ? null : param2.format(param1)" + | } + | }, + | "lastSeen": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.truncatedTo(ChronoUnit.SECONDS)); def param2 = DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"); (param1 == null) ? null : param2.format(param1)" + | } + | } + | }, + | "_source": { + | "includes": [ + | "identifier" + | ] + | } + |}""".stripMargin + .replaceAll("\\s", "") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("if\\(", "if (") .replaceAll("=\\(", " = (") @@ -1084,7 +1325,13 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("return", " return ") .replaceAll(";", "; ") .replaceAll(",ChronoUnit", ", ChronoUnit") + .replaceAll("=DateTimeFormatter", " = DateTimeFormatter") .replaceAll("==", " == ") + .replaceAll("-(\\d)", " - $1") + .replaceAll("\\+", " + ") + .replaceAll("/", " / ") + .replaceAll("\\*", " * ") + .replaceAll("=p", " = p") .replaceAll("!=", " != ") .replaceAll("&&", " && ") .replaceAll("\\|\\|", " || ") @@ -1092,7 +1339,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll(",LocalDate", ", LocalDate") } - it should "handle parse_datetime function" in { + it should "handle datetime_parse function" in { // #25 val select: ElasticSearchRequest = SQLQuery(dateTimeParse) val query = select.query @@ -1131,7 +1378,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "field": "createdAt", | "script": { | "lang": "painless", - | "source": "(def e2 = (def e1 = (def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? DateTimeFormatter.ofPattern('yyyy-MM-dd HH:mm:ss.SSS XXX').parse(e0, ZonedDateTime::from) : null); e1 != null ? e1.truncatedTo(ChronoUnit.MINUTES) : null); e2 != null ? e2.get(ChronoField.YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); (param1 == null) ? null : ZonedDateTime.parse(param1, DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss.SSS XXX\")).truncatedTo(ChronoUnit.MINUTES).get(ChronoField.YEAR)" | } | } | } @@ -1140,7 +1387,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("if\\(", "if (") .replaceAll("=\\(", " = (") @@ -1155,10 +1402,66 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("\\|\\|", " || ") .replaceAll(">", " > ") .replaceAll(",ZonedDateTime", ", ZonedDateTime") + .replaceAll("=DateTimeFormatter", " = DateTimeFormatter") + .replaceAll(",DateTimeFormatter", ", DateTimeFormatter") .replaceAll("SSSXXX", "SSS XXX") .replaceAll("ddHH", "dd HH") } + it should "handle datetime_format function" in { + val select: ElasticSearchRequest = + SQLQuery(dateTimeFormat) + val query = select.query + println(query) + query shouldBe + """{ + | "query": { + | "bool": { + | "filter": [ + | { + | "exists": { + | "field": "identifier2" + | } + | } + | ] + | } + | }, + | "script_fields": { + | "lastSeen": { + | "script": { + | "lang": "painless", + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS)); def param2 = DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss XXX\"); (param1 == null) ? null : param2.format(param1)" + | } + | } + | }, + | "_source": { + | "includes": [ + | "identifier" + | ] + | } + |}""".stripMargin + .replaceAll("\\s", "") + .replaceAll("defp", "def p") + .replaceAll("defe", "def e") + .replaceAll("if\\(", "if (") + .replaceAll("=\\(", " = (") + .replaceAll("\\?", " ? ") + .replaceAll(":null", " : null") + .replaceAll("null:", "null : ") + .replaceAll("return", " return ") + .replaceAll(";", "; ") + .replaceAll("==", " == ") + .replaceAll("!=", " != ") + .replaceAll("&&", " && ") + .replaceAll("\\|\\|", " || ") + .replaceAll(">", " > ") + .replaceAll(",ZonedDateTime", ", ZonedDateTime") + .replaceAll("=DateTimeFormatter", " = DateTimeFormatter") + .replaceAll("SSSXXX", "SSS XXX") + .replaceAll("ddHH", "dd HH") + .replaceAll("XXX", " XXX") + } + it should "handle date_diff function as script field" in { val select: ElasticSearchRequest = SQLQuery(dateDiff) @@ -1173,7 +1476,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "diff": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('updatedAt') || doc['updatedAt'].empty ? null : doc['updatedAt'].value); def arg1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); (arg0 == null || arg1 == null) ? null : ChronoUnit.DAYS.between(arg0, arg1))" + | "source": "def param1 = (!doc.containsKey('updatedAt') || doc['updatedAt'].empty ? null : doc['updatedAt'].value); def param2 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); (param1 == null || param2 == null) ? null : ChronoUnit.DAYS.between(param1, param2)" | } | } | }, @@ -1184,7 +1487,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("defa", "def a") .replaceAll("if\\(", "if (") @@ -1193,7 +1496,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll(":null", " : null") .replaceAll("null:", "null : ") .replaceAll("return", " return ") - .replaceAll(",a", ", a") + .replaceAll(",p", ", p") .replaceAll(";", "; ") .replaceAll("==", " == ") .replaceAll("!=", " != ") @@ -1223,7 +1526,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "max": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('updatedAt') || doc['updatedAt'].empty ? null : doc['updatedAt'].value); def arg1 = (def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? DateTimeFormatter.ofPattern('yyyy-MM-dd HH:mm:ss.SSS XXX').parse(e0, ZonedDateTime::from) : null); (arg0 == null || arg1 == null) ? null : ChronoUnit.DAYS.between(arg0, arg1))" + | "source": "def param1 = (!doc.containsKey('updatedAt') || doc['updatedAt'].empty ? null : doc['updatedAt'].value); def param2 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); def param3 = (param2 == null) ? null : ZonedDateTime.parse(param2, DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss.SSS XXX\")); (param1 == null || param2 == null) ? null : ChronoUnit.DAYS.between(param1, param3)" | } | } | } @@ -1232,7 +1535,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("defa", "def a") .replaceAll("if\\(", "if (") @@ -1241,13 +1544,14 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll(":null", " : null") .replaceAll("null:", "null : ") .replaceAll("return", " return ") - .replaceAll(",a", ", a") + .replaceAll(",p", ", p") .replaceAll(";", "; ") .replaceAll("==", " == ") .replaceAll("!=", " != ") .replaceAll("&&", " && ") .replaceAll("\\|\\|", " || ") - .replaceAll("ZonedDateTime", " ZonedDateTime") + .replaceAll("=DateTimeFormatter", " = DateTimeFormatter") + .replaceAll(",DateTimeFormatter", ", DateTimeFormatter") .replaceAll("SSSXXX", "SSS XXX") .replaceAll("ddHH", "dd HH") } @@ -1274,7 +1578,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "lastSeen": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); e0 != null ? e0.plus(10, ChronoUnit.DAYS) : null)" + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.plus(10, ChronoUnit.DAYS)); param1" | } | } | }, @@ -1285,7 +1589,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("defs", "def s") .replaceAll("if\\(", "if (") @@ -1303,7 +1607,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("ChronoUnit", " ChronoUnit") } - it should "handle date_sub function as script field" in { + it should "handle date_sub function as script field" in { // 30 val select: ElasticSearchRequest = SQLQuery(dateSub) val query = select.query @@ -1325,7 +1629,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "lastSeen": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); e0 != null ? e0.minus(10, ChronoUnit.DAYS) : null)" + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.minus(10, ChronoUnit.DAYS)); param1" | } | } | }, @@ -1336,7 +1640,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("defs", "def s") .replaceAll("if\\(", "if (") @@ -1376,7 +1680,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "lastSeen": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); e0 != null ? e0.plus(10, ChronoUnit.DAYS) : null)" + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.plus(10, ChronoUnit.DAYS)); param1" | } | } | }, @@ -1387,7 +1691,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("defs", "def s") .replaceAll("if\\(", "if (") @@ -1427,7 +1731,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "lastSeen": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); e0 != null ? e0.minus(10, ChronoUnit.DAYS) : null)" + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.minus(10, ChronoUnit.DAYS)); param1" | } | } | }, @@ -1438,7 +1742,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("defs", "def s") .replaceAll("if\\(", "if (") @@ -1470,20 +1774,20 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "flag": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); e0 == null)" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); param1 == null" | } | } | }, | "_source": true |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("defs", "def s") .replaceAll("if\\(", "if (") .replaceAll("=\\(", " = (") .replaceAll("\\?", " ? ") - .replaceAll(":null", " : null") + .replaceAll(":false", " : false") .replaceAll("null:", "null : ") .replaceAll("return", " return ") .replaceAll("between\\(s,", "between(s, ") @@ -1508,7 +1812,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "flag": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null)" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); param1 != null" | } | } | }, @@ -1519,13 +1823,13 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("defs", "def s") .replaceAll("if\\(", "if (") .replaceAll("=\\(", " = (") .replaceAll("\\?", " ? ") - .replaceAll(":null", " : null") + .replaceAll(":true", " : true") .replaceAll("null:", "null : ") .replaceAll("return", " return ") .replaceAll("between\\(s,", "between(s, ") @@ -1608,7 +1912,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "c": { | "script": { | "lang": "painless", - | "source": "{ def v0 = (def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.minus(35, ChronoUnit.MINUTES) : null);if (v0 != null) return v0; return ZonedDateTime.now(ZoneId.of('Z')).toLocalDate().atStartOfDay(ZoneId.of('Z')); }" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.minus(35, ChronoUnit.MINUTES)); def param2 = ZonedDateTime.now(ZoneId.of('Z')).toLocalDate(); param1 != null ? param1 : param2" | } | } | }, @@ -1619,6 +1923,8 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") + .replaceAll(";defp", "; defp") + .replaceAll("defp", "def p") .replaceAll("defv", " def v") .replaceAll("defe", "def e") .replaceAll("if\\(", "if (") @@ -1627,6 +1933,9 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll(":null", " : null") .replaceAll(";}", "; }") .replaceAll(";e", "; e") + .replaceAll(";v", "; v") + .replaceAll(":p", " : p") + .replaceAll(";p", "; p") .replaceAll(":null", " : null") .replaceAll("null:", "null : ") .replaceAll("return", " return ") @@ -1635,6 +1944,8 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("&&", " && ") .replaceAll("\\|\\|", " || ") .replaceAll("ChronoUnit", " ChronoUnit") + .replaceAll("=ZonedDateTime", " = ZonedDateTime") + .replaceAll(":ZonedDateTime", " : ZonedDateTime") } it should "handle nullif function as script field" in { @@ -1651,7 +1962,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "c": { | "script": { | "lang": "painless", - | "source": "{ def v0 = ((def arg0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); (arg0 == null) ? null : arg0 == DateTimeFormatter.ofPattern('yyyy-MM-dd').parse(\"2025-09-11\", LocalDate::from).minus(2, ChronoUnit.DAYS) ? null : arg0));if (v0 != null) return v0; return ZonedDateTime.now(ZoneId.of('Z')).toLocalDate(); }" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.toLocalDate()); def param2 = LocalDate.parse(\"2025-09-11\", DateTimeFormatter.ofPattern(\"yyyy-MM-dd\")).minus(2, ChronoUnit.DAYS); def param3 = param1 == null || param1.isEqual(param2) ? null : param1; def param4 = ZonedDateTime.now(ZoneId.of('Z')).toLocalDate(); param3 != null ? param3 : param4" | } | } | }, @@ -1662,6 +1973,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") + .replaceAll("defp", "def p") .replaceAll("defv", " def v") .replaceAll("defa", "def a") .replaceAll("if\\(", "if (") @@ -1672,6 +1984,10 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("between\\(s,", "between(s, ") .replaceAll(";def", "; def") .replaceAll(";return", "; return") + .replaceAll(";v", "; v") + .replaceAll("(\\d)=p", "$1 = p") + .replaceAll(";p", "; p") + .replaceAll(":p", " : p") .replaceAll("returnv", " return v") .replaceAll("returne", " return e") .replaceAll(";}", "; }") @@ -1683,8 +1999,11 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll(";\\s\\s", "; ") .replaceAll("ChronoUnit", " ChronoUnit") .replaceAll(",LocalDate", ", LocalDate") + .replaceAll("=LocalDate", " = LocalDate") .replaceAll("=DateTimeFormatter", " = DateTimeFormatter") - .replaceAll("ZonedDateTime", " ZonedDateTime") + .replaceAll(",DateTimeFormatter", ", DateTimeFormatter") + .replaceAll("=ZonedDateTime", " = ZonedDateTime") + .replaceAll(":ZonedDateTime", " : ZonedDateTime") } it should "handle cast function as script field" in { @@ -1701,19 +2020,19 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "c": { | "script": { | "lang": "painless", - | "source": "try { def v0 = ((def arg0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); (arg0 == null) ? null : arg0 == DateTimeFormatter.ofPattern('yyyy-MM-dd').parse(\"2025-09-11\", LocalDate::from) ? null : arg0));if (v0 != null) return v0; return ZonedDateTime.now(ZoneId.of('Z')).toLocalDate().atStartOfDay(ZoneId.of('Z')).minus(2, ChronoUnit.HOURS).toInstant().toEpochMilli(); } catch (Exception e) { return null; }" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.toLocalDate()); def param2 = LocalDate.parse(\"2025-09-11\", DateTimeFormatter.ofPattern(\"yyyy-MM-dd\")); def param3 = param1 == null || param1.isEqual(param2) ? null : param1; def param4 = ZonedDateTime.now(ZoneId.of('Z')).toLocalDate().minus(2, ChronoUnit.HOURS); try { param3 != null ? param3 : param4 } catch (Exception e) { return null; }" | } | }, | "c2": { | "script": { | "lang": "painless", - | "source": "ZonedDateTime.now(ZoneId.of('Z')).toInstant().toEpochMilli()" + | "source": "def param1 = ZonedDateTime.now(ZoneId.of('Z')); param1.toInstant().toEpochMilli()" | } | }, | "c3": { | "script": { | "lang": "painless", - | "source": "ZonedDateTime.now(ZoneId.of('Z')).toLocalDate()" + | "source": "def param1 = ZonedDateTime.now(ZoneId.of('Z')); param1.toLocalDate()" | } | }, | "c4": { @@ -1725,7 +2044,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "c5": { | "script": { | "lang": "painless", - | "source": "LocalDate.parse(\"2025-09-11\", DateTimeFormatter.ofPattern('yyyy-MM-dd'))" + | "source": "def param1 = LocalDate.parse(\"2025-09-11\", DateTimeFormatter.ofPattern(\"yyyy-MM-dd\")); param1" | } | } | }, @@ -1736,6 +2055,8 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") + .replaceAll(";defp", "; defp") + .replaceAll("defp", "def p") .replaceAll("defv", " def v") .replaceAll("defa", "def a") .replaceAll("if\\(", "if (") @@ -1759,6 +2080,14 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("\\}catch", "} catch ") .replaceAll("Exceptione\\)", "Exception e) ") .replaceAll(",DateTimeFormatter", ", DateTimeFormatter") + .replaceAll("(\\d)=p", "$1 = p") + .replaceAll(";p", "; p") + .replaceAll(":p", " : p") + .replaceAll("=ZonedDateTime", " = ZonedDateTime") + .replaceAll("=LocalDate", " = LocalDate") + .replaceAll(":ZonedDateTime", " : ZonedDateTime") + .replaceAll("try \\{", "try { ") + .replaceAll("} catch", " } catch") } it should "handle case function as script field" in { @@ -1775,7 +2104,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "c": { | "script": { | "lang": "painless", - | "source": "{ if (def left = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); left == null ? false : left > ZonedDateTime.now(ZoneId.of('Z')).minus(7, ChronoUnit.DAYS)) return left; if (def left = (!doc.containsKey('lastSeen') || doc['lastSeen'].empty ? null : doc['lastSeen'].value); left != null) return left.plus(2, ChronoUnit.DAYS); def dval = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); return dval; }" + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); def param2 = ZonedDateTime.now(ZoneId.of('Z')).minus(7, ChronoUnit.DAYS); def param3 = param1 == null ? false : (param1.isAfter(param2)); def param4 = (!doc.containsKey('lastSeen') || doc['lastSeen'].empty ? null : doc['lastSeen'].value.plus(2, ChronoUnit.DAYS)); def param5 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); param3 ? param1 : param4 != null ? param4 : param5" | } | } | }, @@ -1786,7 +2115,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defd", " def d") .replaceAll("defe", " def e") .replaceAll("defl", " def l") @@ -1809,6 +2138,9 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll(">", " > ") .replaceAll("if \\(\\s*def", "if (def") .replaceAll("ChronoUnit", " ChronoUnit") + .replaceAll("=ZonedDateTime", " = ZonedDateTime") + .replaceAll("=p", " = p") + .replaceAll(":p", " : p") } it should "handle case with expression function as script field" in { @@ -1825,7 +2157,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "c": { | "script": { | "lang": "painless", - | "source": "{ def expr = ZonedDateTime.now(ZoneId.of('Z')).toLocalDate().minus(7, ChronoUnit.DAYS); def e0 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); def val0 = e0 != null ? e0.minus(3, ChronoUnit.DAYS).atStartOfDay(ZoneId.of('Z')) : null; if (expr == val0) return e0; def val1 = (!doc.containsKey('lastSeen') || doc['lastSeen'].empty ? null : doc['lastSeen'].value); if (expr == val1) return val1.plus(2, ChronoUnit.DAYS); def dval = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); return dval; }" + | "source": "def param1 = ZonedDateTime.now(ZoneId.of('Z')).toLocalDate().minus(7, ChronoUnit.DAYS); def param2 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.toLocalDate().minus(3, ChronoUnit.DAYS)); def param3 = (!doc.containsKey('lastSeen') || doc['lastSeen'].empty ? null : doc['lastSeen'].value.toLocalDate().plus(2, ChronoUnit.DAYS)); def param4 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.toLocalDate()); param1 != null && param1.isEqual(param2) ? param2 : param1 != null && param1.isEqual(param3) ? param3 : param4" | } | } | }, @@ -1836,7 +2168,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defd", " def d") .replaceAll("defe", " def e") .replaceAll("defl", " def l") @@ -1861,6 +2193,9 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("ChronoUnit", " ChronoUnit") .replaceAll("=ZonedDateTime", " = ZonedDateTime") .replaceAll("=e", " = e") + .replaceAll("=ZonedDateTime", " = ZonedDateTime") + .replaceAll("=p", " = p") + .replaceAll(":p", " : p") } it should "handle extract function as script field" in { @@ -1877,98 +2212,98 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "dom": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.DAY_OF_MONTH) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.DAY_OF_MONTH)); param1" | } | }, | "dow": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.DAY_OF_WEEK) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.DAY_OF_WEEK)); param1" | } | }, | "doy": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.DAY_OF_YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.DAY_OF_YEAR)); param1" | } | }, | "m": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.MONTH_OF_YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.MONTH_OF_YEAR)); param1" | } | }, | "y": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.YEAR)); param1" | } | }, | "h": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.HOUR_OF_DAY) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.HOUR_OF_DAY)); param1" | } | }, | "minutes": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.MINUTE_OF_HOUR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.MINUTE_OF_HOUR)); param1" | } | }, | "s": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.SECOND_OF_MINUTE) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.SECOND_OF_MINUTE)); param1" | } | }, | "nano": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.NANO_OF_SECOND) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.NANO_OF_SECOND)); param1" | } | }, | "micro": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.MICRO_OF_SECOND) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.MICRO_OF_SECOND)); param1" | } | }, | "milli": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.MILLI_OF_SECOND) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.MILLI_OF_SECOND)); param1" | } | }, | "epoch": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.EPOCH_DAY) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.EPOCH_DAY)); param1" | } | }, | "off": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.OFFSET_SECONDS) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.OFFSET_SECONDS)); param1" | } | }, | "w": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(java.time.temporal.IsoFields.WEEK_OF_WEEK_BASED_YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(java.time.temporal.IsoFields.WEEK_OF_WEEK_BASED_YEAR)); param1" | } | }, | "q": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(java.time.temporal.IsoFields.QUARTER_OF_YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(java.time.temporal.IsoFields.QUARTER_OF_YEAR)); param1" | } | } | }, | "_source": true |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defe", "def e") + .replaceAll("defp", "def p") .replaceAll("if\\(", "if (") .replaceAll("=\\(", " = (") .replaceAll("\\?", " ? ") @@ -2001,7 +2336,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "script": { | "script": { | "lang": "painless", - | "source": "def lv0 = ((!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value)); ( lv0 == null ) ? null : (lv0 * (ZonedDateTime.now(ZoneId.of('Z')).toLocalDate().get(ChronoField.YEAR) - 10)) > 10000" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); def param2 = ZonedDateTime.now(ZoneId.of('Z')).toLocalDate().get(ChronoField.YEAR); (param1 == null) ? null : (param1 * (param2 - 10)) > 10000" | } | } | } @@ -2012,37 +2347,37 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "add": { | "script": { | "lang": "painless", - | "source": "def lv0 = ((!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value)); ( lv0 == null ) ? null : (lv0 + 1)" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : (param1 + 1)" | } | }, | "sub": { | "script": { | "lang": "painless", - | "source": "def lv0 = ((!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value)); ( lv0 == null ) ? null : (lv0 - 1)" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : (param1 - 1)" | } | }, | "mul": { | "script": { | "lang": "painless", - | "source": "def lv0 = ((!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value)); ( lv0 == null ) ? null : (lv0 * 2)" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : (param1 * 2)" | } | }, | "div": { | "script": { | "lang": "painless", - | "source": "def lv0 = ((!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value)); ( lv0 == null ) ? null : (lv0 / 2)" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : (param1 / 2)" | } | }, | "mod": { | "script": { | "lang": "painless", - | "source": "def lv0 = ((!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value)); ( lv0 == null ) ? null : (lv0 % 2)" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : (param1 % 2)" | } | }, | "identifier_mul_identifier2_minus_10": { | "script": { | "lang": "painless", - | "source": "def lv0 = ((def lv1 = ((!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value)); def rv1 = ((!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value)); ( lv1 == null || rv1 == null ) ? null : (lv1 * rv1))); ( lv0 == null ) ? null : (lv0 - 10)" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); def param2 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); def lv0 = ((param1 == null || param2 == null) ? null : (param1 * param2)); (lv0 == null) ? null : (lv0 - 10)" | } | } | }, @@ -2053,12 +2388,14 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s", "") - .replaceAll("defv", "def v") + .replaceAll("defp", "def p") .replaceAll("defe", "def e") .replaceAll("defl", "def l") .replaceAll("defr", "def r") .replaceAll("if\\(", "if (") .replaceAll("=\\(", " = (") + // .replaceAll("(\\d)=", "$1 =") + .replaceAll("=ZonedDateTime", " = ZonedDateTime") .replaceAll("\\?", " ? ") .replaceAll(":null", " : null") .replaceAll("null:", "null : ") @@ -2088,7 +2425,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "script": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.sqrt(arg0)) > 100.0" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.sqrt(param1) > 100.0" | } | } | } @@ -2099,109 +2436,109 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "abs_identifier_plus_1_0_mul_2": { | "script": { | "lang": "painless", - | "source": "((def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.abs(arg0)) + 1.0) * ((double) 2)" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); ((param1 == null) ? null : Math.abs(param1) + 1.0) * ((double) 2)" | } | }, | "ceil_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.ceil(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.ceil(param1)" | } | }, | "floor_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.floor(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.floor(param1)" | } | }, | "sqrt_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.sqrt(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.sqrt(param1)" | } | }, | "exp_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.exp(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.exp(param1)" | } | }, | "log_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.log(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.log(param1)" | } | }, | "log10_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.log10(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.log10(param1)" | } | }, | "pow_identifier_3": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.pow(arg0, 3))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.pow(param1, 3)" | } | }, | "round_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : (def p = Math.pow(10, 0); Math.round((arg0 * p) / p)))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); def param2 = Math.pow(10, 0); (param1 == null || param2 == null) ? null : Math.round((param1 * param2) / param2)" | } | }, | "round_identifier_2": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : (def p = Math.pow(10, 2); Math.round((arg0 * p) / p)))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); def param2 = Math.pow(10, 2); (param1 == null || param2 == null) ? null : Math.round((param1 * param2) / param2)" | } | }, | "sign_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); arg0 != null ? (arg0 > 0 ? 1 : (arg0 < 0 ? -1 : 0)) : null)" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : (param1 > 0 ? 1 : (param1 < 0 ? -1 : 0))" | } | }, | "cos_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.cos(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.cos(param1)" | } | }, | "acos_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.acos(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.acos(param1)" | } | }, | "sin_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.sin(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.sin(param1)" | } | }, | "asin_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.asin(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.asin(param1)" | } | }, | "tan_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.tan(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.tan(param1)" | } | }, | "atan_identifier": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.atan(arg0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.atan(param1)" | } | }, | "atan2_identifier_3_0": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (arg0 == null) ? null : Math.atan2(arg0, 3.0))" + | "source": "def param1 = (!doc.containsKey('identifier') || doc['identifier'].empty ? null : doc['identifier'].value); (param1 == null) ? null : Math.atan2(param1, 3.0)" | } | } | }, @@ -2212,7 +2549,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defa", "def a") .replaceAll("defp", "def p") .replaceAll("if\\(", "if (") @@ -2258,7 +2595,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "script": { | "script": { | "lang": "painless", - | "source": "def left = (def e1 = (def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.trim() : null); e1 != null ? e1.length() : null); left == null ? false : left > 10" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.trim().length() > 10" | } | } | } @@ -2269,85 +2606,85 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "len": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.length() : null)" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.length()" | } | }, | "low": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.lower() : null)" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.toLowerCase()" | } | }, | "upp": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.upper() : null)" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.toUpperCase()" | } | }, | "sub": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : arg0.substring(1 - 1, Math.min(1 - 1 + 3, arg0.length())))" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.substring(0, Math.min(3, param1.length()))" | } | }, | "tr": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.trim() : null)" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.trim()" | } | }, | "ltr": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.replaceAll(\"^\\\\s+\",\"\") : null)" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.replaceAll(\"^\\\\s+\",\"\")" | } | }, | "rtr": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); e0 != null ? e0.replaceAll(\"\\\\s+$\",\"\") : null)" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.replaceAll(\"\\\\s+$\",\"\")" | } | }, | "con": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : String.valueOf(arg0) + \"_test\" + String.valueOf(1))" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : String.valueOf(param1) + \"_test\" + String.valueOf(1)" | } | }, | "l": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : arg0.substring(0, Math.min(5, arg0.length())))" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.substring(0, Math.min(5, param1.length()))" | } | }, | "r": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : 3 == 0 ? \"\" : arg0.substring(arg0.length() - Math.min(3, arg0.length())))" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.substring(param1.length() - Math.min(3, param1.length()))" | } | }, | "rep": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : arg0.replace(\"el\", \"le\"))" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.replace(\"el\", \"le\")" | } | }, | "rev": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : new StringBuilder(arg0).reverse().toString())" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : new StringBuilder(param1).reverse().toString()" | } | }, | "pos": { | "script": { | "lang": "painless", - | "source": "(def arg1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg1 == null) ? null : arg1.indexOf(\"soft\", 1 - 1) + 1)" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : param1.indexOf(\"soft\", 0) + 1" | } | }, | "reg": { | "script": { | "lang": "painless", - | "source": "(def arg0 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (arg0 == null) ? null : java.util.regex.Pattern.compile(\"soft\", java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.MULTILINE).matcher(arg0).find())" + | "source": "def param1 = (!doc.containsKey('identifier2') || doc['identifier2'].empty ? null : doc['identifier2'].value); (param1 == null) ? null : java.util.regex.Pattern.compile(\"soft\", java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.MULTILINE).matcher(param1).find()" | } | } | }, @@ -2358,7 +2695,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defa", "def a") .replaceAll("defe", "def e") .replaceAll("defl", "def l") @@ -2415,7 +2752,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "hire_date": { | "script": { | "lang": "painless", - | "source": "(!doc.containsKey('hire_date') || doc['hire_date'].empty ? null : doc['hire_date'].value)" + | "source": "def param1 = (!doc.containsKey('hire_date') || doc['hire_date'].empty ? null : doc['hire_date'].value.toLocalDate()); param1" | } | } | }, @@ -2494,7 +2831,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defa", "def a") .replaceAll("defe", "def e") .replaceAll("defl", "def l") @@ -2538,7 +2875,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "script": { | "script": { | "lang": "painless", - | "source": "(def e1 = ZonedDateTime.now(ZoneId.of('Z')).toLocalDate(); e1.withDayOfMonth(e1.lengthOfMonth())).get(ChronoField.DAY_OF_MONTH) > 28" + | "source": "def param1 = ZonedDateTime.now(ZoneId.of('Z')); param1.toLocalDate().withDayOfMonth(param1.toLocalDate().lengthOfMonth()).get(ChronoField.DAY_OF_MONTH) > 28" | } | } | } @@ -2549,7 +2886,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "ld": { | "script": { | "lang": "painless", - | "source": "(def e1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e1 != null ? e1.withDayOfMonth(e1.lengthOfMonth()) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.toLocalDate()); (param1 == null) ? null : param1.withDayOfMonth(param1.lengthOfMonth())" | } | } | }, @@ -2560,7 +2897,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | } |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defa", "def a") .replaceAll("defe", "def e") .replaceAll("defl", "def l") @@ -2605,98 +2942,98 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "y": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.YEAR)); param1" | } | }, | "m": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.MONTH_OF_YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.MONTH_OF_YEAR)); param1" | } | }, | "wd": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.DAY_OF_WEEK) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); (param1 == null) ? null : (param1.get(ChronoField.DAY_OF_WEEK) + 6) % 7" | } | }, | "yd": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.DAY_OF_YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.DAY_OF_YEAR)); param1" | } | }, | "d": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.DAY_OF_MONTH) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.DAY_OF_MONTH)); param1" | } | }, | "h": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.HOUR_OF_DAY) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.HOUR_OF_DAY)); param1" | } | }, | "minutes": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.MINUTE_OF_HOUR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.MINUTE_OF_HOUR)); param1" | } | }, | "s": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.SECOND_OF_MINUTE) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.SECOND_OF_MINUTE)); param1" | } | }, | "nano": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.NANO_OF_SECOND) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.NANO_OF_SECOND)); param1" | } | }, | "micro": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.MICRO_OF_SECOND) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.MICRO_OF_SECOND)); param1" | } | }, | "milli": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.MILLI_OF_SECOND) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.MILLI_OF_SECOND)); param1" | } | }, | "epoch": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.EPOCH_DAY) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.EPOCH_DAY)); param1" | } | }, | "off": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(ChronoField.OFFSET_SECONDS) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(ChronoField.OFFSET_SECONDS)); param1" | } | }, | "w": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(java.time.temporal.IsoFields.WEEK_OF_WEEK_BASED_YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(java.time.temporal.IsoFields.WEEK_OF_WEEK_BASED_YEAR)); param1" | } | }, | "q": { | "script": { | "lang": "painless", - | "source": "(def e0 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value); e0 != null ? e0.get(java.time.temporal.IsoFields.QUARTER_OF_YEAR) : null)" + | "source": "def param1 = (!doc.containsKey('createdAt') || doc['createdAt'].empty ? null : doc['createdAt'].value.get(java.time.temporal.IsoFields.QUARTER_OF_YEAR)); param1" | } | } | }, | "_source": true |}""".stripMargin .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defa", "def a") .replaceAll("defe", "def e") .replaceAll("defl", "def l") @@ -2719,6 +3056,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("-", " - ") .replaceAll("\\*", " * ") .replaceAll("/", " / ") + .replaceAll("%", " % ") .replaceAll(">", " > ") .replaceAll("<", " < ") .replaceAll("!=", " != ") @@ -2874,7 +3212,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "script": { | "script": { | "lang": "painless", - | "source": "def left = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); left == null ? false : left >= (def e2 = LocalDate.parse(\"2025-09-11\", DateTimeFormatter.ofPattern('yyyy-MM-dd')); e2.withDayOfMonth(e2.lengthOfMonth()))" + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.toLocalDate()); def param2 = LocalDate.parse(\"2025-09-11\", DateTimeFormatter.ofPattern(\"yyyy-MM-dd\")); param1 == null ? false : (param1.isBefore(param2.withDayOfMonth(param2.lengthOfMonth())) == false)" | } | } | }, @@ -2899,7 +3237,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { |}""".stripMargin .replaceAll("\\s+", "") .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defa", "def a") .replaceAll("defe", "def e") .replaceAll("defl", "def l") @@ -2964,7 +3302,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "script": { | "script": { | "lang": "painless", - | "source": "def left = (!doc.containsKey('comments.replies.lastUpdated') || doc['comments.replies.lastUpdated'].empty ? null : doc['comments.replies.lastUpdated'].value); left == null ? false : left < (def e2 = LocalDate.parse(\"2025-09-10\", DateTimeFormatter.ofPattern('yyyy-MM-dd')); e2.withDayOfMonth(e2.lengthOfMonth()))" + | "source": "def param1 = (!doc.containsKey('comments.replies.lastUpdated') || doc['comments.replies.lastUpdated'].empty ? null : doc['comments.replies.lastUpdated'].value.toLocalDate()); def param2 = LocalDate.parse(\"2025-09-10\", DateTimeFormatter.ofPattern(\"yyyy-MM-dd\")); param1 == null ? false : (param1.isBefore(param2.withDayOfMonth(param2.lengthOfMonth())))" | } | } | } @@ -3007,7 +3345,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("\\s+", "") .replaceAll("\\s+", "") .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defa", "def a") .replaceAll("defe", "def e") .replaceAll("defl", "def l") @@ -3062,7 +3400,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "script": { | "script": { | "lang": "painless", - | "source": "def left = (!doc.containsKey('replies.lastUpdated') || doc['replies.lastUpdated'].empty ? null : doc['replies.lastUpdated'].value); left == null ? false : left < (def e2 = LocalDate.parse(\"2025-09-10\", DateTimeFormatter.ofPattern('yyyy-MM-dd')); e2.withDayOfMonth(e2.lengthOfMonth()))" + | "source": "def param1 = (!doc.containsKey('replies.lastUpdated') || doc['replies.lastUpdated'].empty ? null : doc['replies.lastUpdated'].value.toLocalDate()); def param2 = LocalDate.parse(\"2025-09-10\", DateTimeFormatter.ofPattern(\"yyyy-MM-dd\")); param1 == null ? false : (param1.isBefore(param2.withDayOfMonth(param2.lengthOfMonth())))" | } | } | }, @@ -3117,7 +3455,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("\\s+", "") .replaceAll("\\s+", "") .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defa", "def a") .replaceAll("defe", "def e") .replaceAll("defl", "def l") @@ -3169,7 +3507,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { | "script": { | "script": { | "lang": "painless", - | "source": "def left = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value); left == null ? false : left < ZonedDateTime.now(ZoneId.of('Z')).toLocalDate()" + | "source": "def param1 = (!doc.containsKey('lastUpdated') || doc['lastUpdated'].empty ? null : doc['lastUpdated'].value.toLocalDate()); def param2 = ZonedDateTime.now(ZoneId.of('Z')).toLocalDate(); param1 == null ? false : (param1.isBefore(param2))" | } | } | } @@ -3221,7 +3559,7 @@ class SQLQuerySpec extends AnyFlatSpec with Matchers { .replaceAll("\\s+", "") .replaceAll("\\s+", "") .replaceAll("\\s+", "") - .replaceAll("defv", " def v") + .replaceAll("defp", "def p") .replaceAll("defa", "def a") .replaceAll("defe", "def e") .replaceAll("defl", "def l") diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/SQLImplicits.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/SQLImplicits.scala index 377867e9..924270c2 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/SQLImplicits.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/SQLImplicits.scala @@ -1,3 +1,19 @@ +/* + * 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 import app.softnetwork.elastic.sql.parser.Parser diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/function/aggregate/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/function/aggregate/package.scala index babe2357..3dbd08b5 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/function/aggregate/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/function/aggregate/package.scala @@ -1,3 +1,19 @@ +/* + * 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.function import app.softnetwork.elastic.sql.query.{Bucket, Field, Limit, OrderBy, SQLSearchRequest} diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/function/cond/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/function/cond/package.scala index 7a870dbc..9670a497 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/function/cond/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/function/cond/package.scala @@ -1,13 +1,44 @@ +/* + * 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.function -import app.softnetwork.elastic.sql.{Expr, Identifier, PainlessScript, TokenRegex} -import app.softnetwork.elastic.sql.`type`.{SQLAny, SQLBool, SQLType, SQLTypeUtils, SQLTypes} -import app.softnetwork.elastic.sql.query.Expression +import app.softnetwork.elastic.sql.{ + Expr, + Identifier, + LiteralParam, + PainlessContext, + PainlessScript, + TokenRegex +} +import app.softnetwork.elastic.sql.`type`.{ + SQLAny, + SQLBool, + SQLTemporal, + SQLType, + SQLTypeUtils, + SQLTypes +} +import app.softnetwork.elastic.sql.parser.Validator +import app.softnetwork.elastic.sql.query.{CriteriaWithConditionalFunction, Expression} package object cond { sealed trait ConditionalOp extends PainlessScript with TokenRegex { - override def painless(): String = sql + override def painless(context: Option[PainlessContext] = None): String = sql } case object Coalesce extends Expr("COALESCE") with ConditionalOp @@ -32,7 +63,6 @@ package object cond { override def outputType: SQLBool = SQLTypes.Boolean - override def toPainless(base: String, idx: Int): String = s"($base${painless()})" } case class IsNull(identifier: Identifier) extends ConditionalFunction[SQLAny] { @@ -44,13 +74,14 @@ package object cond { override def toSQL(base: String): String = sql - override def painless(): String = s" == null" - override def toPainless(base: String, idx: Int): String = { - if (nullable) - s"(def e$idx = $base; e$idx${painless()})" - else - s"$base${painless()}" - } + override def checkIfNullable: Boolean = false + + override def toPainlessCall(callArgs: List[String], context: Option[PainlessContext]): String = + callArgs match { + case List(arg) => + s"${arg.trim} == null" // TODO check when identifier is nullable and has functions + case _ => throw new IllegalArgumentException("ISNULL requires exactly one argument") + } } case class IsNotNull(identifier: Identifier) extends ConditionalFunction[SQLAny] { @@ -62,13 +93,14 @@ package object cond { override def toSQL(base: String): String = sql - override def painless(): String = s" != null" - override def toPainless(base: String, idx: Int): String = { - if (nullable) - s"(def e$idx = $base; e$idx${painless()})" - else - s"$base${painless()}" - } + override def checkIfNullable: Boolean = false + + override def toPainlessCall(callArgs: List[String], context: Option[PainlessContext]): String = + callArgs match { + case List(arg) => + s"${arg.trim} != null" // TODO check when identifier is nullable and has functions + case _ => throw new IllegalArgumentException("ISNOTNULL requires exactly one argument") + } } case class Coalesce(values: List[PainlessScript]) @@ -80,7 +112,8 @@ package object cond { override def args: List[PainlessScript] = values - override def outputType: SQLType = SQLTypeUtils.leastCommonSuperType(args.map(_.baseType)) + override def outputType: SQLType = + baseType //SQLTypeUtils.leastCommonSuperType(args.map(_.baseType)) override def identifier: Identifier = Identifier() @@ -89,33 +122,26 @@ package object cond { override def sql: String = s"$Coalesce(${values.map(_.sql).mkString(", ")})" // Reprend l’idée de SQLValues mais pour n’importe quel token - override def baseType: SQLType = - SQLTypeUtils.leastCommonSuperType(values.map(_.baseType).distinct) - - override def applyType(in: SQLType): SQLType = out + override def baseType: SQLType = SQLTypeUtils.leastCommonSuperType(argTypes) override def validate(): Either[String, Unit] = { if (values.isEmpty) Left("COALESCE requires at least one argument") else Right(()) } - override def toPainless(base: String, idx: Int): String = s"$base${painless()}" - - override def painless(): String = { - require(values.nonEmpty, "COALESCE requires at least one argument") + override def checkIfNullable: Boolean = false - val checks = values - .take(values.length - 1) - .zipWithIndex - .map { case (v, index) => - var check = s"def v$index = ${SQLTypeUtils.coerce(v, out)};" - check += s"if (v$index != null) return v$index;" - check - } - .mkString(" ") - // final fallback - s"{ $checks return ${SQLTypeUtils.coerce(values.last, out)}; }" - } + override def toPainlessCall(callArgs: List[String], context: Option[PainlessContext]): String = + callArgs match { + case Nil => throw new IllegalArgumentException("COALESCE requires at least one argument") + case _ => + callArgs + .take(values.length - 1) + .map { arg => + s"${arg.trim} != null ? ${arg.trim}" // TODO check when value is nullable and has functions + } + .mkString(" : ") + s" : ${callArgs.last}" + } override def nullable: Boolean = values.forall(_.nullable) } @@ -130,16 +156,49 @@ package object cond { override def inputType: SQLAny = SQLTypes.Any - override def baseType: SQLType = expr1.out + override def baseType: SQLType = SQLTypeUtils.leastCommonSuperType(argTypes) - override def applyType(in: SQLType): SQLType = out + private[this] def checkIfExpressionNullable(expr: PainlessScript): Boolean = expr match { + case f: FunctionChain if f.functions.nonEmpty => true + case _ => false + } - override def toPainlessCall(callArgs: List[String]): String = { + override def checkIfNullable: Boolean = + false //checkIfExpressionNullable(expr1) || checkIfExpressionNullable(expr2) + + override def toPainlessCall( + callArgs: List[String], + context: Option[PainlessContext] + ): String = { callArgs match { - case List(arg0, arg1) => s"${arg0.trim} == ${arg1.trim} ? null : $arg0" + case List(arg0, arg1) => + val expr = + out match { + case SQLTypes.Varchar => + s"$arg0 == null || $arg0.compareTo($arg1) == 0 ? null : $arg0" + case _: SQLTemporal => s"$arg0 == null || $arg0.isEqual($arg1) ? null : $arg0" + case _ => s"$arg0 == $arg1 ? null : $arg0" + } + context match { + case Some(ctx) => + ctx.addParam(LiteralParam(expr)) match { + case Some(e) => return e + case _ => + } + case _ => + } + expr case _ => throw new IllegalArgumentException("NULLIF requires exactly two arguments") } } + + override def validate(): Either[String, Unit] = { + for { + _ <- expr1.validate() + _ <- expr2.validate() + _ <- Validator.validateTypesMatching(expr1.out, expr2.out) + } yield () + } } case class Case( @@ -147,7 +206,9 @@ package object cond { conditions: List[(PainlessScript, PainlessScript)], default: Option[PainlessScript] ) extends TransformFunction[SQLAny, SQLAny] { - override def args: List[PainlessScript] = List.empty + override def args: List[PainlessScript] = expression.toList ++ + conditions.map { case (_, res) => res } ++ + default.toList override def inputType: SQLAny = SQLTypes.Any override def outputType: SQLAny = SQLTypes.Any @@ -161,12 +222,7 @@ package object cond { s"$exprPart $whenThen$elsePart $END" } - override def baseType: SQLType = - SQLTypeUtils.leastCommonSuperType( - conditions.map(_._2.baseType) ++ default.map(_.baseType).toList - ) - - override def applyType(in: SQLType): SQLType = baseType + override def baseType: SQLType = SQLTypeUtils.leastCommonSuperType(argTypes) override def validate(): Either[String, Unit] = { if (conditions.isEmpty) Left("CASE WHEN requires at least one condition") @@ -183,64 +239,126 @@ package object cond { else Right(()) } - override def painless(): String = { - val base = - expression match { - case Some(expr) => - s"def expr = ${SQLTypeUtils.coerce(expr, expr.out)}; " - case _ => "" - } - val cases = conditions.zipWithIndex - .map { case ((cond, res), idx) => - val name = - cond match { - case e: Expression => - e.identifier.name - case i: Identifier => - i.name - case _ => "" - } - expression match { - case Some(expr) => - val c = SQLTypeUtils.coerce(cond, expr.out) - if (cond.sql == res.sql) { - s"def val$idx = $c; if (expr == val$idx) return val$idx;" - } else { - res match { - case i: Identifier if i.name == name && cond.isInstanceOf[Identifier] => - i.withNullable(false) - if (cond.asInstanceOf[Identifier].functions.isEmpty) - s"def val$idx = $c; if (expr == val$idx) return ${SQLTypeUtils.coerce(i.toPainless(s"val$idx"), i.baseType, out, nullable = false)};" - else { - cond.asInstanceOf[Identifier].withNullable(false) - s"def e$idx = ${i.checkNotNull}; def val$idx = e$idx != null ? ${SQLTypeUtils - .coerce(cond.asInstanceOf[Identifier].toPainless(s"e$idx"), cond.baseType, out, nullable = false)} : null; if (expr == val$idx) return ${SQLTypeUtils - .coerce(i.toPainless(s"e$idx"), i.baseType, out, nullable = false)};" + private[this] def checkCase(e: String, c: String, v: String): String = { + out match { + case SQLTypes.Varchar => + s"$e != null && $e.compareTo($c) == 0 ? $v" + case _: SQLTemporal => + s"$e != null && $e.isEqual($c) ? $v" + case _ => s"$e == $c ? $v" + } + } + + override def painless(context: Option[PainlessContext] = None): String = { + context match { + case Some(ctx) => + var cases = + expression match { + case Some(expr) => // case with expression to evaluate + val e = SQLTypeUtils.coerce(expr, out, context) + val expParam = ctx.addParam( + LiteralParam(e) + ) + conditions + .map { case (cond, res) => + val name = + cond match { + case e: Expression => + e.identifier.name + case f: FunctionWithIdentifier => + f.identifier.name + case i: Identifier => + i.name + case _ => "" + } + val c = SQLTypeUtils.coerce(cond, out, context) + val r = + res match { + case i: Identifier if i.name == name && name.nonEmpty => + i.withNullable(false) + SQLTypeUtils.coerce( + i, + out, + context + ) + case _ => + SQLTypeUtils.coerce(res, out, context) + } + expParam match { + case Some(e) => + if (cond.nullable) { + ctx.addParam(LiteralParam(c)) match { + case Some(c) => checkCase(e, c, r) + case _ => checkCase(e, c, r) + } + } else { + checkCase(e, c, r) + } + case _ => + checkCase(e, c, r) + } + } + .mkString(" : ") + case _ => + conditions + .map { case (cond, res) => + val name = + cond match { + case e: Expression => + e.identifier.name + case f: FunctionWithIdentifier => + f.identifier.name + case i: Identifier => + i.name + case _ => "" + } + val c = SQLTypeUtils.coerce(cond, SQLTypes.Boolean, context) + val r = + res match { + case i: Identifier if i.name == name && name.nonEmpty => + i.withNullable(false) + SQLTypeUtils.coerce( + i, + out, + context + ) + case _ => + SQLTypeUtils.coerce(res, out, context) + } + if (!cond.isInstanceOf[CriteriaWithConditionalFunction[_]] && cond.nullable) { + ctx.addParam(LiteralParam(c)) match { + case Some(c) => s"$c ? $r" + case _ => s"$c ? $r" + } + } else { + s"$c ? $r" } - case _ => - s"if (expr == $c) return ${SQLTypeUtils.coerce(res, out)};" + } + .mkString(" : ") + } + default match { + case Some(df) => + val d = SQLTypeUtils.coerce(df, out, context) + if (df.nullable) { + ctx.addParam(LiteralParam(d)) match { + case Some(d) => cases = s"$cases : $d" + case _ => cases = s"$cases : $d" } + } else { + cases = s"$cases : $d" } - case None => - val c = SQLTypeUtils.coerce(cond, SQLTypes.Boolean) - val r = - res match { - case i: Identifier if i.name == name && cond.isInstanceOf[Expression] => - i.withNullable(false) - SQLTypeUtils.coerce(i.toPainless("left"), i.baseType, out, nullable = false) - case _ => SQLTypeUtils.coerce(res, out) - } - s"if ($c) return $r;" + case _ => } - } - .mkString(" ") - val defaultCase = default - .map(d => s"def dval = ${SQLTypeUtils.coerce(d, out)}; return dval;") - .getOrElse("return null;") - s"{ $base$cases $defaultCase }" + + cases + + case _ => + throw new IllegalArgumentException(s"Painless context is required for $sql") + } } - override def toPainless(base: String, idx: Int): String = s"$base${painless()}" + override def toPainless(base: String, idx: Int, context: Option[PainlessContext]): String = + s"$base${painless(context)}" override def nullable: Boolean = conditions.exists { case (_, res) => res.nullable } || default.forall(_.nullable) diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/function/convert/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/function/convert/package.scala index 0f74f7b9..8a71bcf4 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/function/convert/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/function/convert/package.scala @@ -1,7 +1,31 @@ +/* + * 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.function -import app.softnetwork.elastic.sql.{Alias, DateMathRounding, Expr, PainlessScript, TokenRegex} -import app.softnetwork.elastic.sql.`type`.{SQLType, SQLTypeUtils} +import app.softnetwork.elastic.sql.{ + Alias, + DateMathRounding, + Expr, + Identifier, + PainlessContext, + PainlessScript, + TokenRegex +} +import app.softnetwork.elastic.sql.`type`.{SQLType, SQLTypeUtils, SQLTypes} package object convert { @@ -19,12 +43,36 @@ package object convert { //override def nullable: Boolean = value.nullable - override def painless(): String = SQLTypeUtils.coerce(value, targetType) - - override def toPainless(base: String, idx: Int): String = { - val ret = SQLTypeUtils.coerce(base, value.baseType, targetType, value.nullable) + override def painless(context: Option[PainlessContext] = None): String = + SQLTypeUtils.coerce(value, targetType, context) + + override def toPainless(base: String, idx: Int, context: Option[PainlessContext]): String = { + context match { + case Some(ctx) => + value match { + case _: Identifier => + inputType match { + case SQLTypes.Any => + ctx.find(base) match { + case Some(identifier) => + outputType match { + case SQLTypes.Date => + identifier.addPainlessMethod(".toLocalDate()") + case SQLTypes.Time => + identifier.addPainlessMethod(".toLocalTime()") + case _ => // do nothing + } + case _ => // do nothing + } + case _ => // do nothing + } + case _ => // do nothing + } + case _ => // do nothing + } + val ret = SQLTypeUtils.coerce(base, value.baseType, targetType, value.nullable, context) val bloc = ret.startsWith("{") && ret.endsWith("}") - val retWithBrackets = if (bloc) ret else s"{ return $ret; }" + val retWithBrackets = if (bloc) ret else s"{ $ret }" if (safe) s"try $retWithBrackets catch (Exception e) { return null; }" else ret } diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/function/geo/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/function/geo/package.scala index a574ff0c..4f2d46fb 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/function/geo/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/function/geo/package.scala @@ -1,3 +1,19 @@ +/* + * 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.function import app.softnetwork.elastic.sql.`type`.{SQLAny, SQLDouble, SQLTypes} @@ -5,6 +21,7 @@ import app.softnetwork.elastic.sql.{ DoubleValue, Expr, Identifier, + PainlessContext, PainlessParams, PainlessScript, Token, @@ -57,7 +74,7 @@ package object geo { case object Distance extends Expr("ST_DISTANCE") with Function with Operator { override def words: List[String] = List(sql, "DISTANCE") - override def painless(): String = ".arcDistance" + override def painless(context: Option[PainlessContext] = None): String = ".arcDistance" def haversine(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double = { val R = 6371e3 // Radius of the earth in meters @@ -125,7 +142,7 @@ package object geo { else Map.empty - override def painless(): String = { + override def painless(context: Option[PainlessContext]): String = { val nullCheck = identifiers.zipWithIndex .map { case (_, i) => s"arg$i == null" } @@ -141,7 +158,7 @@ package object geo { val ret = if (oneIdentifier) { - s"arg0${fun.map(_.painless()).getOrElse("")}(params.lat, params.lon)" + s"arg0${fun.map(_.painless(context)).getOrElse("")}(params.lat, params.lon)" } else if (identifiers.isEmpty) { s"${Distance.haversine( fromPoint.get.lat.value, @@ -150,7 +167,7 @@ package object geo { toPoint.get.lon.value )}" } else { - s"arg0${fun.map(_.painless()).getOrElse("")}(arg1.lat, arg1.lon)" + s"arg0${fun.map(_.painless(context)).getOrElse("")}(arg1.lat, arg1.lon)" } if (identifiers.nonEmpty) diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/function/math/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/function/math/package.scala index 0e49d227..79752d8a 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/function/math/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/function/math/package.scala @@ -1,12 +1,37 @@ +/* + * 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.function -import app.softnetwork.elastic.sql.{Expr, Identifier, IntValue, PainlessScript, TokenRegex} +import app.softnetwork.elastic.sql.{ + Expr, + Identifier, + IntValue, + PainlessContext, + PainlessParam, + PainlessScript, + TokenRegex +} import app.softnetwork.elastic.sql.`type`.{SQLNumeric, SQLType, SQLTypes} package object math { sealed trait MathOp extends PainlessScript with TokenRegex { - override def painless(): String = s"Math.${sql.toLowerCase()}" + override def painless(context: Option[PainlessContext] = None): String = + s"Math.${sql.toLowerCase()}" override def toString: String = s" $sql " override def baseType: SQLNumeric = SQLTypes.Numeric @@ -72,15 +97,37 @@ package object math { override def nullable: Boolean = arg.nullable } + case class PowParam(scale: Int) extends PainlessParam with PainlessScript { + override def param: String = s"Math.pow(10, $scale)" + override def checkNotNull: String = "" + override def sql: String = param + override def nullable: Boolean = true + + /** Generate painless script for this token + * + * @param context + * the painless context + * @return + * the painless script + */ + override def painless(context: Option[PainlessContext]): String = param + } + case class Round(arg: PainlessScript, scale: Option[Int]) extends MathematicalFunction { override def mathOp: MathOp = Round - override def args: List[PainlessScript] = - List(arg) ++ scale.map(IntValue(_)).toList + override def args: List[PainlessScript] = List(arg, PowParam(scale.getOrElse(0))) + + override def toPainlessCall(callArgs: List[String], context: Option[PainlessContext]): String = + callArgs match { + case List(a, p) => s"${mathOp.painless(context)}(($a * $p) / $p)" + case _ => throw new IllegalArgumentException("Round function requires exactly one argument") + } + + override def sql: String = { + s"${fun.map(_.sql).getOrElse("")}($arg${scale.map(s => s", $s").getOrElse("")})" + } - override def toPainlessCall(callArgs: List[String]): String = - s"(def p = ${Pow(IntValue(10), scale.getOrElse(0)) - .painless()}; ${mathOp.painless()}((${callArgs.head} * p) / p))" } case class Sign(arg: PainlessScript) extends MathematicalFunction { @@ -88,13 +135,12 @@ package object math { override def args: List[PainlessScript] = List(arg) - override def painless(): String = { - val ret = "arg0 > 0 ? 1 : (arg0 < 0 ? -1 : 0)" - if (arg.nullable) - s"(def arg0 = ${arg.painless()}; arg0 != null ? ($ret) : null)" - else - s"(def arg0 = ${arg.painless()}; $ret)" - } + override def toPainlessCall(callArgs: List[String], context: Option[PainlessContext]): String = + callArgs match { + case List(a) => s"($a > 0 ? 1 : ($a < 0 ? -1 : 0))" + case _ => throw new IllegalArgumentException("Sign function requires exactly one argument") + } + } case class Atan2(y: PainlessScript, x: PainlessScript) extends MathematicalFunction { diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/function/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/function/package.scala index ea2d9aee..972b9989 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/function/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/function/package.scala @@ -1,6 +1,22 @@ +/* + * 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 -import app.softnetwork.elastic.sql.`type`.{SQLType, SQLTypeUtils} +import app.softnetwork.elastic.sql.`type`.{SQLType, SQLTypeUtils, SQLTypes} import app.softnetwork.elastic.sql.function.aggregate.AggregateFunction import app.softnetwork.elastic.sql.operator.math.ArithmeticExpression import app.softnetwork.elastic.sql.parser.Validator @@ -100,6 +116,18 @@ package object function { this.baseType } } + + def find(function: Function): Option[Function] = { + functions.find(_ == function) + } + + def contains(function: Function): Boolean = { + functions.contains(function) + } + + def indexOf(function: Function): Int = { + functions.indexOf(function) + } } trait FunctionN[In <: SQLType, Out <: SQLType] extends Function with PainlessScript { @@ -117,47 +145,116 @@ package object function { override def in: SQLType = inputType override def baseType: SQLType = outputType - override def applyType(in: SQLType): SQLType = outputType + override def applyType(in: SQLType): SQLType = baseType override def sql: String = s"${fun.map(_.sql).getOrElse("")}(${args.map(_.sql).mkString(argsSeparator)})" override def toSQL(base: String): String = s"$base$sql" - override def painless(): String = { + def checkIfNullable: Boolean = args.exists(_.nullable) + + override def painless(context: Option[PainlessContext]): String = { + context match { + case Some(ctx) => + args.foreach(arg => ctx.addParam(arg)) // ensure all args are added to the context + case _ => + } + val nullCheck = - args.zipWithIndex - .filter(_._1.nullable) - .map { case (_, i) => s"arg$i == null" } - .mkString(" || ") + if (checkIfNullable) { + args.zipWithIndex + .filter(_._1.nullable) + .map { case (a, i) => + context.flatMap(ctx => ctx.get(a)).getOrElse(s"arg$i") + " == null" + } + .mkString(" || ") + } else + "" val assignments = args.zipWithIndex .filter(_._1.nullable) .map { case (a, i) => - s"def arg$i = ${SQLTypeUtils.coerce(a.painless(), a.baseType, argTypes(i), nullable = false)};" + context + .flatMap(ctx => ctx.get(a).map(_ => "")) + .getOrElse( + s"def arg$i = ${SQLTypeUtils + .coerce(a.painless(context), a.baseType, argTypes(i), nullable = false, context)};" + ) } .mkString(" ") + .trim val callArgs = args.zipWithIndex .map { case (a, i) => - if (a.nullable) - s"arg$i" - else - SQLTypeUtils.coerce(a.painless(), a.baseType, argTypes(i), nullable = false) + (context match { + case Some(ctx) => + ctx.get(a) match { + case Some(paramName) => + a match { + case chain: FunctionChain if chain.functions.nonEmpty => + val ret = SQLTypeUtils + .coerce( + a, + argTypes(i), + context + ) + if (ret.startsWith(".")) { + // apply methods + ctx.find(paramName) match { + case Some(p) => + p.addPainlessMethod(ret) + case _ => + } + Option(paramName) + } else if (ret == paramName) + Option(paramName) + else { + ctx.addParam(LiteralParam(ret)) + } + case identifier: Identifier => + identifier.baseType match { + case SQLTypes.Any => // in painless context, Any is ZonedDateTime + out match { + case SQLTypes.Date => + identifier.addPainlessMethod(".toLocalDate()") + case SQLTypes.Time => + identifier.addPainlessMethod(".toLocalTime()") + case _ => + } + case _ => + } + Option(paramName) + case _ => + Option(paramName) + } + case _ => None + } + case _ => None + }).getOrElse { + if (a.nullable) s"arg$i" + else + SQLTypeUtils + .coerce(a.painless(context), a.baseType, argTypes(i), nullable = false, context) + } } - if (args.exists(_.nullable)) - s"($assignments ($nullCheck) ? null : ${toPainlessCall(callArgs)})" - else - s"${toPainlessCall(callArgs)}" + val painlessCall = toPainlessCall(callArgs, context) + + if (checkIfNullable) { + if (assignments.nonEmpty) + s"$assignments ($nullCheck) ? $nullValue : $painlessCall" + else s"($nullCheck) ? $nullValue : $painlessCall" + } else + s"$painlessCall" } - def toPainlessCall(callArgs: List[String]): String = + def toPainlessCall(callArgs: List[String], context: Option[PainlessContext]): String = if (callArgs.nonEmpty) - s"${fun.map(_.painless()).getOrElse("")}(${callArgs.mkString(argsSeparator)})" + s"${fun.map(_.painless(context)).getOrElse("")}(${callArgs.mkString(argsSeparator)})" else - fun.map(_.painless()).getOrElse("") + fun.map(_.painless(context)).getOrElse("") } trait BinaryFunction[In1 <: SQLType, In2 <: SQLType, Out <: SQLType] extends FunctionN[In2, Out] { @@ -172,11 +269,37 @@ package object function { } trait TransformFunction[In <: SQLType, Out <: SQLType] extends FunctionN[In, Out] { - def toPainless(base: String, idx: Int): String = { - if (nullable && base.nonEmpty) - s"(def e$idx = $base; e$idx != null ? e$idx${painless()} : null)" - else - s"$base${painless()}" + override def checkIfNullable: Boolean = + super.checkIfNullable && (this match { + case f: FunctionWithIdentifier + if f.identifier.functions.size > 1 && f.identifier.functions.reverse.headOption.exists( + !_.equals(this) + ) => + false + case _ => + true + }) + + def toPainless(base: String, idx: Int, context: Option[PainlessContext]): String = { + context match { + case Some(ctx) => + val p = painless(context) + if (p.startsWith(".") && base.nonEmpty) { // method call + ctx.find(base) match { + case Some(param) => + param.addPainlessMethod(p) + base + case _ => + s"$base$p" + } + } else + p + case None => + if (checkIfNullable && base.nonEmpty) + s"(def e$idx = $base; e$idx != null ? e$idx${painless(context)} : null)" + else + s"$base${painless(context)}" + } } } diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/function/string/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/function/string/package.scala index 5e985fe7..4629692c 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/function/string/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/function/string/package.scala @@ -1,6 +1,29 @@ +/* + * 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.function -import app.softnetwork.elastic.sql.{Expr, Identifier, IntValue, PainlessScript, TokenRegex} +import app.softnetwork.elastic.sql.{ + Expr, + Identifier, + IntValue, + PainlessContext, + PainlessScript, + TokenRegex +} import app.softnetwork.elastic.sql.`type`.{ SQLBigInt, SQLBool, @@ -13,30 +36,36 @@ import app.softnetwork.elastic.sql.`type`.{ package object string { sealed trait StringOp extends PainlessScript with TokenRegex { - override def painless(): String = s".${sql.toLowerCase()}()" + override def painless(context: Option[PainlessContext]): String = s".${sql.toLowerCase()}()" } case object Concat extends Expr("CONCAT") with StringOp { - override def painless(): String = " + " + override def painless(context: Option[PainlessContext]): String = " + " } case object Pipe extends Expr("\\|\\|") with StringOp { - override def painless(): String = " + " + override def painless(context: Option[PainlessContext]): String = " + " } case object Lower extends Expr("LOWER") with StringOp { override lazy val words: List[String] = List(sql, "LCASE") + + override def painless(context: Option[PainlessContext]): String = s".toLowerCase()" } case object Upper extends Expr("UPPER") with StringOp { override lazy val words: List[String] = List(sql, "UCASE") + + override def painless(context: Option[PainlessContext]): String = s".toUpperCase()" } case object Trim extends Expr("TRIM") with StringOp case object Ltrim extends Expr("LTRIM") with StringOp { - override def painless(): String = ".replaceAll(\"^\\\\s+\",\"\")" + override def painless(context: Option[PainlessContext]): String = + ".replaceAll(\"^\\\\s+\",\"\")" } case object Rtrim extends Expr("RTRIM") with StringOp { - override def painless(): String = ".replaceAll(\"\\\\s+$\",\"\")" + override def painless(context: Option[PainlessContext]): String = + ".replaceAll(\"\\\\s+$\",\"\")" } case object Substring extends Expr("SUBSTRING") with StringOp { - override def painless(): String = ".substring" + override def painless(context: Option[PainlessContext]): String = ".substring" override lazy val words: List[String] = List(sql, "SUBSTR") } case object LeftOp extends Expr("LEFT") with StringOp @@ -47,22 +76,22 @@ package object string { } case object Replace extends Expr("REPLACE") with StringOp { override lazy val words: List[String] = List(sql, "STR_REPLACE") - override def painless(): String = ".replace" + override def painless(context: Option[PainlessContext]): String = ".replace" } case object Reverse extends Expr("REVERSE") with StringOp case object Position extends Expr("POSITION") with StringOp { override lazy val words: List[String] = List(sql, "STRPOS") - override def painless(): String = ".indexOf" + override def painless(context: Option[PainlessContext]): String = ".indexOf" } case object RegexpLike extends Expr("REGEXP_LIKE") with StringOp { override lazy val words: List[String] = List(sql, "REGEXP") - override def painless(): String = ".matches" + override def painless(context: Option[PainlessContext]): String = ".matches" } case class MatchFlags(flags: String) extends PainlessScript { override def sql: String = s"'$flags'" - override def painless(): String = flags.toCharArray + override def painless(context: Option[PainlessContext]): String = flags.toCharArray .map { case 'i' => "java.util.regex.Pattern.CASE_INSENSITIVE" case 'c' => "0" @@ -88,22 +117,33 @@ package object string { def stringOp: StringOp - override def fun: Option[PainlessScript] = Some(stringOp) - override def identifier: Identifier = Identifier(this) - override def toSQL(base: String): String = s"$sql($base)" + override def toSQL(base: String): String = + if (base.nonEmpty) s"$sql($base)" + else sql override def sql: String = if (args.isEmpty) s"${fun.map(_.sql).getOrElse("")}" else - super.sql + s"$stringOp(${args.map(_.sql).mkString(argsSeparator)})" + + override def toPainlessCall( + callArgs: List[String], + context: Option[PainlessContext] + ): String = { + callArgs match { + case List(str) => s"$str${stringOp.painless(context)}" + case _ => throw new IllegalArgumentException(s"${stringOp.sql} requires 1 argument") + } + } } - case class StringFunctionWithOp(stringOp: StringOp) extends StringFunction[SQLVarchar] { + case class StringFunctionWithOp(str: PainlessScript, stringOp: StringOp) + extends StringFunction[SQLVarchar] { override def outputType: SQLVarchar = SQLTypes.Varchar - override def args: List[PainlessScript] = List.empty + override def args: List[PainlessScript] = List(str) } case class Substring(str: PainlessScript, start: Int, length: Option[Int]) @@ -116,15 +156,18 @@ package object string { override def nullable: Boolean = str.nullable - override def toPainlessCall(callArgs: List[String]): String = { + override def toPainlessCall( + callArgs: List[String], + context: Option[PainlessContext] + ): String = { callArgs match { // SUBSTRING(expr, start, length) - case List(arg0, arg1, arg2) => - s"$arg0.substring($arg1 - 1, Math.min($arg1 - 1 + $arg2, $arg0.length()))" + case List(arg0, _, _) => + s"$arg0.substring(${start - 1}, Math.min(${start - 1 + length.get}, $arg0.length()))" // SUBSTRING(expr, start) case List(arg0, arg1) => - s"$arg0.substring(Math.min($arg1 - 1, $arg0.length() - 1))" + s"$arg0.substring(Math.min(${start - 1}, $arg0.length() - 1))" case _ => throw new IllegalArgumentException("SUBSTRING requires 2 or 3 arguments") } @@ -150,15 +193,24 @@ package object string { override def nullable: Boolean = values.exists(_.nullable) - override def toPainlessCall(callArgs: List[String]): String = { + override def toPainlessCall( + callArgs: List[String], + context: Option[PainlessContext] + ): String = { if (callArgs.isEmpty) throw new IllegalArgumentException("CONCAT requires at least one argument") else callArgs.zipWithIndex .map { case (arg, idx) => - SQLTypeUtils.coerce(arg, values(idx).baseType, SQLTypes.Varchar, nullable = false) + SQLTypeUtils.coerce( + arg, + values(idx).baseType, + SQLTypes.Varchar, + nullable = false, + context + ) } - .mkString(stringOp.painless()) + .mkString(stringOp.painless(context)) } override def validate(): Either[String, Unit] = @@ -172,10 +224,10 @@ package object string { override def toSQL(base: String): String = sql } - case class Length() extends StringFunction[SQLBigInt] { + case class Length(str: PainlessScript) extends StringFunction[SQLBigInt] { override def outputType: SQLBigInt = SQLTypes.BigInt override def stringOp: StringOp = Length - override def args: List[PainlessScript] = List.empty + override def args: List[PainlessScript] = List(str) } case class LeftFunction(str: PainlessScript, length: Int) extends StringFunction[SQLVarchar] { @@ -186,7 +238,10 @@ package object string { override def nullable: Boolean = str.nullable - override def toPainlessCall(callArgs: List[String]): String = { + override def toPainlessCall( + callArgs: List[String], + context: Option[PainlessContext] + ): String = { callArgs match { case List(arg0, arg1) => s"$arg0.substring(0, Math.min($arg1, $arg0.length()))" @@ -211,10 +266,14 @@ package object string { override def nullable: Boolean = str.nullable - override def toPainlessCall(callArgs: List[String]): String = { + override def toPainlessCall( + callArgs: List[String], + context: Option[PainlessContext] + ): String = { callArgs match { case List(arg0, arg1) => - s"""$arg1 == 0 ? "" : $arg0.substring($arg0.length() - Math.min($arg1, $arg0.length()))""" + if (length == 0) "" + else s"""$arg0.substring($arg0.length() - Math.min($arg1, $arg0.length()))""" case _ => throw new IllegalArgumentException("RIGHT requires 2 arguments") } } @@ -237,7 +296,10 @@ package object string { override def nullable: Boolean = str.nullable || search.nullable || replace.nullable - override def toPainlessCall(callArgs: List[String]): String = { + override def toPainlessCall( + callArgs: List[String], + context: Option[PainlessContext] + ): String = { callArgs match { case List(arg0, arg1, arg2) => s"$arg0.replace($arg1, $arg2)" @@ -262,7 +324,10 @@ package object string { override def nullable: Boolean = str.nullable - override def toPainlessCall(callArgs: List[String]): String = { + override def toPainlessCall( + callArgs: List[String], + context: Option[PainlessContext] + ): String = { callArgs match { case List(arg0) => s"new StringBuilder($arg0).reverse().toString()" case _ => throw new IllegalArgumentException("REVERSE requires 1 argument") @@ -284,9 +349,12 @@ package object string { override def nullable: Boolean = substr.nullable || str.nullable - override def toPainlessCall(callArgs: List[String]): String = { + override def toPainlessCall( + callArgs: List[String], + context: Option[PainlessContext] + ): String = { callArgs match { - case List(arg0, arg1, arg2) => s"$arg1.indexOf($arg0, $arg2 - 1) + 1" + case List(arg0, arg1, _) => s"$arg1.indexOf($arg0, ${start - 1}) + 1" case _ => throw new IllegalArgumentException("POSITION requires 3 arguments") } } @@ -317,7 +385,10 @@ package object string { override def nullable: Boolean = str.nullable || pattern.nullable - override def toPainlessCall(callArgs: List[String]): String = { + override def toPainlessCall( + callArgs: List[String], + context: Option[PainlessContext] + ): String = { callArgs match { case List(arg0, arg1) => s"java.util.regex.Pattern.compile($arg1).matcher($arg0).find()" case List(arg0, arg1, arg2) => diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/function/time/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/function/time/package.scala index aaf30401..72da4a77 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/function/time/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/function/time/package.scala @@ -1,3 +1,19 @@ +/* + * 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.function import app.softnetwork.elastic.sql.{ @@ -5,6 +21,9 @@ import app.softnetwork.elastic.sql.{ DateMathScript, Expr, Identifier, + LiteralParam, + PainlessContext, + PainlessParam, PainlessScript, StringValue, TokenRegex @@ -43,13 +62,11 @@ package object time { case _ => None } - private[this] var _out: SQLType = outputType - - override def out: SQLType = _out - override def applyType(in: SQLType): SQLType = { - _out = interval.checkType(in).getOrElse(out) - _out + interval.checkType(in) match { + case Left(_) => baseType + case Right(_) => cast(in) + } } override def validate(): Either[String, Unit] = interval.checkType(out) match { @@ -57,11 +74,28 @@ package object time { case Right(_) => Right(()) } - override def toPainless(base: String, idx: Int): String = - if (nullable) - s"(def e$idx = $base; e$idx != null ? ${SQLTypeUtils.coerce(s"e$idx", expr.baseType, out, nullable = false)}${painless()} : null)" - else - s"${SQLTypeUtils.coerce(base, expr.baseType, out, nullable = expr.nullable)}${painless()}" + override def toPainless(base: String, idx: Int, context: Option[PainlessContext]): String = { + context match { + case Some(ctx) => + ctx.last match { + case Some(p) => + ctx.find(p) match { + case Some(param) => + param.addPainlessMethod(painless(context)) + return p + case _ => + return s"($p != null ? ${SQLTypeUtils.coerce(base, expr.baseType, out, nullable = false, context)}${painless(context)} : null)" + } + case _ => + } + case _ => + } + if (nullable) { + // ensure unique variable names + s"(def e$idx = $base; e$idx != null ? ${SQLTypeUtils.coerce(s"e$idx", expr.baseType, out, nullable = false, context)}${painless(context)} : null)" + } else + s"${SQLTypeUtils.coerce(base, expr.baseType, out, nullable = expr.nullable, context)}${painless(context)}" + } } sealed trait AddInterval[IO <: SQLTemporal] extends IntervalFunction[IO] { @@ -101,21 +135,33 @@ package object time { sealed trait CurrentFunction extends SystemFunction with PainlessScript with DateMathScript { override def script: Option[String] = Some("now") + + def param: String + + override def painless(context: Option[PainlessContext]): String = { + context match { + case Some(ctx) => + ctx.addParam(LiteralParam(param)) match { + case Some(p) => + return SQLTypeUtils.coerce(p, this.baseType, this.out, nullable = false, context) + case _ => + } + case _ => + } + SQLTypeUtils.coerce(param, this.baseType, this.out, nullable = false, context) + } } sealed trait CurrentDateTimeFunction extends DateTimeFunction with CurrentFunction { - override def painless(): String = - SQLTypeUtils.coerce(now, this.baseType, this.out, nullable = false) + override def param: String = now } sealed trait CurrentDateFunction extends DateFunction with CurrentFunction { - override def painless(): String = - SQLTypeUtils.coerce(s"$now.toLocalDate()", this.baseType, this.out, nullable = false) + override def param: String = s"$now.toLocalDate()" } sealed trait CurrentTimeFunction extends TimeFunction with CurrentFunction { - override def painless(): String = - SQLTypeUtils.coerce(s"$now.toLocalTime()", this.baseType, this.out, nullable = false) + override def param: String = s"$now.toLocalTime()" } case object CurrentDate extends Expr("CURRENT_DATE") with TokenRegex { @@ -161,7 +207,7 @@ package object time { } case object DateTrunc extends Expr("DATE_TRUNC") with TokenRegex with PainlessScript { - override def painless(): String = ".truncatedTo" + override def painless(context: Option[PainlessContext]): String = ".truncatedTo" override lazy val words: List[String] = List(sql, "DATETRUNC") } @@ -169,7 +215,7 @@ package object time { extends DateTimeFunction with TransformFunction[SQLTemporal, SQLTemporal] with FunctionWithIdentifier - with DateMathRounding { + with DateMathRounding { // FIXME check Unit compatibility with inputType override def fun: Option[PainlessScript] = Some(DateTrunc) override def args: List[PainlessScript] = List(unit) @@ -185,10 +231,48 @@ package object time { override def roundingScript: Option[String] = unit.roundingScript override def dateMathScript: Boolean = identifier.dateMathScript + + override def toPainlessCall( + callArgs: List[String], + context: Option[PainlessContext] + ): String = { + unit match { + case TimeUnit.YEARS => ".withDayOfYear(1).truncatedTo(ChronoUnit.DAYS)" + case TimeUnit.MONTHS => ".withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS)" + case TimeUnit.WEEKS => ".with(DayOfWeek.SUNDAY).truncatedTo(ChronoUnit.DAYS)" + case TimeUnit.QUARTERS => + context match { + case Some(ctx) => + ctx.addParam(identifier) match { + case Some(p) => + val quarter = + s"$p.withMonth(((($p.getMonthValue() - 1) / 3) * 3) + 1).withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS)" + val quarterExpr = + if (identifier.nullable) { + s"$p != null ? $quarter : null" + } else { + quarter + } + ctx.addParam( + LiteralParam( + quarterExpr + ) + ) match { + case Some(p) => return p + case _ => + } + case _ => + } + case _ => + } + super.toPainlessCall(callArgs, context) + case _ => super.toPainlessCall(callArgs, context) + } + } } case object Extract extends Expr("EXTRACT") with TokenRegex with PainlessScript { - override def painless(): String = ".get" + override def painless(context: Option[PainlessContext]): String = ".get" } case class Extract(field: TimeField) @@ -221,7 +305,23 @@ package object time { class DayOfMonth extends TimeFieldExtract(DAY_OF_MONTH) - class DayOfWeek extends TimeFieldExtract(DAY_OF_WEEK) + class DayOfWeek(date: Identifier) + extends TimeFieldExtract(DAY_OF_WEEK) + with FunctionWithIdentifier { + override def identifier: Identifier = date + override def args: List[PainlessScript] = List(identifier) + override def toPainlessCall( + callArgs: List[String], + context: Option[PainlessContext] + ): String = { + callArgs match { + case arg :: Nil => + s"($arg.get(${field.painless(context)}) + 6) % 7" + case _ => throw new IllegalArgumentException("DayOfWeek requires exactly one argument") + } + } + + } class DayOfYear extends TimeFieldExtract(DAY_OF_YEAR) @@ -248,7 +348,7 @@ package object time { class WeekOfWeekBasedYear extends TimeFieldExtract(WEEK_OF_WEEK_BASED_YEAR) case object LastDayOfMonth extends Expr("LAST_DAY") with TokenRegex with PainlessScript { - override def painless(): String = ".withDayOfMonth" + override def painless(context: Option[PainlessContext]): String = ".withDayOfMonth" override lazy val words: List[String] = List(sql, "LASTDAY") } @@ -263,25 +363,18 @@ package object time { override def inputType: SQLDate = SQLTypes.Date override def outputType: SQLDate = SQLTypes.Date - override def nullable: Boolean = identifier.nullable - override def sql: String = LastDayOfMonth.sql override def toSQL(base: String): String = { s"$sql($base)" } - override def toPainless(base: String, idx: Int): String = { - val arg = SQLTypeUtils.coerce(base, identifier.baseType, SQLTypes.Date, nullable = false) - if (nullable && base.nonEmpty) - s"(def e$idx = $arg; e$idx != null ? ${toPainlessCall(List(s"e$idx"))} : null)" - else - s"(def e$idx = $arg; ${toPainlessCall(List(s"e$idx"))})" - } - - override def toPainlessCall(callArgs: List[String]): String = { + override def toPainlessCall( + callArgs: List[String], + context: Option[PainlessContext] + ): String = { callArgs match { - case arg :: Nil => s"$arg${LastDayOfMonth.painless()}($arg.lengthOfMonth())" + case arg :: Nil => s"$arg${LastDayOfMonth.painless(context)}($arg.lengthOfMonth())" case _ => throw new IllegalArgumentException("LastDayOfMonth requires exactly one argument") } } @@ -289,7 +382,7 @@ package object time { } case object DateDiff extends Expr("DATE_DIFF") with TokenRegex with PainlessScript { - override def painless(): String = ".between" + override def painless(context: Option[PainlessContext]): String = ".between" override lazy val words: List[String] = List(sql, "DATEDIFF") } @@ -309,8 +402,8 @@ package object time { override def toSQL(base: String): String = s"$sql(${end.sql}, ${start.sql}, ${unit.sql})" - override def toPainlessCall(callArgs: List[String]): String = - s"${unit.painless()}${DateDiff.painless()}(${callArgs.mkString(", ")})" + override def toPainlessCall(callArgs: List[String], context: Option[PainlessContext]): String = + s"${unit.painless(context)}${DateDiff.painless(context)}(${callArgs.mkString(", ")})" } case object DateAdd extends Expr("DATE_ADD") with TokenRegex { @@ -352,6 +445,10 @@ package object time { sealed trait FunctionWithDateTimeFormat { def format: String + def includeTimeZone: Boolean = false + + protected def param: String = s"DateTimeFormatter.ofPattern(\"${convert()}\")" + val sqlToJava: Map[String, String] = Map( "%Y" -> "yyyy", "%y" -> "yy", @@ -380,7 +477,7 @@ package object time { "%X" -> "YYYY" ) - def convert(includeTimeZone: Boolean = false): String = { + def convert(): String = { val basePattern = sqlToJava.foldLeft(format) { case (pattern, (sql, java)) => pattern.replace(sql, java) } @@ -395,7 +492,7 @@ package object time { } case object DateParse extends Expr("DATE_PARSE") with TokenRegex with PainlessScript { - override def painless(): String = ".parse" + override def painless(context: Option[PainlessContext]): String = ".parse" } case class DateParse(identifier: Identifier, format: String) @@ -404,9 +501,9 @@ package object time { with FunctionWithIdentifier with FunctionWithDateTimeFormat with DateMathScript { - override def fun: Option[PainlessScript] = Some(DateParse) + override def fun: Option[PainlessScript] = None - override def args: List[PainlessScript] = List.empty + override def args: List[PainlessScript] = List(identifier) override def inputType: SQLVarchar = SQLTypes.Varchar override def outputType: SQLDate = SQLTypes.Date @@ -416,14 +513,24 @@ package object time { s"$sql($base, '$format')" } - override def painless(): String = throw new NotImplementedError( - "Use toPainless instead" - ) - override def toPainless(base: String, idx: Int): String = - if (nullable) - s"(def e$idx = $base; e$idx != null ? DateTimeFormatter.ofPattern('${convert()}').parse(e$idx, LocalDate::from) : null)" - else - s"DateTimeFormatter.ofPattern('${convert()}').parse($base, LocalDate::from)" + override def toPainlessCall(callArgs: List[String], context: Option[PainlessContext]): String = + callArgs match { + case arg :: Nil => + context match { + case Some(ctx) => + identifier.baseType match { + case SQLTypes.Varchar => + ctx.addParam(LiteralParam(s"LocalDate.parse($arg, $param)")) match { + case Some(p) => return p + case _ => + } + case _ => + } + case _ => + } + s"LocalDate.parse($arg, $param)" + case _ => throw new IllegalArgumentException("DateParse requires exactly one argument") + } override def script: Option[String] = { val base: String = FunctionUtils @@ -442,7 +549,7 @@ package object time { } case object DateFormat extends Expr("DATE_FORMAT") with TokenRegex with PainlessScript { - override def painless(): String = ".format" + override def painless(context: Option[PainlessContext]): String = ".format" } case class DateFormat(identifier: Identifier, format: String) @@ -452,7 +559,7 @@ package object time { with FunctionWithDateTimeFormat { override def fun: Option[PainlessScript] = Some(DateFormat) - override def args: List[PainlessScript] = List.empty + override def args: List[PainlessScript] = List(identifier) override def inputType: SQLDate = SQLTypes.Date override def outputType: SQLVarchar = SQLTypes.Varchar @@ -462,14 +569,29 @@ package object time { s"$sql($base, '$format')" } - override def painless(): String = throw new NotImplementedError( - "Use toPainless instead" - ) - override def toPainless(base: String, idx: Int): String = - if (nullable) - s"(def e$idx = $base; e$idx != null ? DateTimeFormatter.ofPattern('${convert()}').format(e$idx) : null)" - else - s"DateTimeFormatter.ofPattern('${convert()}').format($base)" + override def toPainlessCall(callArgs: List[String], context: Option[PainlessContext]): String = + callArgs match { + case arg :: Nil => + context match { + case Some(ctx) => + identifier.baseType match { + case SQLTypes.Varchar => + ctx.addParam(LiteralParam(s"$param.format($arg)")) match { + case Some(p) => return p + case _ => + } + case _ => + ctx.addParam(LiteralParam(param)) match { + case Some(p) => return s"$p.format($arg)" + case _ => + } + + } + case _ => + } + s"$param.format($arg)" + case _ => throw new IllegalArgumentException("DateParse requires exactly one argument") + } } case object DateTimeAdd extends Expr("DATETIME_ADD") with TokenRegex { @@ -509,7 +631,7 @@ package object time { } case object DateTimeParse extends Expr("DATETIME_PARSE") with TokenRegex with PainlessScript { - override def painless(): String = ".parse" + override def painless(context: Option[PainlessContext]): String = ".parse" } case class DateTimeParse(identifier: Identifier, format: String) @@ -518,9 +640,9 @@ package object time { with FunctionWithIdentifier with FunctionWithDateTimeFormat with DateMathScript { - override def fun: Option[PainlessScript] = Some(DateTimeParse) + override def fun: Option[PainlessScript] = None - override def args: List[PainlessScript] = List.empty + override def args: List[PainlessScript] = List(identifier) override def inputType: SQLVarchar = SQLTypes.Varchar override def outputType: SQLDateTime = SQLTypes.DateTime @@ -530,14 +652,26 @@ package object time { s"$sql($base, '$format')" } - override def painless(): String = throw new NotImplementedError( - "Use toPainless instead" - ) - override def toPainless(base: String, idx: Int): String = - if (nullable) - s"(def e$idx = $base; e$idx != null ? DateTimeFormatter.ofPattern('${convert(includeTimeZone = true)}').parse(e$idx, ZonedDateTime::from) : null)" - else - s"DateTimeFormatter.ofPattern('${convert(includeTimeZone = true)}').parse($base, ZonedDateTime::from)" + override def includeTimeZone: Boolean = true + + override def toPainlessCall(callArgs: List[String], context: Option[PainlessContext]): String = + callArgs match { + case arg :: Nil => + context match { + case Some(ctx) => + identifier.baseType match { + case SQLTypes.Varchar => + ctx.addParam(LiteralParam(s"ZonedDateTime.parse($arg, $param)")) match { + case Some(p) => return p + case _ => + } + case _ => + } + case _ => + } + s"ZonedDateTime.parse($arg, $param)" + case _ => throw new IllegalArgumentException("DateParse requires exactly one argument") + } override def script: Option[String] = { val base: String = FunctionUtils @@ -556,7 +690,7 @@ package object time { } case object DateTimeFormat extends Expr("DATETIME_FORMAT") with TokenRegex with PainlessScript { - override def painless(): String = ".format" + override def painless(context: Option[PainlessContext]): String = ".format" } case class DateTimeFormat(identifier: Identifier, format: String) @@ -566,7 +700,7 @@ package object time { with FunctionWithDateTimeFormat { override def fun: Option[PainlessScript] = Some(DateTimeFormat) - override def args: List[PainlessScript] = List.empty + override def args: List[PainlessScript] = List(identifier) override def inputType: SQLDateTime = SQLTypes.DateTime override def outputType: SQLVarchar = SQLTypes.Varchar @@ -576,14 +710,32 @@ package object time { s"$sql($base, '$format')" } - override def painless(): String = throw new NotImplementedError( - "Use toPainless instead" - ) - override def toPainless(base: String, idx: Int): String = - if (nullable) - s"(def e$idx = $base; e$idx != null ? DateTimeFormatter.ofPattern('${convert(includeTimeZone = true)}').format(e$idx) : null)" - else - s"DateTimeFormatter.ofPattern('${convert(includeTimeZone = true)}').format($base)" + override def includeTimeZone: Boolean = true + + override def toPainlessCall(callArgs: List[String], context: Option[PainlessContext]): String = + callArgs match { + case arg :: Nil => + context match { + case Some(ctx) => + identifier.baseType match { + case SQLTypes.Varchar => + ctx.addParam(LiteralParam(s"$param.format($arg)")) match { + case Some(p) => return p + case _ => + } + case _ => + ctx.addParam(LiteralParam(param)) match { + case Some(p) => return s"$p.format($arg)" + case _ => + } + + } + case _ => + } + s"$param.format($arg)" + case _ => throw new IllegalArgumentException("DateParse requires exactly one argument") + } + } } diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/operator/math/ArithmeticExpression.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/operator/math/ArithmeticExpression.scala index bdc702ca..f2670e80 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/operator/math/ArithmeticExpression.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/operator/math/ArithmeticExpression.scala @@ -1,3 +1,19 @@ +/* + * 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.operator.math import app.softnetwork.elastic.sql._ @@ -41,40 +57,71 @@ case class ArithmeticExpression( override def nullable: Boolean = left.nullable || right.nullable - override def toPainless(base: String, idx: Int): String = { + override def toPainless(base: String, idx: Int, context: Option[PainlessContext]): String = { + context match { + case Some(ctx) => + ctx.addParam(left) + ctx.addParam(right) + case _ => + } if (nullable) { - val l = left match { - case t: TransformFunction[_, _] => - SQLTypeUtils.coerce(t.toPainless("", idx + 1), left.baseType, out, nullable = false) - case _ => SQLTypeUtils.coerce(left.painless(), left.baseType, out, nullable = false) - } - val r = right match { - case t: TransformFunction[_, _] => - SQLTypeUtils.coerce(t.toPainless("", idx + 1), right.baseType, out, nullable = false) - case _ => SQLTypeUtils.coerce(right.painless(), right.baseType, out, nullable = false) - } + val l = context + .flatMap(ctx => ctx.get(left)) + .getOrElse(left match { + case t: TransformFunction[_, _] => + SQLTypeUtils.coerce( + t.toPainless("", idx + 1, context), + left.baseType, + out, + nullable = false, + context + ) + case _ => + SQLTypeUtils + .coerce(left.painless(context), left.baseType, out, nullable = false, context) + }) + val r = context + .flatMap(ctx => ctx.get(right)) + .getOrElse(right match { + case t: TransformFunction[_, _] => + SQLTypeUtils.coerce( + t.toPainless("", idx + 1, context), + right.baseType, + out, + nullable = false, + context + ) + case _ => + SQLTypeUtils + .coerce(right.painless(context), right.baseType, out, nullable = false, context) + }) var expr = "" + val leftParam = context.flatMap(ctx => ctx.get(left)).getOrElse(s"lv$idx") + val rightParam = context.flatMap(ctx => ctx.get(right)).getOrElse(s"rv$idx") if (left.nullable) - expr += s"def lv$idx = ($l); " + expr += (if (context.exists(ctx => ctx.get(left).nonEmpty)) "" + else s"def $leftParam = $l; ") if (right.nullable) - expr += s"def rv$idx = ($r); " + expr += (if (context.exists(ctx => ctx.get(right).nonEmpty)) "" + else s"def $rightParam = $r; ") if (left.nullable && right.nullable) - expr += s"(lv$idx == null || rv$idx == null) ? null : (lv$idx ${operator.painless()} rv$idx)" + expr += s"($leftParam == null || $rightParam == null) ? null : ($leftParam ${operator + .painless(context)} $rightParam)" else if (left.nullable) - expr += s"(lv$idx == null) ? null : (lv$idx ${operator.painless()} $r)" + expr += s"($leftParam == null) ? null : ($leftParam ${operator.painless(context)} $r)" else - expr += s"(rv$idx == null) ? null : ($l ${operator.painless()} rv$idx)" + expr += s"($rightParam == null) ? null : ($l ${operator.painless(context)} $rightParam)" if (group) expr = s"($expr)" return s"$base$expr" } - s"$base${painless()}" + s"$base${painless(context)}" } - override def painless(): String = { - val l = SQLTypeUtils.coerce(left, out) - val r = SQLTypeUtils.coerce(right, out) - val expr = s"$l ${operator.painless()} $r" + override def painless(context: Option[PainlessContext]): String = { + val l = SQLTypeUtils.coerce(left, out, context) + val r = SQLTypeUtils.coerce(right, out, context) + val expr = s"$l ${operator.painless(context)} $r" if (group) s"($expr)" else diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/operator/math/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/operator/math/package.scala index 4d91f947..adb3b05b 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/operator/math/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/operator/math/package.scala @@ -1,3 +1,19 @@ +/* + * 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.operator import app.softnetwork.elastic.sql.Expr diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/operator/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/operator/package.scala index d63c7a97..b698a751 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/operator/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/operator/package.scala @@ -1,9 +1,25 @@ +/* + * 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 package object operator { trait Operator extends Token with PainlessScript with TokenRegex { - override def painless(): String = this match { + override def painless(context: Option[PainlessContext]): String = this match { case AND => "&&" case OR => "||" case NOT => "!" diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/operator/time/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/operator/time/package.scala index 828a4191..e96bcaee 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/operator/time/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/operator/time/package.scala @@ -1,13 +1,29 @@ +/* + * 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.operator -import app.softnetwork.elastic.sql.{DateMathScript, Expr} +import app.softnetwork.elastic.sql.{DateMathScript, Expr, PainlessContext} package object time { sealed trait IntervalOperator extends Operator with BinaryOperator with DateMathScript { override def script: Option[String] = Some(sql) override def toString: String = s" $sql " - override def painless(): String = this match { + override def painless(context: Option[PainlessContext]): String = this match { case PLUS => ".plus" case MINUS => ".minus" case _ => sql @@ -15,11 +31,11 @@ package object time { } case object PLUS extends Expr("+") with IntervalOperator { - override def painless(): String = ".plus" + override def painless(context: Option[PainlessContext]): String = ".plus" } case object MINUS extends Expr("-") with IntervalOperator { - override def painless(): String = ".minus" + override def painless(context: Option[PainlessContext]): String = ".minus" } } diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/package.scala index 4e4e05af..b02cc9b9 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/package.scala @@ -1,3 +1,19 @@ +/* + * 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 import app.softnetwork.elastic.sql.function.aggregate.{MAX, MIN} @@ -27,6 +43,8 @@ package object sql { case _ => "" } + /** Base trait for all tokens + */ trait Token extends Serializable with Validation { def sql: String override def toString: String = sql @@ -34,11 +52,11 @@ package object sql { def in: SQLType = baseType private[this] var _out: SQLType = SQLTypes.Null def out: SQLType = if (_out == SQLTypes.Null) baseType else _out - def out_=(t: SQLType): Unit = { + /*def out_=(t: SQLType): Unit = { _out = t - } + }*/ def cast(targetType: SQLType): SQLType = { - this.out = targetType + this._out = targetType this.out } def system: Boolean = false @@ -51,21 +69,153 @@ package object sql { def value: Any } + /** Trait for tokens that can be used in painless scripts + */ trait PainlessScript extends Token { - def painless(): String + + /** Generate painless script for this token + * + * @param context + * the painless context + * @return + * the painless script + */ + def painless(context: Option[PainlessContext] = None): String def nullValue: String = "null" } + /** Trait for tokens that can be used as parameters in painless scripts + */ + trait PainlessParam extends Token { + def param: String + def checkNotNull: String + override def hashCode(): Int = param.hashCode + override def equals(obj: Any): Boolean = { + obj match { + case p: PainlessParam => p.param == param + case _ => false + } + } + + def paramValue: String = + if (nullable && checkNotNull.nonEmpty) + checkNotNull + else + s"$param${painlessMethods.mkString("")}" + + private[this] var _painlessMethods: collection.mutable.Seq[String] = + collection.mutable.Seq.empty + + def addPainlessMethod(method: String): PainlessParam = { + if (!_painlessMethods.contains(method)) + _painlessMethods = _painlessMethods :+ method // FIXME we should apply functions only once + this + } + + def painlessMethods: Seq[String] = _painlessMethods.toSeq + + } + + case class LiteralParam(param: String) extends PainlessParam { + override def sql: String = "" + override def checkNotNull: String = "" + } + + /** Context for painless scripts + */ + case class PainlessContext() { + // List of parameter keys + private[this] var _keys: collection.mutable.Seq[PainlessParam] = collection.mutable.Seq.empty + + // List of parameter names + private[this] var _values: collection.mutable.Seq[String] = collection.mutable.Seq.empty + + // Last parameter name added + private[this] var _lastParam: Option[String] = None + + /** Add a token parameter to the context if not already present + * + * @param token + * the token parameter to add + * @return + * the optional parameter name + */ + def addParam(token: Token): Option[String] = { + token match { + case param: PainlessParam + if param.param.nonEmpty && (param.isInstanceOf[LiteralParam] || param.nullable) => + get(param) match { + case Some(p) => Some(p) + case _ => + val index = _values.indexOf(param.param) + if (index >= 0) { + Some(param.param) + } else { + val paramName = s"param${_keys.size + 1}" + _keys = _keys :+ param + _values = _values :+ paramName + _lastParam = Some(paramName) + _lastParam + } + } + case _ => None + } + } + + def get(token: Token): Option[String] = { + token match { + case param: PainlessParam => + if (exists(param)) Try(_values(_keys.indexOf(param))).toOption + else None + case f: FunctionWithIdentifier => get(f.identifier) + case _ => None + } + } + + def exists(token: Token): Boolean = { + token match { + case param: PainlessParam => _keys.contains(param) + case f: FunctionWithIdentifier => exists(f.identifier) + case _ => false + } + } + + def isEmpty: Boolean = _keys.isEmpty + + def nonEmpty: Boolean = _keys.nonEmpty + + def last: Option[String] = _lastParam + + def find(paramName: String): Option[PainlessParam] = { + val index = _values.indexOf(paramName) + if (index >= 0) Some(_keys(index)) + else None + } + + override def toString: String = { + if (isEmpty) "" + else + _keys + .flatMap { param => + get(param) match { + case Some(v) => Some(s"def $v = ${param.paramValue}; ") + case None => None // should not happen + } + } + .mkString("") + } + } + trait PainlessParams extends PainlessScript { def params: Map[String, Any] } + /** Trait for tokens that can be used in date math scripts + */ trait DateMathScript extends Token { def script: Option[String] - def hasScript: Boolean = script.isDefined override def dateMathScript: Boolean = true def formatScript: Option[String] = None - def hasFormat: Boolean = formatScript.isDefined } object DateMathRounding { @@ -114,7 +264,7 @@ package object sql { case _ => values.headOption } } - override def painless(): String = + override def painless(context: Option[PainlessContext]): String = SQLTypeUtils.coerce( value match { case s: String => s""""$s"""" @@ -124,7 +274,8 @@ package object sql { }, this.baseType, this.out, - nullable = false + nullable = false, + context ) override def nullable: Boolean = false @@ -132,7 +283,7 @@ package object sql { case object Null extends Value[Null](null) with TokenRegex { override def sql: String = "NULL" - override def painless(): String = "null" + override def painless(context: Option[PainlessContext]): String = "null" override def nullable: Boolean = true override def baseType: SQLType = SQLTypes.Null } @@ -236,13 +387,13 @@ package object sql { case object PiValue extends Value[Double](Math.PI) with TokenRegex { override def sql: String = "PI" - override def painless(): String = "Math.PI" + override def painless(context: Option[PainlessContext]): String = "Math.PI" override def baseType: SQLNumeric = SQLTypes.Double } case object EValue extends Value[Double](Math.E) with TokenRegex { override def sql: String = "E" - override def painless(): String = "Math.E" + override def painless(context: Option[PainlessContext]): String = "Math.E" override def baseType: SQLNumeric = SQLTypes.Double } @@ -252,7 +403,7 @@ package object sql { override def baseType: SQLNumeric = SQLTypes.Double override def sql: String = s"$longValue $unit" def geoDistance: String = s"$longValue$unit" - override def painless(): String = s"$value" + override def painless(context: Option[PainlessContext]): String = s"$value" } sealed abstract class FromTo(val from: TokenValue, val to: TokenValue) extends Token { @@ -317,8 +468,8 @@ package object sql { extends Token with PainlessScript { override def sql = s"(${values.map(_.sql).mkString(",")})" - override def painless(): String = - s"[${values.map(_.painless()).mkString(",")}]" + override def painless(context: Option[PainlessContext]): String = + s"[${values.map(_.painless(context)).mkString(",")}]" lazy val innerValues: Seq[R] = values.map(_.value) override def nullable: Boolean = values.exists(_.nullable) override def baseType: SQLArray = SQLTypes.Array(SQLTypes.Any) @@ -450,7 +601,8 @@ package object sql { with Source with FunctionChain with PainlessScript - with DateMathScript { + with DateMathScript + with PainlessParam { def name: String def withFunctions(functions: List[Function]): Identifier @@ -500,9 +652,13 @@ package object sql { lazy val identifierName: String = functions.reverse.foldLeft(name)((expr, fun) => { fun.toSQL(expr) - }) // FIXME use AliasUtils.normalize? + }) // TODO use AliasUtils.normalize? - lazy val innerHitsName: Option[String] = if (nested) tableAlias else None + lazy val innerHitsName: Option[String] = + nestedElement match { + case Some(ne) => Some(ne.innerHitsName) + case None => None + } lazy val aliasOrName: String = fieldAlias.getOrElse(name) @@ -522,19 +678,6 @@ package object sql { s"doc['$path'].value" else "" - def toPainless(base: String): String = { - val orderedFunctions = FunctionUtils.transformFunctions(this).reverse - var expr = base - orderedFunctions.zipWithIndex.foreach { case (f, idx) => - f match { - case f: TransformFunction[_, _] => expr = f.toPainless(expr, idx) - case f: PainlessScript => expr = s"$expr${f.painless()}" - case f => expr = f.toSQL(expr) // fallback - } - } - expr - } - def script: Option[String] = if (isTemporal) { var orderedFunctions = FunctionUtils.transformFunctions(this).reverse @@ -588,14 +731,33 @@ package object sql { def checkNotNull: String = if (path.isEmpty) "" else - s"(!doc.containsKey('$path') || doc['$path'].empty ? $nullValue : doc['$path'].value)" + s"(!doc.containsKey('$path') || doc['$path'].empty ? $nullValue : doc['$path'].value${painlessMethods + .mkString("")})" + + override def painless(context: Option[PainlessContext]): String = { + val base = + context match { + case Some(ctx) => + ctx.addParam(this).getOrElse("") + case _ => + if (nullable) + checkNotNull + else + paramName + } + val orderedFunctions = FunctionUtils.transformFunctions(this).reverse + var expr = base + orderedFunctions.zipWithIndex.foreach { case (f, idx) => + f match { + case f: TransformFunction[_, _] => expr = f.toPainless(expr, idx, context) + case f: PainlessScript => expr = s"$expr${f.painless(context)}" + case f => expr = f.toSQL(expr) // fallback + } + } + expr + } - override def painless(): String = toPainless( - if (nullable) - checkNotNull - else - paramName - ) + override def param: String = paramName private[this] var _nullable = this.name.nonEmpty && (!aggregation || functions.size > 1) @@ -614,7 +776,7 @@ package object sql { override def value: String = script match { case Some(s) => s - case _ => painless() + case _ => painless(None) } def withNested(nested: Boolean): Identifier = this match { diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/Delimiter.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/Delimiter.scala index f8ffc49f..fe9625c7 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/Delimiter.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/Delimiter.scala @@ -1,3 +1,19 @@ +/* + * 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 app.softnetwork.elastic.sql.{Expr, Token} 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 53800f8d..b18ea945 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 @@ -1,3 +1,19 @@ +/* + * 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 app.softnetwork.elastic.sql.query.{ 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 c6d74c01..4b7670b4 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 @@ -1,3 +1,19 @@ +/* + * 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 app.softnetwork.elastic.sql.query.{Bucket, GroupBy} 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 59e3588e..7187345d 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 @@ -1,3 +1,19 @@ +/* + * 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 app.softnetwork.elastic.sql.query.Having 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 2bc2a0cb..fbbe37d4 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 @@ -1,3 +1,19 @@ +/* + * 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 app.softnetwork.elastic.sql.query.{Limit, Offset} 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 bbf56622..884f0616 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 @@ -1,3 +1,19 @@ +/* + * 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 app.softnetwork.elastic.sql.function.Function 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 3e5ef049..7f8f6ac2 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 @@ -1,3 +1,19 @@ +/* + * 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 app.softnetwork.elastic.sql._ @@ -106,7 +122,7 @@ trait Parser } def sql_function: PackratParser[Function] = - aggregate_function | time_function | conditional_function | string_function + aggregate_function | time_function | conditional_function private val reservedKeywords = Seq( "select", 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 6746f1ee..752fcaa8 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 @@ -1,3 +1,19 @@ +/* + * 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 app.softnetwork.elastic.sql.query.{Except, Field, Select} diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/Validator.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/Validator.scala index 15e6e32d..1ea0e14c 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/parser/Validator.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/parser/Validator.scala @@ -1,3 +1,19 @@ +/* + * 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 app.softnetwork.elastic.sql.`type`.{SQLType, SQLTypeUtils} 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 0eced705..b5d880e3 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 @@ -1,3 +1,19 @@ +/* + * 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 app.softnetwork.elastic.sql.function.geo.Meters @@ -54,7 +70,7 @@ import app.softnetwork.elastic.sql.query.{ InExpr, IsNotNullExpr, IsNullExpr, - MatchCriteria, + MultiMatchCriteria, Predicate, Where } @@ -193,12 +209,12 @@ trait WhereParser { DistanceCriteria(d, o, g) }*/ - def matchCriteria: PackratParser[MatchCriteria] = + def matchCriteria: PackratParser[MultiMatchCriteria] = MATCH.regex ~ start ~ rep1sep( any_identifier, separator ) ~ end ~ AGAINST.regex ~ start ~ literal ~ end ^^ { case _ ~ _ ~ i ~ _ ~ _ ~ _ ~ l ~ _ => - MatchCriteria(i, l) + MultiMatchCriteria(i, l) } def and: PackratParser[PredicateOperator] = AND.regex ^^ (_ => AND) 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 8da134ce..8a962efd 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 @@ -1,3 +1,19 @@ +/* + * 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.function import app.softnetwork.elastic.sql.Identifier 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 04b3a7f4..e96bbb6f 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 @@ -1,3 +1,19 @@ +/* + * 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.function import app.softnetwork.elastic.sql.function.{FunctionWithIdentifier, TransformFunction} 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 6fce1809..f1845297 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 @@ -1,3 +1,19 @@ +/* + * 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.function import app.softnetwork.elastic.sql.function.convert.{Cast, CastOperator, Convert, TryCast} 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 421dc0f6..752f8db2 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 @@ -1,3 +1,19 @@ +/* + * 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.function import app.softnetwork.elastic.sql.{GeoDistance, 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 6893fe43..9c3038e4 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 @@ -1,3 +1,19 @@ +/* + * 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.function import app.softnetwork.elastic.sql.Identifier 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 a265a5ff..d924e914 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 @@ -1,3 +1,19 @@ +/* + * 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.function import app.softnetwork.elastic.sql.Identifier @@ -64,44 +80,53 @@ package object string { ) } - def stringFunctionWithIdentifier: PackratParser[Identifier] = - (concat | substr | left | right | replace | reverse | position | regexp) ^^ { sf => - sf.identifier - } - def length: PackratParser[StringFunction[SQLBigInt]] = - Length.regex ^^ { _ => - Length() + Length.regex ~ start ~ valueExpr ~ end ^^ { case _ ~ _ ~ v ~ _ => + Length(v) } def lower: PackratParser[StringFunction[SQLVarchar]] = - Lower.regex ^^ { _ => - StringFunctionWithOp(Lower) + Lower.regex ~ start ~ valueExpr ~ end ^^ { case _ ~ _ ~ v ~ _ => + StringFunctionWithOp(v, Lower) } def upper: PackratParser[StringFunction[SQLVarchar]] = - Upper.regex ^^ { _ => - StringFunctionWithOp(Upper) + Upper.regex ~ start ~ valueExpr ~ end ^^ { case _ ~ _ ~ v ~ _ => + StringFunctionWithOp(v, Upper) } def trim: PackratParser[StringFunction[SQLVarchar]] = - Trim.regex ^^ { _ => - StringFunctionWithOp(Trim) + Trim.regex ~ start ~ valueExpr ~ end ^^ { case _ ~ _ ~ v ~ _ => + StringFunctionWithOp(v, Trim) } def ltrim: PackratParser[StringFunction[SQLVarchar]] = - Ltrim.regex ^^ { _ => - StringFunctionWithOp(Ltrim) + Ltrim.regex ~ start ~ valueExpr ~ end ^^ { case _ ~ _ ~ v ~ _ => + StringFunctionWithOp(v, Ltrim) } def rtrim: PackratParser[StringFunction[SQLVarchar]] = - Rtrim.regex ^^ { _ => - StringFunctionWithOp(Rtrim) + Rtrim.regex ~ start ~ valueExpr ~ end ^^ { case _ ~ _ ~ v ~ _ => + StringFunctionWithOp(v, Rtrim) } - def string_function: Parser[ - StringFunction[_] - ] = /*concatFunction | substringFunction |*/ length | lower | upper | trim | ltrim | rtrim + def stringFunctionWithIdentifier: PackratParser[Identifier] = + (concat | + substr | + left | + right | + replace | + reverse | + position | + regexp | + length | + lower | + upper | + trim | + ltrim | + rtrim) ^^ { sf => + sf.identifier + } } } 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 f0061cec..aead8076 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 @@ -1,7 +1,23 @@ +/* + * 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.function import app.softnetwork.elastic.sql.{function, Identifier, StringValue} -import app.softnetwork.elastic.sql.`type`.{SQLLiteral, SQLNumeric, SQLTemporal} +import app.softnetwork.elastic.sql.`type`.{SQLLiteral, SQLNumeric, SQLTemporal, SQLTypes} import app.softnetwork.elastic.sql.function.{ BinaryFunction, FunctionWithIdentifier, @@ -83,7 +99,9 @@ package object time { def last_day: Parser[DateFunction with FunctionWithIdentifier] = LastDayOfMonth.regex ~ start ~ (identifierWithTransformation | identifierWithIntervalFunction | identifierWithFunction | identifier) ~ end ^^ { - case _ ~ _ ~ i ~ _ => LastDayOfMonth(i) + case _ ~ _ ~ i ~ _ => + i.cast(SQLTypes.Date) + LastDayOfMonth(i) } def date_function: PackratParser[DateFunction with FunctionWithIdentifier] = @@ -173,14 +191,21 @@ package object time { import TimeField._ + def 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 => + dw.identifier.withFunctions(dw +: dw.identifier.functions) + } + def year_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = YEAR.regex ^^ (_ => new Year) def month_of_year_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = MONTH_OF_YEAR.regex ^^ (_ => new MonthOfYear) def day_of_month_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = DAY_OF_MONTH.regex ^^ (_ => new DayOfMonth) - def day_of_week_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = - DAY_OF_WEEK.regex ^^ (_ => new DayOfWeek) def day_of_year_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = DAY_OF_YEAR.regex ^^ (_ => new DayOfYear) def hour_of_day_tr: PackratParser[TransformFunction[SQLTemporal, SQLNumeric]] = @@ -210,7 +235,6 @@ package object time { year_tr | month_of_year_tr | day_of_month_tr | - day_of_week_tr | day_of_year_tr | hour_of_day_tr | minute_of_hour_tr | @@ -232,6 +256,7 @@ package object time { dateTimeFunctionWithIdentifier | date_diff_identifier | date_trunc_identifier | + day_of_week_identifier | extract_identifier) ~ rep(intervalFunction) ^^ { case i ~ f => i.withFunctions(f ++ i.functions) } 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 991ec3ba..4e5e7b81 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 @@ -1,3 +1,19 @@ +/* + * 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.operator import app.softnetwork.elastic.sql.function.{Function, FunctionWithIdentifier} 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 b3ec69f8..ff3983ff 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 @@ -1,3 +1,19 @@ +/* + * 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 app.softnetwork.elastic.sql.Identifier 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 f3195889..228673ea 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 @@ -1,3 +1,19 @@ +/* + * 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 app.softnetwork.elastic.sql.{ diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/query/From.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/query/From.scala index d95c8b06..eb0db185 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/query/From.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/query/From.scala @@ -1,3 +1,19 @@ +/* + * 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.query import app.softnetwork.elastic.sql.{ diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/query/GroupBy.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/query/GroupBy.scala index ba43a67b..e619614e 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/query/GroupBy.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/query/GroupBy.scala @@ -1,3 +1,19 @@ +/* + * 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.query import app.softnetwork.elastic.sql.`type`.SQLTypes @@ -70,7 +86,7 @@ object MetricSelectorScript { case Predicate(left, _, right, _, _) => extractMetricsPath(left) ++ extractMetricsPath(right) case relation: ElasticRelation => extractMetricsPath(relation.criteria) - case _: MatchCriteria => Map.empty //MATCH is not supported in bucket_selector + case _: MultiMatchCriteria => Map.empty //MATCH is not supported in bucket_selector case e: Expression if e.aggregation => import e._ maybeValue match { @@ -85,7 +101,7 @@ object MetricSelectorScript { val leftStr = metricSelector(left) val rightStr = metricSelector(right) val opStr = op match { - case AND | OR => op.painless() + case AND | OR => op.painless(None) case _ => throw new IllegalArgumentException(s"Unsupported logical operator: $op") } val not = maybeNot.nonEmpty @@ -96,10 +112,10 @@ object MetricSelectorScript { case relation: ElasticRelation => metricSelector(relation.criteria) - case _: MatchCriteria => "1 == 1" //MATCH is not supported in bucket_selector + case _: MultiMatchCriteria => "1 == 1" //MATCH is not supported in bucket_selector case e: Expression if e.aggregation => - val painless = e.painless() + val painless = e.painless(None) e.maybeValue match { case Some(value) if e.operator.isInstanceOf[ComparisonOperator] => value.out match { // compare epoch millis diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/query/Having.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/query/Having.scala index 929da2fd..07ad6625 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/query/Having.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/query/Having.scala @@ -1,3 +1,19 @@ +/* + * 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.query import app.softnetwork.elastic.sql.{Expr, TokenRegex, Updateable} diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/query/Limit.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/query/Limit.scala index 3cb1f3af..412d525a 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/query/Limit.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/query/Limit.scala @@ -1,3 +1,19 @@ +/* + * 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.query import app.softnetwork.elastic.sql.{Expr, TokenRegex} diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/query/OrderBy.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/query/OrderBy.scala index c0ba2906..68d6ae60 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/query/OrderBy.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/query/OrderBy.scala @@ -1,3 +1,19 @@ +/* + * 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.query import app.softnetwork.elastic.sql.function.{Function, FunctionChain} diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/query/SQLMultiSearchRequest.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/query/SQLMultiSearchRequest.scala index eda9f18d..f464f914 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/query/SQLMultiSearchRequest.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/query/SQLMultiSearchRequest.scala @@ -1,3 +1,19 @@ +/* + * 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.query import app.softnetwork.elastic.sql.Token diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/query/SQLQuery.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/query/SQLQuery.scala index 7fd44b24..96132b08 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/query/SQLQuery.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/query/SQLQuery.scala @@ -1,3 +1,19 @@ +/* + * 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.query case class SQLQuery(query: String, score: Option[Double] = None) { diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/query/SQLSearchRequest.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/query/SQLSearchRequest.scala index 0f631a0c..4dda85f8 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/query/SQLSearchRequest.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/query/SQLSearchRequest.scala @@ -1,3 +1,19 @@ +/* + * 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.query import app.softnetwork.elastic.sql.function.aggregate.TopHitsAggregation @@ -19,8 +35,18 @@ case class SQLSearchRequest( lazy val fieldAliases: Map[String, String] = select.fieldAliases lazy val tableAliases: Map[String, String] = from.tableAliases lazy val unnestAliases: Map[String, (String, Option[Limit])] = from.unnestAliases - lazy val bucketNames: Map[String, Bucket] = buckets.map { b => - b.identifier.identifierName -> b + lazy val bucketNames: Map[String, Bucket] = buckets.flatMap { b => + val name = b.identifier.identifierName + "\\d+".r.findFirstIn(name) match { + case Some(n) => + val identifier = select.fields(n.toInt - 1).identifier + val updated = b.copy(identifier = select.fields(n.toInt - 1).identifier) + Map( + n -> updated, // also map numeric bucket to field name + identifier.identifierName -> updated + ) + case _ => Map(name -> b) + } }.toMap lazy val unnests: Map[String, Unnest] = from.unnests.map(u => u.alias.map(_.alias).getOrElse(u.name) -> u).toMap @@ -32,7 +58,6 @@ case class SQLSearchRequest( lazy val nested: Seq[NestedElement] = from.unnests.map(toNestedElement).groupBy(_.path).map(_._2.head).toList private[this] lazy val nestedFieldsWithoutCriteria: Map[String, Seq[Field]] = { - // nested fields that are not part of where, having or group by clauses val innerHitsWithCriteria = (where.map(_.nestedElements).getOrElse(Seq.empty) ++ having.map(_.nestedElements).getOrElse(Seq.empty) ++ groupBy.map(_.nestedElements).getOrElse(Seq.empty)) @@ -45,6 +70,7 @@ case class SQLSearchRequest( } ret } + // nested fields that are not part of where, having or group by clauses lazy val nestedElementsWithoutCriteria: Seq[NestedElement] = nested.filter(n => nestedFieldsWithoutCriteria.keys.toSeq.contains(n.innerHitsName)) diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/query/Select.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/query/Select.scala index 5cd3a74e..57d9bf9b 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/query/Select.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/query/Select.scala @@ -1,3 +1,19 @@ +/* + * 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.query import app.softnetwork.elastic.sql.function.aggregate.TopHitsAggregation @@ -9,6 +25,7 @@ import app.softnetwork.elastic.sql.{ DateMathScript, Expr, Identifier, + PainlessContext, PainlessScript, TokenRegex, Updateable @@ -64,7 +81,7 @@ case class Field( this.copy(identifier = updated.update(request)) } - def painless(): String = identifier.painless() + def painless(context: Option[PainlessContext]): String = identifier.painless(context) def script: Option[String] = identifier.script diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/query/Where.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/query/Where.scala index 512695af..3e11657b 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/query/Where.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/query/Where.scala @@ -1,6 +1,22 @@ +/* + * 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.query -import app.softnetwork.elastic.sql.`type`.{SQLAny, SQLType, SQLTypeUtils, SQLTypes} +import app.softnetwork.elastic.sql.`type`.{SQLAny, SQLTemporal, SQLType, SQLTypeUtils, SQLTypes} import app.softnetwork.elastic.sql.function._ import app.softnetwork.elastic.sql.function.cond.{ConditionalFunction, IsNotNull, IsNull} import app.softnetwork.elastic.sql.function.geo.Distance @@ -19,7 +35,7 @@ sealed trait Criteria extends Updateable with PainlessScript { case Predicate(left, _, right, _, _) => left.identifiers ++ right.identifiers case c: Expression => c.identifiers case relation: ElasticRelation => relation.criteria.identifiers - case m: MatchCriteria => m.identifiers + case m: MultiMatchCriteria => m.identifiers case _ => Nil } @@ -29,50 +45,52 @@ sealed trait Criteria extends Updateable with PainlessScript { def nestedElements: Seq[NestedElement] = this match { - case p: Predicate => p.nestedElements - case r: ElasticRelation => r.criteria.nestedElements - case e: Expression => e.nestedElement.toSeq - case m: MatchCriteria => m.criteria.nestedElements - case _ => Nil + case p: Predicate => p.nestedElements + case r: ElasticRelation => r.criteria.nestedElements + case e: Expression => e.nestedElement.toSeq + case m: MultiMatchCriteria => m.criteria.nestedElements + case _ => Nil } def nestedCriteria(innerHitsName: String): Seq[Criteria] = { this match { case e: ElasticNested => e.criteria.nestedCriteria(innerHitsName) case _ => - nestedElement - .filter(_ => nestedElement.exists(_.innerHitsName == innerHitsName)) + nestedElements + .find(n => n.innerHitsName == innerHitsName) .map(_ => this) .toSeq } } - def extractMetricsPath: Map[String, String] = this match { // used for bucket_selector - case Predicate(left, _, right, _, _) => - left.extractMetricsPath ++ right.extractMetricsPath - case relation: ElasticRelation => relation.criteria.extractMetricsPath - case _: MatchCriteria => Map.empty //MATCH is not supported in bucket_selector - case e: Expression => e.extractMetricsPath - case _ => Map.empty - } + def extractMetricsPath: Map[String, String] = + this match { // used for bucket_selector + case Predicate(left, _, right, _, _) => + left.extractMetricsPath ++ right.extractMetricsPath + case relation: ElasticRelation => relation.criteria.extractMetricsPath + case _: MultiMatchCriteria => Map.empty //MATCH is not supported in bucket_selector + case e: Expression => e.extractMetricsPath + case _ => Map.empty + } def includes( bucket: Bucket, not: Boolean, bucketIncludesExcludes: BucketIncludesExcludes - ): BucketIncludesExcludes = this match { - case Predicate(left, _, right, n, _) => - right.includes( - bucket, - (!not && n.isDefined) || (not && n.isEmpty), - left.includes(bucket, not, bucketIncludesExcludes) - ) - case relation: ElasticRelation => - relation.criteria.includes(bucket, not, bucketIncludesExcludes) - case m: MatchCriteria => m.criteria.includes(bucket, not, bucketIncludesExcludes) - case e: Expression => e.includes(bucket, not, bucketIncludesExcludes) - case _ => bucketIncludesExcludes - } + ): BucketIncludesExcludes = + this match { + case Predicate(left, _, right, n, _) => + right.includes( + bucket, + (!not && n.isDefined) || (not && n.isEmpty), + left.includes(bucket, not, bucketIncludesExcludes) + ) + case relation: ElasticRelation => + relation.criteria.includes(bucket, not, bucketIncludesExcludes) + case m: MultiMatchCriteria => m.criteria.includes(bucket, not, bucketIncludesExcludes) + case e: Expression => e.includes(bucket, not, bucketIncludesExcludes) + case _ => bucketIncludesExcludes + } def excludes( bucket: Bucket, @@ -106,22 +124,22 @@ sealed trait Criteria extends Updateable with PainlessScript { override def out: SQLType = SQLTypes.Boolean - override def painless(): String = this match { + override def painless(context: Option[PainlessContext]): String = this match { case Predicate(left, op, right, maybeNot, group) => - val leftStr = left.painless() - val rightStr = right.painless() + val leftStr = left.painless(context) + val rightStr = right.painless(context) val opStr = op match { - case AND | OR => op.painless() + case AND | OR => op.painless(context) case _ => throw new IllegalArgumentException(s"Unsupported logical operator: $op") } val not = maybeNot.nonEmpty if (group || not) - s"${maybeNot.map(_.painless()).getOrElse("")}($leftStr $opStr $rightStr)" + s"${maybeNot.map(_.painless(context)).getOrElse("")}($leftStr $opStr $rightStr)" else s"$leftStr $opStr $rightStr" - case relation: ElasticRelation => asGroup(relation.criteria.painless()) - case m: MatchCriteria => asGroup(m.criteria.painless()) - case expr: Expression => asGroup(expr.painless()) + case relation: ElasticRelation => asGroup(relation.criteria.painless(context)) + case m: MultiMatchCriteria => asGroup(m.criteria.painless(context)) + case expr: Expression => asGroup(expr.painless(context)) case _ => throw new IllegalArgumentException(s"Unsupported criteria: $this") } } @@ -346,17 +364,17 @@ sealed trait Expression extends FunctionChain with ElasticFilter with Criteria { def painlessNot: String = operator match { case _: ComparisonOperator => "" - case _ => maybeNot.map(_.painless()).getOrElse("") + case _ => maybeNot.map(_.painless(None)).getOrElse("") } def painlessOp: String = operator match { - case o: ComparisonOperator if maybeNot.isDefined => o.not.painless() - case _ => operator.painless() + case o: ComparisonOperator if maybeNot.isDefined => o.not.painless(None) + case _ => operator.painless(None) } - def painlessValue: String = maybeValue + def painlessValue(context: Option[PainlessContext]): String = maybeValue .map { - case v: PainlessScript => v.painless() + case v: PainlessScript => v.painless(context) case v => v.sql } .getOrElse("") /*{ @@ -366,25 +384,110 @@ sealed trait Expression extends FunctionChain with ElasticFilter with Criteria { } }*/ - protected lazy val left: String = { + protected def left(context: Option[PainlessContext]): String = { val targetedType = maybeValue match { - case Some(v) => v.out + case Some(v) => SQLTypeUtils.leastCommonSuperType(List(identifier.out, v.out)) case None => identifier.out } - SQLTypeUtils.coerce(identifier, targetedType) + context match { + case Some(ctx) => + ctx.addParam(identifier) match { + case Some(_) => + identifier.baseType match { + case SQLTypes.Any => // in painless context, Any is ZonedDateTime + maybeValue.map(_.out).getOrElse(SQLTypes.Any) match { + case SQLTypes.Date => + identifier.addPainlessMethod(".toLocalDate()") + case SQLTypes.Time => + identifier.addPainlessMethod(".toLocalTime()") + case _ => + } + case _ => + } + case _ => // do nothing + } + case _ => // do nothing + } + SQLTypeUtils.coerce(identifier, targetedType, context) } - protected lazy val check: String = + protected def check(context: Option[PainlessContext], param: String): String = { operator match { - case _: ComparisonOperator => s" $painlessOp $painlessValue" - case _ => s"$painlessOp($painlessValue)" + case comparison: ComparisonOperator => + comparison match { + case LT => + maybeValue.map(v => v.out).getOrElse(SQLTypes.Any) match { + case SQLTypes.Varchar => + return s"$param.compareTo(${painlessValue(context)}) < 0" + case _: SQLTemporal if !aggregation && !hasBucket => + return s"$param.isBefore(${painlessValue(context)})" + case _ => + } + case GT => + maybeValue.map(v => v.out).getOrElse(SQLTypes.Any) match { + case SQLTypes.Varchar => + return s"$param.compareTo(${painlessValue(context)}) > 0" + case _: SQLTemporal if !aggregation && !hasBucket => + return s"$param.isAfter(${painlessValue(context)})" + case _ => + } + case EQ => + maybeValue.map(v => v.out).getOrElse(SQLTypes.Any) match { + case SQLTypes.Varchar => + return s"$param.compareTo(${painlessValue(context)}) == 0" + case _: SQLTemporal if !aggregation && !hasBucket => + return s"$param.isEqual(${painlessValue(context)})" + case _ => + } + case NE | DIFF => + maybeValue.map(v => v.out).getOrElse(SQLTypes.Any) match { + case SQLTypes.Varchar => + return s"$param.compareTo(${painlessValue(context)}) != 0" + case _: SQLTemporal if !aggregation && !hasBucket => + return s"$param.isEqual(${painlessValue(context)}) == false" + case _ => + } + case GE => + maybeValue.map(v => v.out).getOrElse(SQLTypes.Any) match { + case SQLTypes.Varchar => + return s"$param.compareTo(${painlessValue(context)}) >= 0" + case _: SQLTemporal if !aggregation && !hasBucket => + return s"$param.isBefore(${painlessValue(context)}) == false" + case _ => + } + case LE => + maybeValue.map(v => v.out).getOrElse(SQLTypes.Any) match { + case SQLTypes.Varchar => + return s"$param.compareTo(${painlessValue(context)}) <= 0" + case _: SQLTemporal if !aggregation && !hasBucket => + return s"$param.isAfter(${painlessValue(context)}) == false" + case _ => + } + case _ => + } + s"$param $painlessOp ${painlessValue(context)}" + case _ => s"$param$painlessOp(${painlessValue(context)})" } + } - override def painless(): String = { + override def painless(context: Option[PainlessContext]): String = { + val innerLeft = left(context) + context match { + case Some(ctx) => + ctx.get(identifier) match { + case Some(p) => + if (identifier.nullable) + return s"$p == null ? false : $painlessNot(${check(context, p)})" + else + return s"$painlessNot(${check(context, p)})" + case _ => + } + case _ => + } if (identifier.nullable) { - return s"def left = $left; left == null ? false : ${painlessNot}left$check" + return s"def left = $innerLeft; left == null ? false : $painlessNot(${check(context, "left")})" } - s"$painlessNot$left$check" + s"$painlessNot${check(context, innerLeft)}" } override def validate(): Either[String, Unit] = { @@ -496,11 +599,19 @@ case class IsNullCriteria(identifier: Identifier) extends CriteriaWithConditiona } else updated } - override def painless(): String = { + override def painless(context: Option[PainlessContext]): String = { + context match { + case Some(ctx) => + ctx.addParam(identifier) match { + case Some(p) => return s"$p == null" // TODO check with not + case _ => + } + case _ => + } if (identifier.nullable) { - return s"def left = $left; left == null" + return s"def left = ${left(context)}; left == null" } - s"$painlessNot$left$check" + s"${left(context)} == null" } } @@ -519,11 +630,19 @@ case class IsNotNullCriteria(identifier: Identifier) updated } - override def painless(): String = { + override def painless(context: Option[PainlessContext]): String = { + context match { + case Some(ctx) => + ctx.addParam(identifier) match { + case Some(p) => return s"$p != null" // TODO check with not + case _ => + } + case _ => + } if (identifier.nullable) { - return s"def left = $left; left != null" + return s"def left = ${left(context)}; left != null" } - s"$painlessNot$left$check" + s"${left(context)} != null" } } @@ -568,8 +687,8 @@ case class InExpr[R, +T <: Value[R]]( override def asFilter(currentQuery: Option[ElasticBoolQuery]): ElasticFilter = this - override def painless(): String = - s"$painlessNot${identifier.painless()}$painlessOp($painlessValue)" + override def painless(context: Option[PainlessContext]): String = + s"$painlessNot${identifier.painless(context)}$painlessOp(${painlessValue(context)})" } @@ -600,11 +719,23 @@ case class BetweenExpr( } yield () } - override def painless(): String = { + override def painless(context: Option[PainlessContext]): String = { + context match { + case Some(ctx) => + ctx.addParam(identifier) match { + case Some(p) => + if (identifier.nullable) + return s"$p == null ? false : $painlessNot($p >= ${fromTo.from} && $p <= ${fromTo.to})" + else + return s"$painlessNot($p >= ${fromTo.from} && $p <= ${fromTo.to})" + case _ => + } + case _ => + } if (identifier.nullable) { - return s"def left = $left; left == null ? false : $painlessNot(${fromTo.from} <= left <= ${fromTo.to})" + return s"def left = ${left(context)}; left == null ? false : $painlessNot(${fromTo.from} <= left <= ${fromTo.to})" } - s"$painlessNot(${fromTo.from} <= $left <= ${fromTo.to})" + s"$painlessNot(${fromTo.from} <= ${left(context)} <= ${fromTo.to})" } } @@ -629,11 +760,11 @@ case class DistanceCriteria( override def asFilter(currentQuery: Option[ElasticBoolQuery]): ElasticFilter = this } -case class MatchCriteria( +case class MultiMatchCriteria( override val identifiers: Seq[Identifier], value: StringValue, nestedElement: Option[NestedElement] = None -) extends Criteria { +) extends Criteria { // FIXME map to multi_match override def sql: String = s"$operator (${identifiers.mkString(",")}) $AGAINST ($value)" override def operator: Operator = MATCH @@ -643,7 +774,7 @@ case class MatchCriteria( override lazy val nested: Boolean = identifiers.forall(_.nested) @tailrec - private[this] def toCriteria(matches: List[ElasticMatch], curr: Criteria): Criteria = + private[this] def toCriteria(matches: List[MatchCriteria], curr: Criteria): Criteria = matches match { case Nil => curr case single :: Nil => Predicate(curr, OR, single) @@ -651,7 +782,7 @@ case class MatchCriteria( } lazy val criteria: Criteria = - (identifiers.map(id => ElasticMatch(id, value, None)) match { + (identifiers.map(id => MatchCriteria(id, value, None)) match { case Nil => throw new IllegalArgumentException("No identifiers for MATCH") case single :: Nil => single case first :: rest => toCriteria(rest, first) @@ -670,7 +801,7 @@ case class MatchCriteria( override def group: Boolean = false } -case class ElasticMatch( +case class MatchCriteria( identifier: Identifier, value: StringValue, options: Option[String] @@ -705,8 +836,8 @@ case class ElasticMatch( override def matchCriteria: Boolean = true - override def painless(): String = - s"$painlessNot${identifier.painless()}$painlessOp($painlessValue)" + override def painless(context: Option[PainlessContext]): String = + s"$painlessNot${identifier.painless(context)}$painlessOp(${painlessValue(context)})" } diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/time/package.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/time/package.scala index cbc2cad5..ec373753 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/time/package.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/time/package.scala @@ -1,3 +1,19 @@ +/* + * 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 import app.softnetwork.elastic.sql.`type`._ @@ -7,7 +23,7 @@ import scala.util.matching.Regex package object time { sealed trait TimeField extends PainlessScript with TokenRegex { - override def painless(): String = s"ChronoField.$timeField" + override def painless(context: Option[PainlessContext]): String = s"ChronoField.$timeField" override def nullable: Boolean = false @@ -62,7 +78,8 @@ package object time { sealed trait IsoField extends TimeField { def isoField: String def timeField: String = isoField - override def painless(): String = s"java.time.temporal.IsoFields.$isoField" + override def painless(context: Option[PainlessContext]): String = + s"java.time.temporal.IsoFields.$isoField" } object IsoField { @@ -82,7 +99,7 @@ package object time { def timeUnit: String = sql.toUpperCase() + "S" - override def painless(): String = s"ChronoUnit.$timeUnit" + override def painless(context: Option[PainlessContext]): String = s"ChronoUnit.$timeUnit" override def nullable: Boolean = false @@ -134,7 +151,8 @@ package object time { def unit: TimeUnit override def sql: String = s"$Interval $value ${unit.sql}" - override def painless(): String = s"$value, ${unit.painless()}" + override def painless(context: Option[PainlessContext]): String = + s"$value, ${unit.painless(context)}" override def script: Option[String] = Some(TimeInterval.script(this)) diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/type/SQLType.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/type/SQLType.scala index b5b28491..a61d4f57 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/type/SQLType.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/type/SQLType.scala @@ -1,3 +1,19 @@ +/* + * 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.`type` sealed trait SQLType { diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/type/SQLTypeUtils.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/type/SQLTypeUtils.scala index 57159f9e..0f580aa3 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/type/SQLTypeUtils.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/type/SQLTypeUtils.scala @@ -1,6 +1,22 @@ +/* + * 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.`type` -import app.softnetwork.elastic.sql.PainlessScript +import app.softnetwork.elastic.sql.{Identifier, LiteralParam, PainlessContext, PainlessScript} import app.softnetwork.elastic.sql.`type`.SQLTypes._ object SQLTypeUtils { @@ -87,14 +103,39 @@ object SQLTypeUtils { SQLTypes.Any } - def coerce(in: PainlessScript, to: SQLType): String = { - val expr = in.painless + def coerce(in: PainlessScript, to: SQLType, context: Option[PainlessContext]): String = { + context match { + case Some(_) => + in match { + case identifier: Identifier => + identifier.baseType match { + case SQLTypes.Any => // in painless context, Any is ZonedDateTime + to match { + case SQLTypes.Date => + identifier.addPainlessMethod(".toLocalDate()") + case SQLTypes.Time => + identifier.addPainlessMethod(".toLocalTime()") + case _ => // do nothing + } + case _ => // do nothing + } + case _ => // do nothing + } + case _ => // do nothing + } + val expr = in.painless(context) val from = in.baseType val nullable = in.nullable - coerce(expr, from, to, nullable) + coerce(expr, from, to, nullable, context) } - def coerce(expr: String, from: SQLType, to: SQLType, nullable: Boolean): String = { + def coerce( + expr: String, + from: SQLType, + to: SQLType, + nullable: Boolean, + context: Option[PainlessContext] + ): String = { val ret = { (from, to) match { // ---- DATE & TIME ---- @@ -149,12 +190,52 @@ object SQLTypeUtils { // ---- VARCHAR -> TEMPORAL ---- case (SQLTypes.Varchar, SQLTypes.Date) => - s"LocalDate.parse($expr, DateTimeFormatter.ofPattern('yyyy-MM-dd'))" + context match { + case Some(ctx) => + ctx.addParam( + LiteralParam(s"LocalDate.parse($expr, DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"))") + ) match { + case Some(p) => return p + case None => // continue + } + case None => // continue + } + s"LocalDate.parse($expr, DateTimeFormatter.ofPattern(\"yyyy-MM-dd\"))" case (SQLTypes.Varchar, SQLTypes.Time) => - s"LocalTime.parse($expr, DateTimeFormatter.ofPattern('HH:mm:ss'))" + context match { + case Some(ctx) => + ctx.addParam( + LiteralParam(s"LocalTime.parse($expr, DateTimeFormatter.ofPattern(\"HH:mm:ss\"))") + ) match { + case Some(p) => return p + case None => // continue + } + case None => // continue + } + s"LocalTime.parse($expr, DateTimeFormatter.ofPattern(\"HH:mm:ss\"))" case (SQLTypes.Varchar, SQLTypes.DateTime) => - s"ZonedDateTime.parse($expr, DateTimeFormatter.ISO_DATE_TIME)" + context match { + case Some(ctx) => + ctx.addParam( + LiteralParam(s"LocalDateTime.parse($expr, DateTimeFormatter.ISO_DATE_TIME)") + ) match { + case Some(p) => return p + case None => // continue + } + case None => // continue + } + s"LocalDateTime.parse($expr, DateTimeFormatter.ISO_DATE_TIME)" case (SQLTypes.Varchar, SQLTypes.Timestamp) => + context match { + case Some(ctx) => + ctx.addParam( + LiteralParam(s"ZonedDateTime.parse($expr, DateTimeFormatter.ISO_ZONED_DATE_TIME)") + ) match { + case Some(p) => return p + case None => // continue + } + case None => // continue + } s"ZonedDateTime.parse($expr, DateTimeFormatter.ISO_ZONED_DATE_TIME)" // ---- IDENTITY ---- diff --git a/sql/src/main/scala/app/softnetwork/elastic/sql/type/SQLTypes.scala b/sql/src/main/scala/app/softnetwork/elastic/sql/type/SQLTypes.scala index 0959ba29..52cca678 100644 --- a/sql/src/main/scala/app/softnetwork/elastic/sql/type/SQLTypes.scala +++ b/sql/src/main/scala/app/softnetwork/elastic/sql/type/SQLTypes.scala @@ -1,3 +1,19 @@ +/* + * 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.`type` object SQLTypes { diff --git a/sql/src/test/scala/app/softnetwork/elastic/sql/SQLDateTimeFunctionSuite.scala b/sql/src/test/scala/app/softnetwork/elastic/sql/SQLDateTimeFunctionSuite.scala index d0834bdf..a9998da9 100644 --- a/sql/src/test/scala/app/softnetwork/elastic/sql/SQLDateTimeFunctionSuite.scala +++ b/sql/src/test/scala/app/softnetwork/elastic/sql/SQLDateTimeFunctionSuite.scala @@ -4,7 +4,6 @@ import org.scalatest.funsuite.AnyFunSuite import app.softnetwork.elastic.sql.function._ import app.softnetwork.elastic.sql.function.time._ import app.softnetwork.elastic.sql.time._ -import TimeField._ import app.softnetwork.elastic.sql.`type`.SQLType class SQLDateTimeFunctionSuite extends AnyFunSuite { @@ -40,7 +39,7 @@ class SQLDateTimeFunctionSuite extends AnyFunSuite { require(transforms.nonEmpty, "No transforms provided") val initial: (String, SQLType) = - (transforms.head.toPainless(base, 0), transforms.head.outputType.asInstanceOf[SQLType]) + (transforms.head.toPainless(base, 0, None), transforms.head.outputType.asInstanceOf[SQLType]) val (finalExpr, _) = transforms.tail.foldLeft(initial) { case ((expr, currentType), t: FunctionN[_, _]) => @@ -49,7 +48,7 @@ class SQLDateTimeFunctionSuite extends AnyFunSuite { s"Type mismatch: expected ${currentType.getClass.getSimpleName}, got ${t.inputType.getClass.getSimpleName}" ) } - (t.toPainless(expr, 0), t.outputType.asInstanceOf[SQLType]) + (t.toPainless(expr, 0, None), t.outputType.asInstanceOf[SQLType]) } finalExpr @@ -81,9 +80,6 @@ class SQLDateTimeFunctionSuite extends AnyFunSuite { val names = chain.map(_.sql).mkString(" -> ") test(s"Valid chain $idx: $names") { val chained = chainTransformsTyped(baseDate, chain) - val expected = chain.reverse.tail.foldLeft(chain.last.toPainless(baseDate, 0)) { (expr, f) => - f.toPainless(expr, 0) - } // On ne teste que la génération de code Painless sans évaluer le résultat assert(chained.nonEmpty) } @@ -92,7 +88,7 @@ class SQLDateTimeFunctionSuite extends AnyFunSuite { // Test simple pour chaque fonction individuelle transformFunctions.foreach { f => test(s"Single transformation ${f.sql}") { - val result = f.toPainless(baseDate, 0) + val result = f.toPainless(baseDate, 0, None) assert(result.nonEmpty) } } diff --git a/sql/src/test/scala/app/softnetwork/elastic/sql/SQLParserSpec.scala b/sql/src/test/scala/app/softnetwork/elastic/sql/SQLParserSpec.scala index c74d48a4..186833e3 100644 --- a/sql/src/test/scala/app/softnetwork/elastic/sql/SQLParserSpec.scala +++ b/sql/src/test/scala/app/softnetwork/elastic/sql/SQLParserSpec.scala @@ -123,8 +123,19 @@ object Queries { val aggregationWithDateDiff = "SELECT MAX(date_diff(datetime_parse(createdAt, '%Y-%m-%d %H:%i:%s.%f'), updatedAt, DAY)) AS max_diff FROM Table GROUP BY identifier" - val dateFormat = - "SELECT identifier, date_format(date_trunc(lastUpdated, MONTH), '%Y-%m-%d') AS lastSeen FROM Table WHERE identifier2 is NOT null" + val dateFormat: String = + """SELECT + |identifier, + |date_format(date_trunc(lastUpdated, YEAR), '%Y-%m-%d') AS y, + |date_format(date_trunc(lastUpdated, QUARTER), '%Y-%m-%d') AS q, + |date_format(date_trunc(lastUpdated, MONTH), '%Y-%m-%d') AS m, + |date_format(date_trunc(lastUpdated, WEEK), '%Y-%m-%d') AS w, + |date_format(date_trunc(lastUpdated, DAY), '%Y-%m-%d') AS d, + |date_format(date_trunc(lastUpdated, HOUR), '%Y-%m-%d') AS h, + |date_format(date_trunc(lastUpdated, MINUTE), '%Y-%m-%d') AS m2, + |date_format(date_trunc(lastUpdated, SECOND), '%Y-%m-%d') AS lastSeen + |FROM Table + |WHERE identifier2 IS NOT NULL""".stripMargin.replaceAll("\n", " ") val dateTimeFormat = "SELECT identifier, datetime_format(date_trunc(lastUpdated, MONTH), '%Y-%m-%d %H:%i:%s') AS lastSeen FROM Table WHERE identifier2 is NOT null" val dateAdd = @@ -826,8 +837,7 @@ class SQLParserSpec extends AnyFlatSpec with Matchers { val result = Parser(mathematical) result.toOption .flatMap(_.left.toOption.map(_.sql)) - .getOrElse("") - .equalsIgnoreCase(mathematical) shouldBe true + .getOrElse("") shouldBe mathematical } it should "parse string functions" in {