Skip to content

Commit 0e154fc

Browse files
authored
Merge pull request #36 from SOFTNETWORK-APP/feature/valueCoercion
- add value coercion - add -W option to REPL
2 parents ec2aeb3 + a3f8eaa commit 0e154fc

7 files changed

Lines changed: 230 additions & 5 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ testkit/target*
2020
*.sc
2121
_bmad*
2222
docker/data
23+
.claude

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ ThisBuild / javaOptions ++= Seq(
6969
"--add-opens=java.base/sun.nio.ch=ALL-UNNAMED"
7070
)
7171

72-
Test / javaOptions ++= (javaOptions.value)
72+
Test / javaOptions ++= javaOptions.value
7373

7474
ThisBuild / resolvers ++= Seq(
7575
"Softnetwork Server" at "https://softnetwork.jfrog.io/artifactory/releases/",

core/src/main/resources/help/commands/ddl/create_table.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
" ...",
1212
" [PRIMARY KEY (column, ...)]",
1313
")",
14-
"[PARTITIONED BY (column granularity)]",
14+
"[PARTITIONED BY column (granularity)]",
1515
"[OPTIONS (",
1616
" [settings = (setting = value, ...)],",
1717
" [mappings = (mapping = value, ...)],",

core/src/main/scala/app/softnetwork/elastic/client/Cli.scala

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ object Cli extends App {
7575
var bearerToken: Option[String] = None
7676
var executeFile: Option[String] = None
7777
var executeCommand: Option[String] = None
78+
var promptPassword = false
7879

7980
var i = 0
8081
while (i < args.length) {
@@ -99,6 +100,10 @@ object Cli extends App {
99100
password = Some(args(i + 1))
100101
i += 2
101102

103+
case "-W" =>
104+
promptPassword = true
105+
i += 1
106+
102107
case "-k" | "--api-key" =>
103108
apiKey = Some(args(i + 1))
104109
i += 2
@@ -126,6 +131,17 @@ object Cli extends App {
126131
}
127132
}
128133

134+
if (promptPassword) {
135+
val console = System.console()
136+
if (console == null) {
137+
System.err.println("Error: -W requires an interactive terminal")
138+
System.exit(1)
139+
}
140+
System.err.print("Enter password: ")
141+
System.err.flush()
142+
password = Some(new String(console.readPassword()))
143+
}
144+
129145
CliConfig(
130146
scheme,
131147
host,
@@ -153,6 +169,7 @@ object Cli extends App {
153169
| -p, --port <port> Elasticsearch port (default: 9200)
154170
| -u, --username <user> Username for authentication
155171
| -P, --password <pass> Password for authentication
172+
| -W Prompt for password interactively (input not echoed)
156173
| -k, --api-key <key> API key for authentication
157174
| -b, --bearer-token <token> Bearer token for authentication
158175
| -f, --file <path> Execute SQL from file and exit

es6/rest/src/main/scala/app/softnetwork/elastic/client/rest/RestHighLevelClientApi.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1607,9 +1607,9 @@ trait RestHighLevelClientScrollApi extends ScrollApi with RestHighLevelClientHel
16071607
Some((nextSearchAfter, hits))
16081608
}
16091609
}
1610-
}(system, logger).recover { case ex: Exception =>
1610+
}(system, logger).recoverWith { case ex: Exception =>
16111611
logger.error(s"Search after failed after retries: ${ex.getMessage}", ex)
1612-
None
1612+
Future.failed(ex)
16131613
}
16141614
}
16151615
.mapConcat(identity)

project/Versions.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ object Versions {
1616

1717
val scalaLogging = "3.9.2"
1818

19-
val logback = "1.2.3"
19+
val logback = "1.5.32"
2020

2121
val slf4j = "1.7.36"
2222

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* Copyright 2025 SOFTNETWORK
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package app.softnetwork.elastic.sql.`type`
18+
19+
/** Runtime value type inference and coercion utilities.
20+
*
21+
* These functions operate on plain Scala/Java runtime values (not Painless scripts) and are
22+
* independent of any JDBC or Arrow-specific API. Both the JDBC driver and the Arrow Flight SQL
23+
* server delegate to this object for type inference and value coercion.
24+
*
25+
* JDBC-specific mappings (`toJdbcType`, `toJdbcTypeName`, `coerceValue`, etc.) remain in the
26+
* `driver` module's `TypeMapping` object, which delegates here for the general-purpose methods.
27+
*/
28+
object ValueCoercion {
29+
30+
// ─── Type inference ─────────────────────────────────────────────────────────
31+
32+
/** Infer the [[SQLType]] from a runtime value. */
33+
def inferType(value: Any): SQLType = value match {
34+
case null => SQLTypes.Null
35+
case _: Int => SQLTypes.Int
36+
case _: Long => SQLTypes.BigInt
37+
case _: Double => SQLTypes.Double
38+
case _: Float => SQLTypes.Real
39+
case _: Boolean => SQLTypes.Boolean
40+
case _: Short => SQLTypes.SmallInt
41+
case _: Byte => SQLTypes.TinyInt
42+
case _: java.math.BigDecimal => SQLTypes.Double
43+
case _: BigDecimal => SQLTypes.Double
44+
case _: java.sql.Date => SQLTypes.Date
45+
case _: java.sql.Time => SQLTypes.Time
46+
case _: java.sql.Timestamp => SQLTypes.Timestamp
47+
case _: java.time.LocalDate => SQLTypes.Date
48+
case _: java.time.LocalTime => SQLTypes.Time
49+
case _: java.time.LocalDateTime => SQLTypes.Timestamp
50+
case _: java.time.Instant => SQLTypes.Timestamp
51+
case _: java.time.ZonedDateTime => SQLTypes.Timestamp
52+
case _: java.time.temporal.TemporalAccessor => SQLTypes.Timestamp
53+
case _: Seq[_] => SQLTypes.Array(SQLTypes.Any)
54+
case _: Map[_, _] => SQLTypes.Struct
55+
case _: Array[Byte] => SQLTypes.VarBinary
56+
case _: String => SQLTypes.Varchar
57+
case _: Number => SQLTypes.Double
58+
case _ => SQLTypes.Varchar
59+
}
60+
61+
// ─── Coercions ───────────────────────────────────────────────────────────────
62+
63+
def coerceToString(value: Any): String = value match {
64+
case null => null
65+
case s: String => s
66+
case seq: Seq[_] => seq.mkString("[", ", ", "]")
67+
case map: Map[_, _] => map.map { case (k, v) => s"$k: $v" }.mkString("{", ", ", "}")
68+
case other => other.toString
69+
}
70+
71+
def coerceToInt(value: Any): java.lang.Integer = value match {
72+
case null => null
73+
case n: Number => n.intValue()
74+
case s: String => java.lang.Integer.valueOf(s)
75+
case b: Boolean => if (b) 1 else 0
76+
case _ =>
77+
throw new java.sql.SQLException(s"Cannot convert ${value.getClass.getName} to INT")
78+
}
79+
80+
def coerceToLong(value: Any): java.lang.Long = value match {
81+
case null => null
82+
case n: Number => n.longValue()
83+
case s: String => java.lang.Long.valueOf(s)
84+
case b: Boolean => if (b) 1L else 0L
85+
case _ =>
86+
throw new java.sql.SQLException(s"Cannot convert ${value.getClass.getName} to BIGINT")
87+
}
88+
89+
def coerceToDouble(value: Any): java.lang.Double = value match {
90+
case null => null
91+
case n: Number => n.doubleValue()
92+
case s: String => java.lang.Double.valueOf(s)
93+
case _ =>
94+
throw new java.sql.SQLException(s"Cannot convert ${value.getClass.getName} to DOUBLE")
95+
}
96+
97+
def coerceToFloat(value: Any): java.lang.Float = value match {
98+
case null => null
99+
case n: Number => n.floatValue()
100+
case s: String => java.lang.Float.valueOf(s)
101+
case _ =>
102+
throw new java.sql.SQLException(s"Cannot convert ${value.getClass.getName} to REAL")
103+
}
104+
105+
def coerceToBoolean(value: Any): java.lang.Boolean = value match {
106+
case null => null
107+
case b: Boolean => b
108+
case n: Number => n.intValue() != 0
109+
case s: String => java.lang.Boolean.valueOf(s)
110+
case _ =>
111+
throw new java.sql.SQLException(s"Cannot convert ${value.getClass.getName} to BOOLEAN")
112+
}
113+
114+
def coerceToByte(value: Any): java.lang.Byte = value match {
115+
case null => null
116+
case n: Number => n.byteValue()
117+
case s: String => java.lang.Byte.valueOf(s)
118+
case _ =>
119+
throw new java.sql.SQLException(s"Cannot convert ${value.getClass.getName} to TINYINT")
120+
}
121+
122+
def coerceToShort(value: Any): java.lang.Short = value match {
123+
case null => null
124+
case n: Number => n.shortValue()
125+
case s: String => java.lang.Short.valueOf(s)
126+
case _ =>
127+
throw new java.sql.SQLException(s"Cannot convert ${value.getClass.getName} to SMALLINT")
128+
}
129+
130+
def coerceToBigDecimal(value: Any): java.math.BigDecimal = value match {
131+
case null => null
132+
case bd: java.math.BigDecimal => bd
133+
case bd: BigDecimal => bd.bigDecimal
134+
case n: Number => java.math.BigDecimal.valueOf(n.doubleValue())
135+
case s: String => new java.math.BigDecimal(s)
136+
case _ =>
137+
throw new java.sql.SQLException(s"Cannot convert ${value.getClass.getName} to DECIMAL")
138+
}
139+
140+
def coerceToDate(value: Any): java.sql.Date = value match {
141+
case null => null
142+
case d: java.sql.Date => d
143+
case ts: java.sql.Timestamp => new java.sql.Date(ts.getTime)
144+
case ld: java.time.LocalDate => java.sql.Date.valueOf(ld)
145+
case ldt: java.time.LocalDateTime => java.sql.Date.valueOf(ldt.toLocalDate)
146+
case zdt: java.time.ZonedDateTime => java.sql.Date.valueOf(zdt.toLocalDate)
147+
case i: java.time.Instant => new java.sql.Date(i.toEpochMilli)
148+
case t: java.time.temporal.TemporalAccessor =>
149+
try {
150+
java.sql.Date.valueOf(java.time.LocalDate.from(t))
151+
} catch {
152+
case _: Exception => throw new java.sql.SQLException("Cannot convert temporal to DATE")
153+
}
154+
case s: String =>
155+
try { java.sql.Date.valueOf(s) }
156+
catch { case _: Exception => throw new java.sql.SQLException(s"Cannot parse '$s' as DATE") }
157+
case _ =>
158+
throw new java.sql.SQLException(s"Cannot convert ${value.getClass.getName} to DATE")
159+
}
160+
161+
def coerceToTime(value: Any): java.sql.Time = value match {
162+
case null => null
163+
case t: java.sql.Time => t
164+
case lt: java.time.LocalTime => java.sql.Time.valueOf(lt)
165+
case ldt: java.time.LocalDateTime => java.sql.Time.valueOf(ldt.toLocalTime)
166+
case s: String =>
167+
try { java.sql.Time.valueOf(s) }
168+
catch { case _: Exception => throw new java.sql.SQLException(s"Cannot parse '$s' as TIME") }
169+
case _ =>
170+
throw new java.sql.SQLException(s"Cannot convert ${value.getClass.getName} to TIME")
171+
}
172+
173+
def coerceToTimestamp(value: Any): java.sql.Timestamp = value match {
174+
case null => null
175+
case ts: java.sql.Timestamp => ts
176+
case d: java.sql.Date => new java.sql.Timestamp(d.getTime)
177+
case i: java.time.Instant => java.sql.Timestamp.from(i)
178+
case ldt: java.time.LocalDateTime => java.sql.Timestamp.valueOf(ldt)
179+
case zdt: java.time.ZonedDateTime => java.sql.Timestamp.from(zdt.toInstant)
180+
case ld: java.time.LocalDate => java.sql.Timestamp.valueOf(ld.atStartOfDay())
181+
case t: java.time.temporal.TemporalAccessor =>
182+
try {
183+
java.sql.Timestamp.from(java.time.Instant.from(t))
184+
} catch {
185+
case _: Exception =>
186+
try {
187+
java.sql.Timestamp.valueOf(java.time.LocalDateTime.from(t))
188+
} catch {
189+
case _: Exception =>
190+
throw new java.sql.SQLException("Cannot convert temporal to TIMESTAMP")
191+
}
192+
}
193+
case s: String =>
194+
try { java.sql.Timestamp.valueOf(s) }
195+
catch {
196+
case _: Exception =>
197+
try { java.sql.Timestamp.from(java.time.Instant.parse(s)) }
198+
catch {
199+
case _: Exception =>
200+
throw new java.sql.SQLException(s"Cannot parse '$s' as TIMESTAMP")
201+
}
202+
}
203+
case n: Number => new java.sql.Timestamp(n.longValue())
204+
case _ =>
205+
throw new java.sql.SQLException(s"Cannot convert ${value.getClass.getName} to TIMESTAMP")
206+
}
207+
}

0 commit comments

Comments
 (0)