diff --git a/.gitignore b/.gitignore index 8a26121..97dc336 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,23 @@ target/ adapters/file/data/files/2022CbpOceanEconomyTable__2022CbpIaOceanEconomy.json adapters/file/data/files/2022CbpOceanEconomyTable__Notes.json /grafana/tempo-data/ + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates +*.sln.ide/ + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ diff --git a/calcite-rs-jni/odbc/.gitignore b/calcite-rs-jni/odbc/.gitignore new file mode 100644 index 0000000..0ca9d2f --- /dev/null +++ b/calcite-rs-jni/odbc/.gitignore @@ -0,0 +1,19 @@ +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates +*.sln.ide/ + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ diff --git a/calcite-rs-jni/odbc/DDN-ODBC-Tester/DDN-ODBC-Tester.csproj b/calcite-rs-jni/odbc/DDN-ODBC-Tester/DDN-ODBC-Tester.csproj new file mode 100644 index 0000000..b4bb47d --- /dev/null +++ b/calcite-rs-jni/odbc/DDN-ODBC-Tester/DDN-ODBC-Tester.csproj @@ -0,0 +1,12 @@ + + + Exe + net8.0 + DDN_ODBC_Tester + enable + enable + + + + + diff --git a/calcite-rs-jni/odbc/DDN-ODBC-Tester/Program.cs b/calcite-rs-jni/odbc/DDN-ODBC-Tester/Program.cs new file mode 100644 index 0000000..439ea0c --- /dev/null +++ b/calcite-rs-jni/odbc/DDN-ODBC-Tester/Program.cs @@ -0,0 +1,215 @@ +using System; +using System.Data; +using DDN_ODBC; + +namespace DDN_ODBC_Tester +{ + class OdbcMetadataExample + { + static void Main() + { + string connectionString = "jdbc:graphql:http://localhost:3280/graphql"; + + try + { + using (DDN_ODBC.DDN_ODBC connection = new DDN_ODBC.DDN_ODBC(connectionString)) + { + connection.Open(); + Console.WriteLine("Connected successfully.\n"); + + // Get Tables + Console.WriteLine("=== Tables ==="); + using (IDbCommand command = connection.CreateCommand()) + { + command.CommandText = "SQLTables"; + + using (IDataReader reader = command.ExecuteReader()) + { + // Print column names + for (int i = 0; i < reader.FieldCount; i++) + { + Console.Write($"{reader.GetName(i)}\t"); + } + Console.WriteLine(); + + // Print data + while (reader.Read()) + { + for (int i = 0; i < reader.FieldCount; i++) + { + Console.Write($"{reader[i]}\t"); + } + Console.WriteLine(); + } + } + } + + // Get Tables Parameterized + Console.WriteLine("=== Tables ==="); + using (IDbCommand command = connection.CreateCommand()) + { + command.CommandText = "{ CALL SQLTables(?, ?, ?, ?) }"; + + // Adding parameters (catalog, schema, table, table type) + // Create parameters + var param1 = new DDNDataParameter("@catalog", DbType.String) { Value = DBNull.Value }; + var param2 = new DDNDataParameter("@schema", DbType.String) { Value = DBNull.Value }; + var param3 = new DDNDataParameter("@table", DbType.String) { Value = DBNull.Value }; + var param4 = new DDNDataParameter("@tableType", DbType.String) { Value = "TABLE" }; + + // Add parameters + command.Parameters.Add(param1); + command.Parameters.Add(param2); + command.Parameters.Add(param3); + command.Parameters.Add(param4); + + using (IDataReader reader = command.ExecuteReader()) + { + // Print column names + for (int i = 0; i < reader.FieldCount; i++) + { + Console.Write($"{reader.GetName(i)}\t"); + } + Console.WriteLine(); + + // Print data + while (reader.Read()) + { + for (int i = 0; i < reader.FieldCount; i++) + { + Console.Write($"{reader[i]}\t"); + } + Console.WriteLine(); + } + } + } + + // Get Columns + Console.WriteLine("\n=== Columns for YourTable ==="); + using (IDbCommand command = connection.CreateCommand()) + { + command.CommandText = "SQLColumns"; + + using (IDataReader reader = command.ExecuteReader()) + { + // Print column names + for (int i = 0; i < reader.FieldCount; i++) + { + Console.Write($"{reader.GetName(i)}\t"); + } + Console.WriteLine(); + + // Print data + while (reader.Read()) + { + for (int i = 0; i < reader.FieldCount; i++) + { + Console.Write($"{reader[i]}\t"); + } + Console.WriteLine(); + } + } + } + + // Get Columns + Console.WriteLine("\n=== Columns for YourTable ==="); + using (IDbCommand command = connection.CreateCommand()) + { + command.CommandText = "{ CALL SQLColumns(?, ?, ?, ?) }"; + + // Adding parameters (catalog, schema, table, table type) + // Create parameters + var param1 = new DDNDataParameter("@catalog", DbType.String) { Value = DBNull.Value }; + var param2 = new DDNDataParameter("@schema", DbType.String) { Value = DBNull.Value }; + var param3 = new DDNDataParameter("@table", DbType.String) { Value = DBNull.Value }; + var param4 = new DDNDataParameter("@column", DbType.String) { Value = "fi%" }; + + // Add parameters + command.Parameters.Add(param1); + command.Parameters.Add(param2); + command.Parameters.Add(param3); + command.Parameters.Add(param4); + + using (IDataReader reader = command.ExecuteReader()) + { + // Print column names + for (int i = 0; i < reader.FieldCount; i++) + { + Console.Write($"{reader.GetName(i)}\t"); + } + Console.WriteLine(); + + // Print data + while (reader.Read()) + { + for (int i = 0; i < reader.FieldCount; i++) + { + Console.Write($"{reader[i]}\t"); + } + Console.WriteLine(); + } + } + } + + // Get Columns + Console.WriteLine("\n=== Plain old SQL for Customer Table ==="); + using (IDbCommand command = connection.CreateCommand()) + { + command.CommandText = "SELECT * FROM Customer"; + + using (IDataReader reader = command.ExecuteReader()) + { + // Print column names + for (int i = 0; i < reader.FieldCount; i++) + { + Console.Write($"{reader.GetName(i)}\t"); + } + Console.WriteLine(); + + // Print data + while (reader.Read()) + { + for (int i = 0; i < reader.FieldCount; i++) + { + Console.Write($"{reader[i]}\t"); + } + Console.WriteLine(); + } + } + } + + // Get Primary Keys + Console.WriteLine("\n=== Primary Keys ==="); + using (IDbCommand command = connection.CreateCommand()) + { + command.CommandText = "SQLPrimaryKeys"; + + using (IDataReader reader = command.ExecuteReader()) + { + // Print column names + for (int i = 0; i < reader.FieldCount; i++) + { + Console.Write($"{reader.GetName(i)}\t"); + } + Console.WriteLine(); + + // Print data + while (reader.Read()) + { + for (int i = 0; i < reader.FieldCount; i++) + { + Console.Write($"{reader[i]}\t"); + } + Console.WriteLine(); + } + } + } + } + } + catch (Exception ex) + { + Console.WriteLine($"\nError: {ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/calcite-rs-jni/odbc/DDN-ODBC.sln b/calcite-rs-jni/odbc/DDN-ODBC.sln new file mode 100644 index 0000000..d6de259 --- /dev/null +++ b/calcite-rs-jni/odbc/DDN-ODBC.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDN-ODBC-Tester", "DDN-ODBC-Tester\DDN-ODBC-Tester.csproj", "{BD626E9E-0270-49A1-8634-82513B607B30}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDN-ODBC", "DDN-ODBC\DDN-ODBC.csproj", "{F0F0EDBF-19F1-4B16-B1CE-45F86526ABAD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BD626E9E-0270-49A1-8634-82513B607B30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD626E9E-0270-49A1-8634-82513B607B30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD626E9E-0270-49A1-8634-82513B607B30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD626E9E-0270-49A1-8634-82513B607B30}.Release|Any CPU.Build.0 = Release|Any CPU + {F0F0EDBF-19F1-4B16-B1CE-45F86526ABAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0F0EDBF-19F1-4B16-B1CE-45F86526ABAD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0F0EDBF-19F1-4B16-B1CE-45F86526ABAD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0F0EDBF-19F1-4B16-B1CE-45F86526ABAD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C87A2184-733D-4127-86EE-CDF175E7BDE5} + EndGlobalSection +EndGlobal diff --git a/calcite-rs-jni/odbc/DDN-ODBC/CommandParser.cs b/calcite-rs-jni/odbc/DDN-ODBC/CommandParser.cs new file mode 100644 index 0000000..b115047 --- /dev/null +++ b/calcite-rs-jni/odbc/DDN-ODBC/CommandParser.cs @@ -0,0 +1,74 @@ +using System.Text.RegularExpressions; + +namespace DDN_ODBC; + +public class CommandParser +{ + private static readonly Regex CallCommandRegex = new(@"{\s*CALL\s+(?\w+)\s*\((?[^)]*)\)\s*}", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex SimpleCommandRegex = new(@"^(?\w+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private static readonly Regex ParameterRegex = new(@"\?", RegexOptions.Compiled); + + private readonly HashSet _validCommands; + + public CommandParser(IEnumerable validCommands) + { + _validCommands = new HashSet(validCommands, StringComparer.OrdinalIgnoreCase); + } + + public CommandParser() + { + var validCommands = new List + { + "SQLTables", + "SQLColumns", + "SQLProcedures", + "SQLProcedureColumns", + "SQLPrimaryKeys", + "SQLForeignKeys", + "SQLStatistics", + "SQLSpecialColumns", + "SQLGetTypeInfo", + "SQLTablePrivileges", + "SQLColumnPrivileges", + "SQLGetFunctions" + }; + _validCommands = new HashSet(validCommands, StringComparer.OrdinalIgnoreCase); + } + + public bool TryParseCommand(string commandText, out string command, out int parameterCount) + { + command = null; + parameterCount = 0; + + // Try to match the CALL command format + var match = CallCommandRegex.Match(commandText); + if (match.Success) + { + command = match.Groups["command"].Value; + if (!_validCommands.Contains(command)) + { + return false; + } + + string paramsGroup = match.Groups["params"].Value; + parameterCount = 0; + if (!string.IsNullOrEmpty(paramsGroup)) + { + parameterCount = ParameterRegex.Matches(paramsGroup).Count; + } + + return true; + } + + // Try to match the simple command format + match = SimpleCommandRegex.Match(commandText); + if (match.Success) + { + command = match.Groups["command"].Value; + return _validCommands.Contains(command); + } + + // No match + return false; + } +} \ No newline at end of file diff --git a/calcite-rs-jni/odbc/DDN-ODBC/DDN-ODBC.csproj b/calcite-rs-jni/odbc/DDN-ODBC/DDN-ODBC.csproj new file mode 100644 index 0000000..f71683a --- /dev/null +++ b/calcite-rs-jni/odbc/DDN-ODBC/DDN-ODBC.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + DDN_ODBC + enable + enable + + + + + + + ./Resources/sqlengine-1.0.0-jar-with-dependencies.jar + + + + + + + + diff --git a/calcite-rs-jni/odbc/DDN-ODBC/DDNColumnMetadata.cs b/calcite-rs-jni/odbc/DDN-ODBC/DDNColumnMetadata.cs new file mode 100644 index 0000000..d6bcd38 --- /dev/null +++ b/calcite-rs-jni/odbc/DDN-ODBC/DDNColumnMetadata.cs @@ -0,0 +1,96 @@ +namespace DDN_ODBC; +using Newtonsoft.Json; +public class DDNColumnMetadata +{ + [JsonProperty("tableCat")] + public string? TableCatalog { get; set; } + + [JsonProperty("tableSchem")] + public string? TableSchema { get; set; } + + [JsonProperty("tableName")] + public string? TableName { get; set; } + + [JsonProperty("columnName")] + public string? ColumnName { get; set; } + + [JsonProperty("dataType")] + public int DataType { get; set; } + + [JsonProperty("typeName")] + public string? TypeName { get; set; } + + [JsonProperty("columnSize")] + public int ColumnSize { get; set; } + + [JsonProperty("bufferLength")] + public int? BufferLength { get; set; } + + [JsonProperty("decimalDigits")] + public int? DecimalDigits { get; set; } + + [JsonProperty("numPrecRadix")] + public int NumPrecRadix { get; set; } + + [JsonProperty("nullable")] + public int Nullable { get; set; } + + [JsonProperty("remarks")] + public string? Remarks { get; set; } + + [JsonProperty("columnDef")] + public string? ColumnDefault { get; set; } + + [JsonProperty("sqlDataType")] + public int? SqlDataType { get; set; } + + [JsonProperty("sqlDatetimeSub")] + public int? SqlDatetimeSub { get; set; } + + [JsonProperty("charOctetLength")] + public int CharOctetLength { get; set; } + + [JsonProperty("ordinalPosition")] + public int OrdinalPosition { get; set; } + + [JsonProperty("isNullable")] + public string? IsNullable { get; set; } + + [JsonProperty("scopeCatalog")] + public string? ScopeCatalog { get; set; } + + [JsonProperty("scopeSchema")] + public string? ScopeSchema { get; set; } + + [JsonProperty("scopeTable")] + public string? ScopeTable { get; set; } + + [JsonProperty("sourceDataType")] + public short? SourceDataType { get; set; } + + [JsonProperty("isAutoincrement")] + public string? IsAutoincrement { get; set; } + + [JsonProperty("isGeneratedcolumn")] + public string? IsGeneratedColumn { get; set; } +} + +// Usage example: +/* +using System.Text.Json; + +// Deserialize JSON +var columnMetadata = JsonSerializer.Deserialize(jsonString); + +// Get ODBC type information +short odbcType = columnMetadata.GetOdbcType(); +string odbcTypeName = columnMetadata.GetOdbcTypeName(); + +// Check type categories +bool isString = columnMetadata.IsStringType(); +bool isNumeric = columnMetadata.IsNumericType(); +bool isTemporal = columnMetadata.IsTemporalType(); + +// Work with nullability +bool isNullable = columnMetadata.Nullable == ColumnMetadata.SQL_NULLABLE; +*/ \ No newline at end of file diff --git a/calcite-rs-jni/odbc/DDN-ODBC/DDNDataParameter.cs b/calcite-rs-jni/odbc/DDN-ODBC/DDNDataParameter.cs new file mode 100644 index 0000000..61bcb01 --- /dev/null +++ b/calcite-rs-jni/odbc/DDN-ODBC/DDNDataParameter.cs @@ -0,0 +1,76 @@ +using System.Data; +using System.Data.Common; + +namespace DDN_ODBC; + +public class DDNDataParameter : DbParameter +{ + private string _parameterName = string.Empty; + private string _sourceColumn = string.Empty; + private object? _value; + private DbType _dbType = DbType.String; + private ParameterDirection _direction = ParameterDirection.Input; // Since read-only + private int _size; + + public DDNDataParameter(string parameterName, DbType dbType) + { + ParameterName = parameterName; + DbType = dbType; + } + + public override DbType DbType + { + get => _dbType; + set => _dbType = value; + } + + public override ParameterDirection Direction + { + get => _direction; + set + { + if (value != ParameterDirection.Input) + throw new NotSupportedException("Only input parameters are supported in read-only mode."); + _direction = value; + } + } + + public override string ParameterName + { + get => _parameterName; + set => _parameterName = value ?? string.Empty; + } + + public override string SourceColumn + { + get => _sourceColumn; + set => _sourceColumn = value ?? string.Empty; + } + + public override bool SourceColumnNullMapping { get; set; } + + public override DataRowVersion SourceVersion { get; set; } = DataRowVersion.Current; + + public override object? Value + { + get => _value; + set => _value = value; + } + + public override int Size + { + get => _size; + set + { + if (value < 0) + throw new ArgumentException("Size must be non-negative.", nameof(value)); + _size = value; + } + } + + public override bool IsNullable { get; set; } + + public override void ResetDbType() => _dbType = DbType.String; + + public override string ToString() => ParameterName; +} \ No newline at end of file diff --git a/calcite-rs-jni/odbc/DDN-ODBC/DDNDataParameterCollection.cs b/calcite-rs-jni/odbc/DDN-ODBC/DDNDataParameterCollection.cs new file mode 100644 index 0000000..0f4d7d1 --- /dev/null +++ b/calcite-rs-jni/odbc/DDN-ODBC/DDNDataParameterCollection.cs @@ -0,0 +1,97 @@ +using System.Collections; +using System.Data; + +namespace DDN_ODBC; + +public class DDNDataParameterCollection : IDataParameterCollection +{ + private readonly List _parameters = new(); + + public object this[string parameterName] + { + get => _parameters.Find(p => p.ParameterName == parameterName); + set + { + var index = _parameters.FindIndex(p => p.ParameterName == parameterName); + if (index != -1) + _parameters[index] = (DDNDataParameter)value; + else + _parameters.Add((DDNDataParameter)value); + } + } + + public object this[int index] + { + get => _parameters[index]; + set => _parameters[index] = (DDNDataParameter)value; + } + + public bool Contains(string parameterName) + { + return _parameters.Exists(p => p.ParameterName == parameterName); + } + + public int IndexOf(string parameterName) + { + return _parameters.FindIndex(p => p.ParameterName == parameterName); + } + + public void RemoveAt(string parameterName) + { + var index = IndexOf(parameterName); + if (index != -1) + _parameters.RemoveAt(index); + } + + public int Count => _parameters.Count; + public bool IsReadOnly => false; + public bool IsFixedSize => false; + public bool IsSynchronized => false; + public object SyncRoot => null; + + public int Add(object value) + { + _parameters.Add((DDNDataParameter)value); + return _parameters.Count - 1; + } + + public void Clear() + { + _parameters.Clear(); + } + + public bool Contains(object value) + { + return _parameters.Contains((DDNDataParameter)value); + } + + public int IndexOf(object value) + { + return _parameters.IndexOf((DDNDataParameter)value); + } + + public void Insert(int index, object value) + { + _parameters.Insert(index, (DDNDataParameter)value); + } + + public void Remove(object value) + { + _parameters.Remove((DDNDataParameter)value); + } + + public void RemoveAt(int index) + { + _parameters.RemoveAt(index); + } + + public void CopyTo(Array array, int index) + { + _parameters.CopyTo((DDNDataParameter[])array, index); + } + + public IEnumerator GetEnumerator() + { + return _parameters.GetEnumerator(); + } +} \ No newline at end of file diff --git a/calcite-rs-jni/odbc/DDN-ODBC/DDNParameterCollection.cs b/calcite-rs-jni/odbc/DDN-ODBC/DDNParameterCollection.cs new file mode 100644 index 0000000..5297907 --- /dev/null +++ b/calcite-rs-jni/odbc/DDN-ODBC/DDNParameterCollection.cs @@ -0,0 +1,89 @@ +using System.Collections; +using System.Data; +using System.Data.Common; + +namespace DDN_ODBC; + +public class DDNParameterCollection : DbParameterCollection +{ + private readonly List _parameters = new(); + + public override int Count => _parameters.Count; + + public override object SyncRoot => ((ICollection)_parameters).SyncRoot; + + public override int Add(object value) + { + if (value is not DbParameter parameter) + throw new ArgumentException("Value must be a DbParameter"); + + _parameters.Add(parameter); + return _parameters.Count - 1; + } + + public override void AddRange(Array values) + { + foreach (var value in values) + Add(value); + } + + public override void Clear() => _parameters.Clear(); + + public override bool Contains(object value) => + value is DbParameter parameter && _parameters.Contains(parameter); + + public override bool Contains(string value) => + _parameters.Any(p => string.Equals(p.ParameterName, value, StringComparison.OrdinalIgnoreCase)); + + public override void CopyTo(Array array, int index) => + ((ICollection)_parameters).CopyTo(array, index); + + public override IEnumerator GetEnumerator() => _parameters.GetEnumerator(); + + public override int IndexOf(object value) => + value is DbParameter parameter ? _parameters.IndexOf(parameter) : -1; + + public override int IndexOf(string parameterName) => + _parameters.FindIndex(p => string.Equals(p.ParameterName, parameterName, StringComparison.OrdinalIgnoreCase)); + + public override void Insert(int index, object value) + { + if (value is not DbParameter parameter) + throw new ArgumentException("Value must be a DbParameter"); + _parameters.Insert(index, parameter); + } + + public override void Remove(object value) + { + if (value is DbParameter parameter) + _parameters.Remove(parameter); + } + + public override void RemoveAt(int index) => _parameters.RemoveAt(index); + + public override void RemoveAt(string parameterName) + { + var index = IndexOf(parameterName); + if (index >= 0) + RemoveAt(index); + } + + protected override DbParameter GetParameter(int index) => _parameters[index]; + + protected override DbParameter GetParameter(string parameterName) => + _parameters.FirstOrDefault(p => string.Equals(p.ParameterName, parameterName, StringComparison.OrdinalIgnoreCase)) + ?? throw new ArgumentException($"Parameter '{parameterName}' not found.", nameof(parameterName)); + + protected override void SetParameter(int index, DbParameter value) => + _parameters[index] = value ?? throw new ArgumentNullException(nameof(value)); + + protected override void SetParameter(string parameterName, DbParameter value) + { + if (value == null) throw new ArgumentNullException(nameof(value)); + var index = IndexOf(parameterName); + if (index >= 0) + _parameters[index] = value; + else + Add(value); + } +} \ No newline at end of file diff --git a/calcite-rs-jni/odbc/DDN-ODBC/DDNTableMetadata.cs b/calcite-rs-jni/odbc/DDN-ODBC/DDNTableMetadata.cs new file mode 100644 index 0000000..3e7d4d5 --- /dev/null +++ b/calcite-rs-jni/odbc/DDN-ODBC/DDNTableMetadata.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace DDN_ODBC; + +public class DDNTableMetadata +{ + [JsonProperty("tableSchem")] public string TABLE_SCHEM { get; set; } + + [JsonProperty("tableType")] public string TABLE_TYPE { get; set; } + + [JsonProperty("tableName")] public string TABLE_NAME { get; set; } + + [JsonProperty("tableCat")] public string TABLE_CAT { get; set; } + + [JsonProperty("remarks")] public string REMARKS { get; set; } +} \ No newline at end of file diff --git a/calcite-rs-jni/odbc/DDN-ODBC/DDN_ODBC.cs b/calcite-rs-jni/odbc/DDN-ODBC/DDN_ODBC.cs new file mode 100644 index 0000000..885509b --- /dev/null +++ b/calcite-rs-jni/odbc/DDN-ODBC/DDN_ODBC.cs @@ -0,0 +1,499 @@ +using System.Data; +using System.Data.Common; +using System.Diagnostics; +using System.Reflection; +using System.Text; +using Newtonsoft.Json; + +namespace DDN_ODBC; + +using System; +public class DDN_ODBC : DbConnection +{ + private readonly CancellationTokenSource _cancellationTokenSource = new(); + private readonly HttpClient _httpClient; + private readonly int _javaAppPort; + private readonly string _javaDDNSQLEngine; + private string _connectionString; + private Process _javaProcess; + private ConnectionState _state = ConnectionState.Closed; + + public DDN_ODBC(string connectionString) + { + _connectionString = connectionString; + _javaDDNSQLEngine = ExtractEmbeddedJar(); + _javaAppPort = GetRandomPort(); + _httpClient = new HttpClient + { + Timeout = TimeSpan.FromSeconds(30) + }; + } + + private string ExtractEmbeddedJar() + { + var assembly = Assembly.GetExecutingAssembly(); + var resourceName = assembly.GetManifestResourceNames() + .Single(str => str.EndsWith("sqlengine-1.0.0-jar-with-dependencies.jar")); + + // Create temp directory if it doesn't exist + var tempPath = Path.Combine(Path.GetTempPath(), "DDN_ODBC"); + Directory.CreateDirectory(tempPath); + + var jarPath = Path.Combine(tempPath, "sqlengine-1.0.0-jar-with-dependencies.jar"); + + // Only extract if it doesn't exist + if (!File.Exists(jarPath)) + { + using (var stream = assembly.GetManifestResourceStream(resourceName)) + using (var file = new FileStream(jarPath, FileMode.Create, FileAccess.Write)) + { + stream.CopyTo(file); + } + } + + return jarPath; + } + + #region IDisposable Implementation + + protected override void Dispose(bool disposing) + { + if (disposing) + try + { + // Cancel any pending operations + _cancellationTokenSource.Cancel(); + + // Close the connection if it's open + if (_state != ConnectionState.Closed) + Close(); + + // Dispose of managed resources + _cancellationTokenSource.Dispose(); + _httpClient.Dispose(); + _javaProcess?.Dispose(); + } + catch + { + // Log disposal errors if you have logging + } + + base.Dispose(disposing); + } + + #endregion + + #region Required DbConnection Implementation + + public override string ConnectionString + { + get => _connectionString; + set + { + if (_state != ConnectionState.Closed) + throw new InvalidOperationException("Cannot change connection string while connection is open."); + _connectionString = value; + } + } + + public override string Database => string.Empty; // Or return actual database name if applicable + + public override string DataSource => $"http://localhost:{_javaAppPort}"; + + public override string ServerVersion => "1.0.0"; // Return your actual server version + + public override ConnectionState State => _state; + + protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) + { + throw new NotSupportedException("Transactions are not supported in read-only mode."); + } + + public override void ChangeDatabase(string databaseName) + { + throw new NotSupportedException("Changing database is not supported."); + } + + protected override DbCommand CreateDbCommand() + { + if (_state != ConnectionState.Open) + throw new InvalidOperationException("Cannot create command on closed connection."); + return new DDN_OdbcCommand(this); + } + + #endregion + + #region Open/Close Implementation + + public override void Open() + { + if (_state == ConnectionState.Open) + return; + + try + { + StartJavaApplication(); + + // Wait for the server to start (with timeout) + var startTime = DateTime.Now; + var timeout = TimeSpan.FromSeconds(30); + var success = false; + + while (DateTime.Now - startTime < timeout) + try + { + var response = GetRequest("/health"); + if (response.Contains("OK")) // Adjust based on your health check response + { + success = true; + break; + } + } + catch + { + // Server might not be ready yet + Thread.Sleep(100); + } + + if (!success) + throw new InvalidOperationException("Failed to start Java application server."); + + _state = ConnectionState.Open; + } + catch (Exception ex) + { + _state = ConnectionState.Closed; + throw new InvalidOperationException("Failed to open connection.", ex); + } + } + + public override async Task OpenAsync(CancellationToken cancellationToken) + { + if (_state == ConnectionState.Open) + return; + + try + { + await StartJavaApplicationAsync(cancellationToken); + + // Wait for the server to start (with timeout) + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + _cancellationTokenSource.Token); + + var startTime = DateTime.Now; + var timeout = TimeSpan.FromSeconds(30); + var success = false; + + while (DateTime.Now - startTime < timeout) + try + { + linkedCts.Token.ThrowIfCancellationRequested(); + var response = await GetRequestAsync("/health", linkedCts.Token); + if (response.Contains("OK")) // Adjust based on your health check response + { + success = true; + break; + } + } + catch (OperationCanceledException) + { + throw; + } + catch + { + // Server might not be ready yet + await Task.Delay(100, linkedCts.Token); + } + + if (!success) + throw new InvalidOperationException("Failed to start Java application server."); + + _state = ConnectionState.Open; + } + catch (Exception ex) + { + _state = ConnectionState.Closed; + if (ex is OperationCanceledException) + throw; + throw new InvalidOperationException("Failed to open connection.", ex); + } + } + + public override void Close() + { + if (_state == ConnectionState.Closed) + return; + + try + { + _cancellationTokenSource.Cancel(); + TerminateJavaApplication(); + } + finally + { + _state = ConnectionState.Closed; + } + } + + #endregion + + #region HTTP Request Helpers + + private async Task GetRequestAsync(string action, CancellationToken cancellationToken) + { + try + { + var uri = new Uri($"http://localhost:{_javaAppPort}{action}"); + var response = await _httpClient.GetAsync(uri, cancellationToken); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(cancellationToken); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to execute GET request to {action}", ex); + } + } + + private string GetRequest(string action) + { + try + { + var uri = new Uri($"http://localhost:{_javaAppPort}{action}"); + var response = _httpClient.GetAsync(uri).Result; + response.EnsureSuccessStatusCode(); + return response.Content.ReadAsStringAsync().Result; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to execute GET request to {action}", ex); + } + } + + public async Task PostRequestAsync(string action, string postData, CancellationToken cancellationToken) + { + try + { + var query = new SqlQuery + { + Sql = postData, + DisallowMutations = true // Force read-only mode + }; + + var jsonString = JsonConvert.SerializeObject(query); + var content = new StringContent(jsonString, Encoding.UTF8, "application/json"); + var uri = new Uri($"http://localhost:{_javaAppPort}{action}"); + + var response = await _httpClient.PostAsync(uri, content, cancellationToken); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync(cancellationToken); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to execute POST request to {action}", ex); + } + } + + public async Task GetTablesAsync(string wherePhrase = "*", CancellationToken cancellationToken = default) + { + return await PostRequestAsync("/v1/sql", $"SELECT * FROM \"metadata\".TABLES {wherePhrase}", cancellationToken); + } + public async Task GetColumnsAsync(string wherePhrase = "*", CancellationToken cancellationToken = default) + { + return await PostRequestAsync("/v1/sql", $"SELECT * FROM \"metadata\".COLUMNS {wherePhrase}", cancellationToken); + } + + public string GetTables(DDNParameterCollection parameters) + { + string result = ""; + if (parameters.Count == 0) + { + result = ""; + } + else + { + List columns = new List(); + foreach (DDNDataParameter parameter in parameters) + { + switch (parameter.ParameterName.Replace("@", "").ToUpper()) + { + case "CATALOG": + if (parameter.Value != DBNull.Value) + { + columns.Add($"tableCat = '{parameter.Value}'"); + } + break; + case "SCHEMA": + if (parameter.Value != DBNull.Value) + { + columns.Add($"tableSchem LIKE '{parameter.Value}'"); + } + break; + case "TABLE": + if (parameter.Value != DBNull.Value) + { + columns.Add($"tableName LIKE '{parameter.Value}'"); + } + + break; + case "TABLETYPE": + if (parameter.Value != DBNull.Value) + { + columns.Add($"tableType LIKE '{parameter.Value}'"); + } + + break; + default: + throw new InvalidOperationException($"Invalid parameter name: {parameter.ParameterName}"); + } + } + result = $"WHERE {string.Join(" AND ", columns)}"; + } + + var task = GetTablesAsync(result, _cancellationTokenSource.Token); + try + { + return task.Result; + } + catch (AggregateException ae) + { + throw ae.InnerException ?? ae; + } + } + + public string GetColumns(DDNParameterCollection parameters) + { + string result = ""; + if (parameters.Count == 0) + { + result = ""; + } + else + { + List columns = new List(); + foreach (DDNDataParameter parameter in parameters) + { + switch (parameter.ParameterName.Replace("@", "").ToUpper()) + { + case "CATALOG": + if (parameter.Value != DBNull.Value) + { + columns.Add($"tableCat = '{parameter.Value}'"); + } + break; + case "SCHEMA": + if (parameter.Value != DBNull.Value) + { + columns.Add($"tableSchem LIKE '{parameter.Value}'"); + } + break; + case "TABLE": + if (parameter.Value != DBNull.Value) + { + columns.Add($"tableName LIKE '{parameter.Value}'"); + } + + break; + case "COLUMN": + if (parameter.Value != DBNull.Value) + { + columns.Add($"columnName LIKE '{parameter.Value}'"); + } + + break; + default: + throw new InvalidOperationException($"Invalid parameter name: {parameter.ParameterName}"); + } + } + result = $"WHERE {string.Join(" AND ", columns)}"; + } + var task = GetColumnsAsync(result, _cancellationTokenSource.Token); + try + { + return task.Result; + } + catch (AggregateException ae) + { + throw ae.InnerException ?? ae; + } + } + #endregion + + #region Java Process Management + + private void StartJavaApplication() + { + var javaHomePath = Environment.GetEnvironmentVariable("JAVA_HOME") + ?? throw new InvalidOperationException("JAVA_HOME environment variable is not set."); + + var javaExecutablePath = Path.Combine(javaHomePath, "bin", "java"); + + var startInfo = new ProcessStartInfo + { + FileName = javaExecutablePath, + Arguments = $"-classpath {_javaDDNSQLEngine} com.hasura.SQLHttpServer", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + }; + + startInfo.EnvironmentVariables["PORT"] = _javaAppPort.ToString(); + startInfo.EnvironmentVariables["JDBC_URL"] = _connectionString; + + _javaProcess = new Process { StartInfo = startInfo }; + + _javaProcess.OutputDataReceived += (sender, args) => + { + if (args.Data != null) + Debug.WriteLine($"[Java Process Output]: {args.Data}"); + }; + + _javaProcess.ErrorDataReceived += (sender, args) => + { + if (args.Data != null) + Debug.WriteLine($"[Java Process Error]: {args.Data}"); + }; + + _javaProcess.Start(); + _javaProcess.BeginOutputReadLine(); + _javaProcess.BeginErrorReadLine(); + } + + private async Task StartJavaApplicationAsync(CancellationToken cancellationToken) + { + await Task.Run(() => StartJavaApplication(), cancellationToken); + } + + private void TerminateJavaApplication() + { + if (_javaProcess != null && !_javaProcess.HasExited) + try + { + _javaProcess.Kill(true); + _javaProcess.WaitForExit(5000); + } + catch (Exception ex) + { + Debug.WriteLine($"Error terminating Java process: {ex.Message}"); + } + } + + private int GetRandomPort() + { + var random = new Random(); + return random.Next(8081, 65535); + } + + #endregion +} + +// Parameter Collection Implementation \ No newline at end of file diff --git a/calcite-rs-jni/odbc/DDN-ODBC/DDN_OdbcCommand.cs b/calcite-rs-jni/odbc/DDN-ODBC/DDN_OdbcCommand.cs new file mode 100644 index 0000000..be4a4ba --- /dev/null +++ b/calcite-rs-jni/odbc/DDN-ODBC/DDN_OdbcCommand.cs @@ -0,0 +1,428 @@ +using System.Data; +using System.Data.Common; +using Newtonsoft.Json; + +namespace DDN_ODBC; + +public class DDN_OdbcCommand : DbCommand +{ + private readonly DDN_ODBC _connection; + private readonly DDNParameterCollection _parameters; + private string _commandText; + private int _commandTimeout = 30; // Default 30 seconds + private CommandType _commandType = CommandType.Text; + private bool _isDisposed; + private UpdateRowSource _updatedRowSource = UpdateRowSource.None; + private int _parametersCount; + private CommandParser parser = new CommandParser(); + + public DDN_OdbcCommand(DDN_ODBC connection) + { + _connection = connection ?? throw new ArgumentNullException(nameof(connection)); + _parameters = new DDNParameterCollection(); + } + + #region IDisposable Implementation + + protected override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + // Clean up managed resources + _commandText = null; + _isDisposed = true; + } + + base.Dispose(disposing); + } + + #endregion + + #region DbCommand Property Implementations + + public override string CommandText + { + get => _commandText; + set + { + ValidateNotDisposed(); + _commandText = value; + } + } + + public override int CommandTimeout + { + get => _commandTimeout; + set + { + ValidateNotDisposed(); + if (value < 0) + throw new ArgumentOutOfRangeException(nameof(value), "Timeout must be non-negative."); + _commandTimeout = value; + } + } + + public override CommandType CommandType + { + get => _commandType; + set + { + ValidateNotDisposed(); + if (!Enum.IsDefined(typeof(CommandType), value)) + throw new ArgumentOutOfRangeException(nameof(value)); + _commandType = value; + } + } + + public override UpdateRowSource UpdatedRowSource + { + get => _updatedRowSource; + set + { + ValidateNotDisposed(); + if (!Enum.IsDefined(typeof(UpdateRowSource), value)) + throw new ArgumentOutOfRangeException(nameof(value)); + _updatedRowSource = value; + } + } + + protected override DbConnection DbConnection + { + get => _connection; + set => throw new NotSupportedException("Changing the connection is not supported."); + } + + protected override DbParameterCollection DbParameterCollection => _parameters; + + protected override DbTransaction DbTransaction + { + get => null; + set => throw new NotSupportedException("Transactions are not supported in read-only mode."); + } + + public override bool DesignTimeVisible { get; set; } + + #endregion + + #region Command Execution Methods + + public override void Cancel() + { + ValidateNotDisposed(); + throw new NotSupportedException("Command cancellation is not supported."); + } + + protected override DbParameter CreateDbParameter() + { + throw new NotImplementedException(); + } + + public override int ExecuteNonQuery() + { + ValidateNotDisposed(); + throw new NotSupportedException("This is a read-only driver."); + } + + public override async Task ExecuteNonQueryAsync(CancellationToken cancellationToken) + { + ValidateNotDisposed(); + throw new NotSupportedException("This is a read-only driver."); + } + + public override object ExecuteScalar() + { + ValidateNotDisposed(); + using var reader = ExecuteDbDataReader(CommandBehavior.SingleRow); + return reader.Read() ? reader.GetValue(0) : null; + } + + public override async Task ExecuteScalarAsync(CancellationToken cancellationToken) + { + ValidateNotDisposed(); + await using var reader = await ExecuteDbDataReaderAsync(CommandBehavior.SingleRow, cancellationToken); + return await reader.ReadAsync(cancellationToken) ? reader.GetValue(0) : null; + } + + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) + { + ValidateNotDisposed(); + ValidateConnection(); + + try + { + return ExecuteReaderInternal(behavior); + } + catch (Exception ex) + { + throw new InvalidOperationException("Failed to execute reader.", ex); + } + } + + protected override async Task ExecuteDbDataReaderAsync(CommandBehavior behavior, + CancellationToken cancellationToken) + { + ValidateNotDisposed(); + ValidateConnection(); + + try + { + return await ExecuteReaderInternalAsync(behavior); + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + throw new InvalidOperationException("Failed to execute reader.", ex); + } + } + + public string GetTypeInfo(DDNParameterCollection parameters) +{ + var types = new[] + { + new { + TYPE_NAME = "VARCHAR", + DATA_TYPE = (short)12, // SQL_VARCHAR + COLUMN_SIZE = 2147483647, + LITERAL_PREFIX = "'", + LITERAL_SUFFIX = "'", + CREATE_PARAMS = (string)"max length", // Explicitly typed as string + NULLABLE = (short)1, + CASE_SENSITIVE = true, + SEARCHABLE = (short)3, + UNSIGNED_ATTRIBUTE = false, + FIXED_PREC_SCALE = false, + AUTO_UNIQUE_VALUE = false, + LOCAL_TYPE_NAME = (string)"", // Empty string instead of null + MINIMUM_SCALE = (short)0, + MAXIMUM_SCALE = (short)0, + SQL_DATA_TYPE = (short)12, + SQL_DATETIME_SUB = (short)0, + NUM_PREC_RADIX = 0 + }, + new { + TYPE_NAME = "INTEGER", + DATA_TYPE = (short)4, + COLUMN_SIZE = 10, + LITERAL_PREFIX = "", + LITERAL_SUFFIX = "", + CREATE_PARAMS = (string)"", // Empty string instead of null + NULLABLE = (short)1, + CASE_SENSITIVE = false, + SEARCHABLE = (short)3, + UNSIGNED_ATTRIBUTE = false, + FIXED_PREC_SCALE = false, + AUTO_UNIQUE_VALUE = false, + LOCAL_TYPE_NAME = (string)"", // Empty string instead of null + MINIMUM_SCALE = (short)0, + MAXIMUM_SCALE = (short)0, + SQL_DATA_TYPE = (short)4, + SQL_DATETIME_SUB = (short)0, + NUM_PREC_RADIX = 10 + }, + new { + TYPE_NAME = "REAL", + DATA_TYPE = (short)7, + COLUMN_SIZE = 15, + LITERAL_PREFIX = "", + LITERAL_SUFFIX = "", + CREATE_PARAMS = (string)"", // Empty string instead of null + NULLABLE = (short)1, + CASE_SENSITIVE = false, + SEARCHABLE = (short)3, + UNSIGNED_ATTRIBUTE = false, + FIXED_PREC_SCALE = false, + AUTO_UNIQUE_VALUE = false, + LOCAL_TYPE_NAME = (string)"", // Empty string instead of null + MINIMUM_SCALE = (short)0, + MAXIMUM_SCALE = (short)0, + SQL_DATA_TYPE = (short)7, + SQL_DATETIME_SUB = (short)0, + NUM_PREC_RADIX = 2 + }, + new { + TYPE_NAME = "TIMESTAMP", + DATA_TYPE = (short)93, + COLUMN_SIZE = 23, + LITERAL_PREFIX = "'", + LITERAL_SUFFIX = "'", + CREATE_PARAMS = (string)"", // Empty string instead of null + NULLABLE = (short)1, + CASE_SENSITIVE = false, + SEARCHABLE = (short)3, + UNSIGNED_ATTRIBUTE = false, + FIXED_PREC_SCALE = false, + AUTO_UNIQUE_VALUE = false, + LOCAL_TYPE_NAME = (string)"", // Empty string instead of null + MINIMUM_SCALE = (short)0, + MAXIMUM_SCALE = (short)3, + SQL_DATA_TYPE = (short)9, + SQL_DATETIME_SUB = (short)3, + NUM_PREC_RADIX = 0 + } + }; + + return JsonConvert.SerializeObject(types); +} + + public override void Prepare() + { + ValidateNotDisposed(); + // No preparation needed for this implementation + } + + #endregion + + #region Private Helper Methods + + private async Task ExecuteReaderInternalAsync(CommandBehavior behavior) + { + parser.TryParseCommand(_commandText, out var command, out _parametersCount); + // Handle special commands + switch (command?.ToUpperInvariant()) + { + case "SQLTABLES": + var tables = JsonConvert.DeserializeObject>(_connection.GetTables(_parameters)); + return CreateTableReader(tables); + + case "SQLCOLUMNS": + var columns = + JsonConvert.DeserializeObject>(_connection.GetColumns(_parameters)); + var sqlColumns = SqlColumnsResult.CreateFromMetadataArray(columns); + return CreateTableReader(sqlColumns); + + case "SQLFOREIGNKEY": + case "SQLSTATISTICS": + case "SQLSPECIALCOLUMNS": + case "SQLTABLEPRIVILEGES": + case "SQLCOLUMNPRIVILEGES": + case "SQLGETFUNCTIONS": + case "SQLPRIMARYKEYS": + return CreateDataReader(GetPrimaryKeys(_parameters), behavior); + + case "SQLGETTYPEINFO": + return CreateDataReader(GetTypeInfo(_parameters), behavior); + + default: + if (_commandText?.ToUpperInvariant().StartsWith("SQL") ?? false) + { + throw new InvalidOperationException($"{_commandText} is not a supported SQL command."); + } + // Handle regular SQL queries + if (string.IsNullOrWhiteSpace(_commandText)) + throw new InvalidOperationException("CommandText cannot be null or empty."); + + var result = ExecuteSqlQuery(_commandText); + return CreateDataReader(result, behavior); + } + } + + private DbDataReader ExecuteReaderInternal(CommandBehavior behavior) + { + return ExecuteReaderInternalAsync(behavior).Result; + } + + private string ExecuteSqlQuery(string sql) + { + return ExecuteSqlQueryAsync(sql).Result; + } + + private string GetPrimaryKeys(DDNParameterCollection parameters) + { + // Create an empty JSON array with the correct schema + var emptyResult = new[] + { + new + { + TABLE_CAT = (string)null, // Catalog name + TABLE_SCHEM = (string)null, // Schema name + TABLE_NAME = "", // Table name + COLUMN_NAME = "", // Column name + KEY_SEQ = 0, // Sequence number in primary key + PK_NAME = (string)null // Primary key name + } + }.Where(x => false); // Empty result set + + return JsonConvert.SerializeObject(emptyResult); + } + + private async Task ExecuteSqlQueryAsync(string sql, CancellationToken cancellationToken = default) + { + ValidateReadOnlyQuery(sql); + + try + { + var response = await _connection.PostRequestAsync("/v1/sql", sql, cancellationToken); + return response; + } + catch (Exception ex) when (ex is not OperationCanceledException) + { + throw new InvalidOperationException($"Failed to execute query: {sql}", ex); + } + } + + private DbDataReader CreateDataReader(string jsonResult, CommandBehavior behavior) + { + // Convert JSON result to appropriate DataReader implementation + // This is just an example - implement according to your needs + try + { + var data = JsonConvert.DeserializeObject(jsonResult); + return new DDN_OdbcDataReader(data, behavior); + } + catch (Exception ex) + { + throw new InvalidOperationException("Failed to create data reader from result.", ex); + } + } + + private DbDataReader CreateTableReader(List items) + { + return new DDN_OdbcDataReader(ConvertToDataTable(items), CommandBehavior.Default); + } + + private static DataTable ConvertToDataTable(IEnumerable data) + { + var table = new DataTable(); + var properties = typeof(T).GetProperties(); + + foreach (var prop in properties) + table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType); + + foreach (var item in data) + { + var row = table.NewRow(); + foreach (var prop in properties) row[prop.Name] = prop.GetValue(item) ?? DBNull.Value; + table.Rows.Add(row); + } + + return table; + } + + private void ValidateConnection() + { + if (_connection == null) + throw new InvalidOperationException("Connection not set."); + + if (_connection.State != ConnectionState.Open) + throw new InvalidOperationException("Connection must be open before executing a command."); + } + + private void ValidateNotDisposed() + { + if (_isDisposed) + throw new ObjectDisposedException(GetType().Name); + } + + private void ValidateReadOnlyQuery(string sql) + { + // Basic SQL validation to ensure read-only operations + var normalizedSql = sql.Trim().ToUpperInvariant(); + + // Check for write operations + var writeOperations = new[] { "INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER", "TRUNCATE", "MERGE" }; + if (writeOperations.Any(op => normalizedSql.StartsWith(op + " "))) + throw new NotSupportedException("This is a read-only driver. Write operations are not supported."); + } + + #endregion +} \ No newline at end of file diff --git a/calcite-rs-jni/odbc/DDN-ODBC/DDN_OdbcDataReader.cs b/calcite-rs-jni/odbc/DDN-ODBC/DDN_OdbcDataReader.cs new file mode 100644 index 0000000..1f1901d --- /dev/null +++ b/calcite-rs-jni/odbc/DDN-ODBC/DDN_OdbcDataReader.cs @@ -0,0 +1,416 @@ +using System.Collections; +using System.Data; +using System.Data.Common; + +namespace DDN_ODBC; + +public class DDN_OdbcDataReader : DbDataReader +{ + private readonly CommandBehavior _behavior; + private readonly DataTable _dataTable; + private readonly DataTableReader _internalReader; + private bool _isClosed; + private bool _isDisposed; + + public DDN_OdbcDataReader(DataTable dataTable, CommandBehavior behavior) + { + _dataTable = dataTable ?? throw new ArgumentNullException(nameof(dataTable)); + _internalReader = dataTable.CreateDataReader(); + _behavior = behavior; + } + + #region DbDataReader Property Implementations + + public override int Depth => _internalReader.Depth; + + public override int FieldCount => _internalReader.FieldCount; + + public override bool HasRows => _dataTable.Rows.Count > 0; + + public override bool IsClosed => _isClosed; + + public override int RecordsAffected => -1; // Always -1 for read-only driver + + public override int VisibleFieldCount => FieldCount; + + #endregion + + #region Data Access Methods + + public override bool GetBoolean(int ordinal) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _internalReader.GetBoolean(ordinal); + } + + public override byte GetByte(int ordinal) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _internalReader.GetByte(ordinal); + } + + public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _internalReader.GetBytes(ordinal, dataOffset, buffer, bufferOffset, length); + } + + public override char GetChar(int ordinal) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _internalReader.GetChar(ordinal); + } + + public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _internalReader.GetChars(ordinal, dataOffset, buffer, bufferOffset, length); + } + + public override string GetDataTypeName(int ordinal) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _dataTable.Columns[ordinal].DataType.Name; + } + + public override DateTime GetDateTime(int ordinal) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _internalReader.GetDateTime(ordinal); + } + + public override decimal GetDecimal(int ordinal) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _internalReader.GetDecimal(ordinal); + } + + public override double GetDouble(int ordinal) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _internalReader.GetDouble(ordinal); + } + + public override IEnumerator GetEnumerator() + { + throw new NotImplementedException(); + } + + public override Type GetFieldType(int ordinal) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _dataTable.Columns[ordinal].DataType; + } + + public override float GetFloat(int ordinal) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _internalReader.GetFloat(ordinal); + } + + public override Guid GetGuid(int ordinal) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _internalReader.GetGuid(ordinal); + } + + public override short GetInt16(int ordinal) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _internalReader.GetInt16(ordinal); + } + + public override int GetInt32(int ordinal) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _internalReader.GetInt32(ordinal); + } + + public override long GetInt64(int ordinal) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _internalReader.GetInt64(ordinal); + } + + public override string GetName(int ordinal) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _dataTable.Columns[ordinal].ColumnName; + } + + public override int GetOrdinal(string name) + { + ValidateNotDisposed(); + ValidateNotClosed(); + + // Case-insensitive column search + for (var i = 0; i < _dataTable.Columns.Count; i++) + if (string.Equals(_dataTable.Columns[i].ColumnName, name, StringComparison.OrdinalIgnoreCase)) + return i; + + throw new IndexOutOfRangeException($"Column '{name}' not found."); + } + + public override string GetString(int ordinal) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _internalReader.GetString(ordinal); + } + + public override object GetValue(int ordinal) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _internalReader.GetValue(ordinal); + } + + public override int GetValues(object[] values) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _internalReader.GetValues(values); + } + + public override bool IsDBNull(int ordinal) + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _internalReader.IsDBNull(ordinal); + } + + #endregion + + #region Navigation Methods + + public override bool Read() + { + ValidateNotDisposed(); + ValidateNotClosed(); + + if (_behavior.HasFlag(CommandBehavior.SingleRow) && _internalReader.RecordsAffected > 0) + return false; + + return _internalReader.Read(); + } + + public override bool NextResult() + { + ValidateNotDisposed(); + ValidateNotClosed(); + return _internalReader.NextResult(); + } + + public override async Task ReadAsync(CancellationToken cancellationToken) + { + ValidateNotDisposed(); + ValidateNotClosed(); + + cancellationToken.ThrowIfCancellationRequested(); + + if (_behavior.HasFlag(CommandBehavior.SingleRow) && _internalReader.RecordsAffected > 0) + return false; + + return await Task.Run(() => _internalReader.Read(), cancellationToken); + } + + public override Task NextResultAsync(CancellationToken cancellationToken) + { + ValidateNotDisposed(); + ValidateNotClosed(); + + return Task.FromResult(false); // We only support single result sets + } + + #endregion + + #region Schema Methods + + public override DataTable GetSchemaTable() + { + ValidateNotDisposed(); + ValidateNotClosed(); + + var schemaTable = new DataTable("SchemaTable"); + + // Add schema columns + schemaTable.Columns.Add("ColumnName", typeof(string)); + schemaTable.Columns.Add("ColumnOrdinal", typeof(int)); + schemaTable.Columns.Add("ColumnSize", typeof(int)); + schemaTable.Columns.Add("DataType", typeof(Type)); + schemaTable.Columns.Add("IsLong", typeof(bool)); + schemaTable.Columns.Add("IsReadOnly", typeof(bool)); + schemaTable.Columns.Add("IsUnique", typeof(bool)); + schemaTable.Columns.Add("IsKey", typeof(bool)); + schemaTable.Columns.Add("IsAutoIncrement", typeof(bool)); + schemaTable.Columns.Add("IsNullable", typeof(bool)); + schemaTable.Columns.Add("NumericPrecision", typeof(int)); + schemaTable.Columns.Add("NumericScale", typeof(int)); + + // Populate schema information + for (var i = 0; i < _dataTable.Columns.Count; i++) + { + var column = _dataTable.Columns[i]; + var row = schemaTable.NewRow(); + + row["ColumnName"] = column.ColumnName; + row["ColumnOrdinal"] = i; + row["ColumnSize"] = GetColumnSize(column); + row["DataType"] = column.DataType; + row["IsLong"] = column.DataType == typeof(byte[]) || column.DataType == typeof(string); + row["IsReadOnly"] = true; // Since this is a read-only driver + row["IsUnique"] = column.Unique; + row["IsKey"] = _dataTable.PrimaryKey.Contains(column); + row["IsAutoIncrement"] = column.AutoIncrement; + row["IsNullable"] = column.AllowDBNull; + row["NumericPrecision"] = GetNumericPrecision(column); + row["NumericScale"] = GetNumericScale(column); + + schemaTable.Rows.Add(row); + } + + return schemaTable; + } + + private int GetColumnSize(DataColumn column) + { + if (column.MaxLength > 0) + return column.MaxLength; + + // Default sizes for common types + return column.DataType.Name switch + { + "Boolean" => 1, + "Byte" => 1, + "Int16" => 2, + "Int32" => 4, + "Int64" => 8, + "Single" => 4, + "Double" => 8, + "Decimal" => 16, + "DateTime" => 8, + "Guid" => 16, + _ => 0 + }; + } + + private int GetNumericPrecision(DataColumn column) + { + return column.DataType.Name switch + { + "Byte" => 3, + "Int16" => 5, + "Int32" => 10, + "Int64" => 19, + "Single" => 7, + "Double" => 15, + "Decimal" => 28, + _ => 0 + }; + } + + private int GetNumericScale(DataColumn column) + { + return column.DataType.Name switch + { + "Single" => 7, + "Double" => 15, + "Decimal" => 28, + _ => 0 + }; + } + + #endregion + + #region IDisposable Implementation + + public override void Close() + { + if (!_isClosed) + { + _internalReader.Close(); + _isClosed = true; + } + } + + protected override void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + Close(); + _internalReader.Dispose(); + } + + _isDisposed = true; + } + + base.Dispose(disposing); + } + + #endregion + + #region Validation Methods + + private void ValidateNotClosed() + { + if (_isClosed) + throw new InvalidOperationException("DataReader is closed."); + } + + private void ValidateNotDisposed() + { + if (_isDisposed) + throw new ObjectDisposedException(GetType().Name); + } + + private void ValidateOrdinal(int ordinal) + { + if (ordinal < 0 || ordinal >= FieldCount) + throw new IndexOutOfRangeException($"Invalid ordinal {ordinal}. Must be between 0 and {FieldCount - 1}."); + } + + #endregion + + #region Indexer Implementation + + public override object this[int ordinal] + { + get + { + ValidateNotDisposed(); + ValidateNotClosed(); + ValidateOrdinal(ordinal); + return GetValue(ordinal); + } + } + + public override object this[string name] + { + get + { + ValidateNotDisposed(); + ValidateNotClosed(); + return GetValue(GetOrdinal(name)); + } + } + + #endregion +} \ No newline at end of file diff --git a/calcite-rs-jni/odbc/DDN-ODBC/JsonDataReader.cs b/calcite-rs-jni/odbc/DDN-ODBC/JsonDataReader.cs new file mode 100644 index 0000000..2a205de --- /dev/null +++ b/calcite-rs-jni/odbc/DDN-ODBC/JsonDataReader.cs @@ -0,0 +1,193 @@ +using System.Collections; +using System.Data; +using System.Data.Common; +using Newtonsoft.Json.Linq; + +namespace DDN_ODBC; + +public class JsonDataReader : DbDataReader +{ + private readonly JArray _data; + private readonly JObject[] _rows; + private int _currentRow = -1; + private readonly Dictionary _columnMap; + private bool _isClosed; + private readonly Dictionary _columnTypes; + + public JsonDataReader(string jsonArrayString) + { + _data = JArray.Parse(jsonArrayString); + _rows = []; //_data.ToArray(); + _columnMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + _columnTypes = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (_rows.Length > 0) + { + int index = 0; + foreach (var prop in _rows[0].Properties()) + { + _columnMap[prop.Name] = index++; + var type = InferColumnType(prop.Name); + _columnTypes[prop.Name] = type ?? typeof(object); + } + } + } + + // Add the missing abstract members: + public override object this[int ordinal] => GetValue(ordinal); + + public override object this[string name] => GetValue(GetOrdinal(name)); + + public override string GetDataTypeName(int ordinal) + { + return GetFieldType(ordinal).Name; + } + + public override IEnumerator GetEnumerator() + { + return new DbEnumerator(this); + } + + // Rest of the implementation remains the same... + private Type InferColumnType(string columnName) + { + foreach (var row in _rows) + { + if (row[columnName] != null && row[columnName].Type != JTokenType.Null) + { + switch (row[columnName].Type) + { + case JTokenType.Integer: + return typeof(long); + case JTokenType.Float: + return typeof(double); + case JTokenType.String: + return typeof(string); + case JTokenType.Boolean: + return typeof(bool); + case JTokenType.Date: + return typeof(DateTime); + default: + return typeof(object); + } + } + } + return typeof(object); + } + + public override bool Read() + { + if (_isClosed) + throw new InvalidOperationException("DataReader is closed"); + + _currentRow++; + return _currentRow < _rows.Length; + } + + public override int FieldCount => _columnMap.Count; + + public override string GetName(int ordinal) + { + if (ordinal < 0 || ordinal >= FieldCount) + throw new IndexOutOfRangeException($"Invalid ordinal {ordinal}"); + + return _columnMap.First(x => x.Value == ordinal).Key; + } + + public override Type GetFieldType(int ordinal) + { + var name = GetName(ordinal); + return _columnTypes[name]; + } + + public override object GetValue(int ordinal) + { + if (_currentRow == -1) + throw new InvalidOperationException("Call Read() first"); + if (_currentRow >= _rows.Length) + throw new InvalidOperationException("No more rows"); + if (ordinal < 0 || ordinal >= FieldCount) + throw new IndexOutOfRangeException($"Invalid ordinal {ordinal}"); + + var propertyName = GetName(ordinal); + var token = _rows[_currentRow][propertyName]; + + if (token == null || token.Type == JTokenType.Null) + return DBNull.Value; + + var fieldType = GetFieldType(ordinal); + try + { + return token.ToObject(fieldType); + } + catch + { + return DBNull.Value; + } + } + + public override int GetOrdinal(string name) + { + if (_columnMap.TryGetValue(name, out int ordinal)) + return ordinal; + throw new IndexOutOfRangeException($"Column '{name}' not found"); + } + + public override bool IsDBNull(int ordinal) + { + var propertyName = GetName(ordinal); + var token = _rows[_currentRow][propertyName]; + return token == null || token.Type == JTokenType.Null; + } + + public override bool GetBoolean(int ordinal) => (bool)GetValue(ordinal); + public override byte GetByte(int ordinal) => (byte)GetValue(ordinal); + public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, int length) + => throw new NotImplementedException(); + public override char GetChar(int ordinal) => (char)GetValue(ordinal); + public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, int length) + => throw new NotImplementedException(); + public override DateTime GetDateTime(int ordinal) => (DateTime)GetValue(ordinal); + public override decimal GetDecimal(int ordinal) => (decimal)GetValue(ordinal); + public override double GetDouble(int ordinal) => (double)GetValue(ordinal); + public override float GetFloat(int ordinal) => (float)GetValue(ordinal); + public override Guid GetGuid(int ordinal) => (Guid)GetValue(ordinal); + public override short GetInt16(int ordinal) => (short)GetValue(ordinal); + public override int GetInt32(int ordinal) => (int)GetValue(ordinal); + public override long GetInt64(int ordinal) => (long)GetValue(ordinal); + public override string GetString(int ordinal) => (string)GetValue(ordinal); + + public override int GetValues(object[] values) + { + var count = Math.Min(values.Length, FieldCount); + for (int i = 0; i < count; i++) + { + values[i] = GetValue(i); + } + return count; + } + + public override bool IsClosed => _isClosed; + public override bool HasRows => _rows.Length > 0; + public override int Depth => 0; + public override int RecordsAffected => -1; + + public override void Close() + { + _isClosed = true; + } + + public override bool NextResult() + { + return false; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + Close(); + } + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/calcite-rs-jni/odbc/DDN-ODBC/ListDataReader.cs b/calcite-rs-jni/odbc/DDN-ODBC/ListDataReader.cs new file mode 100644 index 0000000..144fc3c --- /dev/null +++ b/calcite-rs-jni/odbc/DDN-ODBC/ListDataReader.cs @@ -0,0 +1,178 @@ +using System.Collections; +using System.ComponentModel; +using System.Data; + +namespace DDN_ODBC; + +public class ListDataReader : IDataReader +{ + private readonly IEnumerator _enumerator; + private readonly IList _list; + private readonly Dictionary _nameToIndexMap; + private readonly PropertyDescriptorCollection _properties; + private int _currentIndex = -1; + + public ListDataReader(IList list) + { + _list = list ?? throw new ArgumentNullException(nameof(list)); + + _enumerator = _list.GetEnumerator(); + _properties = TypeDescriptor.GetProperties(typeof(T)); + _nameToIndexMap = new Dictionary(); + + for (var i = 0; i < _properties.Count; i++) _nameToIndexMap[_properties[i].Name] = i; + } + + public int FieldCount => _properties.Count; + + public bool Read() + { + return _enumerator.MoveNext(); + } + + public object GetValue(int i) + { + return _properties[i].GetValue(_enumerator.Current) ?? DBNull.Value; + } + + public int GetOrdinal(string name) + { + return _nameToIndexMap[name]; + } + + public string GetName(int i) + { + return _properties[i].Name; + } + + public Type GetFieldType(int i) + { + return _properties[i].PropertyType; + } + + // Other methods in IDataReader can throw NotImplementedException or be properly implemented if needed + public bool IsDBNull(int i) + { + return GetValue(i) == DBNull.Value; + } + + public object this[int i] => GetValue(i); + public object this[string name] => GetValue(GetOrdinal(name)); + public int Depth => throw new NotImplementedException(); + public bool IsClosed => throw new NotImplementedException(); + public int RecordsAffected => throw new NotImplementedException(); + + public void Close() + { + } + + public DataTable GetSchemaTable() + { + throw new NotImplementedException(); + } + + public bool NextResult() + { + throw new NotImplementedException(); + } + + public int GetValues(object[] values) + { + throw new NotImplementedException(); + } + + public bool GetBoolean(int i) + { + return (bool)GetValue(i); + } + + public byte GetByte(int i) + { + return (byte)GetValue(i); + } + + public long GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length) + { + throw new NotImplementedException(); + } + + public char GetChar(int i) + { + return (char)GetValue(i); + } + + public long GetChars(int i, long fieldoffset, char[] buffer, int bufferoffset, int length) + { + throw new NotImplementedException(); + } + + public IDataReader GetData(int i) + { + throw new NotImplementedException(); + } + + public string GetDataTypeName(int i) + { + throw new NotImplementedException(); + } + + public DateTime GetDateTime(int i) + { + return (DateTime)GetValue(i); + } + + public decimal GetDecimal(int i) + { + return (decimal)GetValue(i); + } + + public double GetDouble(int i) + { + return (double)GetValue(i); + } + + public float GetFloat(int i) + { + return (float)GetValue(i); + } + + public Guid GetGuid(int i) + { + return (Guid)GetValue(i); + } + + public short GetInt16(int i) + { + return (short)GetValue(i); + } + + public int GetInt32(int i) + { + return (int)GetValue(i); + } + + public long GetInt64(int i) + { + return (long)GetValue(i); + } + + public string GetString(int i) + { + return (string)GetValue(i); + } + + public void Dispose() + { + _enumerator.Dispose(); + } + + public IDataReader GetDataTypeReader() + { + throw new NotImplementedException(); + } + + public IEnumerator GetEnumerator() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/calcite-rs-jni/odbc/DDN-ODBC/SQLColumnsResult.cs b/calcite-rs-jni/odbc/DDN-ODBC/SQLColumnsResult.cs new file mode 100644 index 0000000..43a124d --- /dev/null +++ b/calcite-rs-jni/odbc/DDN-ODBC/SQLColumnsResult.cs @@ -0,0 +1,245 @@ +using DDN_ODBC; +using System.Text.Json.Serialization; + +public class SqlColumnsResult +{ + // ODBC SQL type constants from sql.h + public const short SQL_UNKNOWN_TYPE = 0; + public const short SQL_CHAR = 1; + public const short SQL_NUMERIC = 2; + public const short SQL_DECIMAL = 3; + public const short SQL_INTEGER = 4; + public const short SQL_SMALLINT = 5; + public const short SQL_FLOAT = 6; + public const short SQL_REAL = 7; + public const short SQL_DOUBLE = 8; + public const short SQL_DATETIME = 9; + public const short SQL_VARCHAR = 12; + public const short SQL_TYPE_DATE = 91; + public const short SQL_TYPE_TIME = 92; + public const short SQL_TYPE_TIMESTAMP = 93; + public const short SQL_TYPE_TIMESTAMP_WITH_TIMEZONE = 95; + public const short SQL_BIT = -7; + public const short SQL_TINYINT = -6; + public const short SQL_BIGINT = -5; + public const short SQL_LONGVARCHAR = -1; + public const short SQL_BINARY = -2; + public const short SQL_VARBINARY = -3; + public const short SQL_LONGVARBINARY = -4; + public const short SQL_GUID = -11; + public const short SQL_WCHAR = -8; + public const short SQL_WVARCHAR = -9; + public const short SQL_WLONGVARCHAR = -10; + public const short SQL_ARRAY = 2003; + + // SQL datetime sub-type codes + public const short SQL_CODE_DATE = 1; + public const short SQL_CODE_TIME = 2; + public const short SQL_CODE_TIMESTAMP = 3; + public const short SQL_CODE_TIME_WITH_TIMEZONE = 4; + public const short SQL_CODE_TIMESTAMP_WITH_TIMEZONE = 5; + + // Nullable constants + public const int SQL_NO_NULLS = 0; + public const int SQL_NULLABLE = 1; + public const int SQL_NULLABLE_UNKNOWN = 2; + + // Properties matching exact ODBC SQLColumns result set + public string? TABLE_CAT { get; set; } + public string? TABLE_SCHEM { get; set; } + public string? TABLE_NAME { get; set; } + public string? COLUMN_NAME { get; set; } + public short DATA_TYPE { get; set; } + public string TYPE_NAME { get; set; } = string.Empty; + public int COLUMN_SIZE { get; set; } + public int BUFFER_LENGTH { get; set; } + public short DECIMAL_DIGITS { get; set; } + public short NUM_PREC_RADIX { get; set; } + public int NULLABLE { get; set; } + public string? REMARKS { get; set; } + public string? COLUMN_DEF { get; set; } + public short SQL_DATA_TYPE { get; set; } + public short SQL_DATETIME_SUB { get; set; } + public int CHAR_OCTET_LENGTH { get; set; } + public int ORDINAL_POSITION { get; set; } + public string IS_NULLABLE { get; set; } = string.Empty; + public string? SCOPE_CATALOG { get; set; } + public string? SCOPE_SCHEMA { get; set; } + public string? SCOPE_TABLE { get; set; } + public short? SOURCE_DATA_TYPE { get; set; } + public string IS_AUTOINCREMENT { get; set; } = "NO"; + public string IS_GENERATEDCOLUMN { get; set; } = "NO"; + + public SqlColumnsResult(DDNColumnMetadata metadata) + { + // First map the input type to ODBC type + DATA_TYPE = MapDataType(metadata.DataType); + + // Then derive other type information from DATA_TYPE + TYPE_NAME = GetTypeNameFromDataType(DATA_TYPE); + + // Basic metadata + TABLE_CAT = metadata.TableCatalog; + TABLE_SCHEM = metadata.TableSchema; + TABLE_NAME = metadata.TableName; + COLUMN_NAME = metadata.ColumnName; + + // Size and buffer information + COLUMN_SIZE = metadata.ColumnSize == -1 ? GetDefaultColumnSize(TYPE_NAME) : metadata.ColumnSize; + BUFFER_LENGTH = CalculateBufferLength(TYPE_NAME, COLUMN_SIZE); + CHAR_OCTET_LENGTH = metadata.CharOctetLength; + + // Numeric information + DECIMAL_DIGITS = (short)(metadata.DecimalDigits ?? 0); + NUM_PREC_RADIX = (short)(metadata.NumPrecRadix); + + // Nullability + NULLABLE = metadata.Nullable; + IS_NULLABLE = MapIsNullable(metadata.IsNullable, metadata.Nullable); + + // Additional metadata + REMARKS = metadata.Remarks; + COLUMN_DEF = metadata.ColumnDefault; + ORDINAL_POSITION = metadata.OrdinalPosition; + + // Type information + (SQL_DATA_TYPE, SQL_DATETIME_SUB) = MapSqlDataType(DATA_TYPE); + + // Scope information + SCOPE_CATALOG = metadata.ScopeCatalog; + SCOPE_SCHEMA = metadata.ScopeSchema; + SCOPE_TABLE = metadata.ScopeTable; + SOURCE_DATA_TYPE = metadata.SourceDataType; + + // Generation information + IS_AUTOINCREMENT = metadata.IsAutoincrement ?? "NO"; + IS_GENERATEDCOLUMN = metadata.IsGeneratedColumn ?? "NO"; + } + + private short MapDataType(int calciteType) + { + return calciteType switch + { + 1 => SQL_BIT, // BOOLEAN + 2 => SQL_TINYINT, // TINYINT + 3 => SQL_SMALLINT, // SMALLINT + 4 => SQL_INTEGER, // INTEGER + 5 => SQL_BIGINT, // BIGINT + 6 => SQL_FLOAT, // FLOAT + 7 => SQL_REAL, // REAL + 8 => SQL_DOUBLE, // DOUBLE + 9 => SQL_DOUBLE, // DECIMAL + 10 => SQL_TYPE_DATE, // DATE + 11 => SQL_TYPE_TIME, // TIME + 12 => SQL_VARCHAR, // VARCHAR + 13 => SQL_CHAR, // CHAR + 2000 => SQL_VARCHAR, // ANY mapped to VARCHAR + 2003 => SQL_ARRAY, // ARRAY + _ => SQL_UNKNOWN_TYPE + }; + } + + private (short sqlDataType, short sqlDatetimeSub) MapSqlDataType(short odbcType) + { + return odbcType switch + { + // Date/Time types + SQL_TYPE_DATE => (SQL_DATETIME, SQL_CODE_DATE), + SQL_TYPE_TIME => (SQL_DATETIME, SQL_CODE_TIME), + SQL_TYPE_TIMESTAMP => (SQL_DATETIME, SQL_CODE_TIMESTAMP), + SQL_TYPE_TIMESTAMP_WITH_TIMEZONE => (SQL_DATETIME, SQL_CODE_TIMESTAMP_WITH_TIMEZONE), + + // For non-datetime types, SQL_DATA_TYPE equals DATA_TYPE and no datetime subcode + _ => (odbcType, 0) + }; + } + + private string GetTypeNameFromDataType(short dataType) + { + return dataType switch + { + SQL_CHAR => "CHAR", + SQL_VARCHAR => "VARCHAR", + SQL_LONGVARCHAR => "LONGVARCHAR", + SQL_WCHAR => "WCHAR", + SQL_WVARCHAR => "WVARCHAR", + SQL_WLONGVARCHAR => "WLONGVARCHAR", + SQL_DECIMAL => "DECIMAL", + SQL_NUMERIC => "NUMERIC", + SQL_SMALLINT => "SMALLINT", + SQL_INTEGER => "INTEGER", + SQL_REAL => "REAL", + SQL_FLOAT => "FLOAT", + SQL_DOUBLE => "DOUBLE", + SQL_BIT => "BIT", + SQL_TINYINT => "TINYINT", + SQL_BIGINT => "BIGINT", + SQL_BINARY => "BINARY", + SQL_VARBINARY => "VARBINARY", + SQL_LONGVARBINARY => "LONGVARBINARY", + SQL_TYPE_DATE => "DATE", + SQL_TYPE_TIME => "TIME", + SQL_TYPE_TIMESTAMP => "TIMESTAMP", + SQL_TYPE_TIMESTAMP_WITH_TIMEZONE => "TIMESTAMP WITH TIME ZONE", + SQL_GUID => "GUID", + SQL_ARRAY => "ARRAY", + _ => "VARCHAR" // Default to VARCHAR for unknown types + }; + } + + private int GetDefaultColumnSize(string sqlTypeName) + { + return sqlTypeName switch + { + "CHAR" => 1, + "VARCHAR" => 255, + "INTEGER" => 10, + "BIGINT" => 19, + "SMALLINT" => 5, + "TINYINT" => 3, + "DOUBLE" => 15, + "FLOAT" => 7, + "DECIMAL" => 18, + "DATE" => 10, + "TIME" => 8, + "TIMESTAMP" => 23, + "BIT" => 1, + _ => 0 + }; + } + + private int CalculateBufferLength(string sqlTypeName, int columnSize) + { + return sqlTypeName switch + { + "CHAR" or "VARCHAR" => columnSize, + "BINARY" or "VARBINARY" => columnSize, + "INTEGER" => 4, + "BIGINT" => 8, + "SMALLINT" => 2, + "TINYINT" => 1, + "DOUBLE" => 8, + "FLOAT" => 4, + "DATE" => 6, + "TIME" => 6, + "TIMESTAMP" => 16, + "BIT" => 1, + _ => 0 + }; + } + + private string MapIsNullable(string? isNullable, int nullable) + { + if (isNullable == "NO" || nullable == SQL_NO_NULLS) + return "NO"; + if (isNullable == "YES" || nullable == SQL_NULLABLE) + return "YES"; + return ""; // For SQL_NULLABLE_UNKNOWN + } + + // Helper method to create a list from array of ColumnMetadata + public static List CreateFromMetadataArray(List metadata) + { + return metadata.Select(m => new SqlColumnsResult(m)).ToList(); + } +} \ No newline at end of file diff --git a/calcite-rs-jni/odbc/DDN-ODBC/SqlQuery.cs b/calcite-rs-jni/odbc/DDN-ODBC/SqlQuery.cs new file mode 100644 index 0000000..ae45f2f --- /dev/null +++ b/calcite-rs-jni/odbc/DDN-ODBC/SqlQuery.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace DDN_ODBC; + +public class SqlQuery +{ + [JsonProperty("sql")] public string Sql { get; set; } + + [JsonProperty("disallowMutations")] public bool DisallowMutations { get; set; } +} \ No newline at end of file diff --git a/calcite-rs-jni/sqlengine/pom.xml b/calcite-rs-jni/sqlengine/pom.xml index 8a394b3..d7adc0f 100644 --- a/calcite-rs-jni/sqlengine/pom.xml +++ b/calcite-rs-jni/sqlengine/pom.xml @@ -8,6 +8,15 @@ ndc-calcite 1.0.0 + sqlengine + + + 11 + 11 + UTF-8 + 1.0.0 + + org.json @@ -17,18 +26,61 @@ com.hasura graphql-jdbc-driver - 1.0.0 - system - ${project.basedir}/../jdbc/target/graphql-jdbc-driver-1.0.0-jar-with-dependencies.jar + ${jar.version} - sqlengine - - - 11 - 11 - UTF-8 - - + + + + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + + install-local-jar + initialize + + exec + + + mvn + + install:install-file + -Dfile=${project.basedir}/../jdbc/target/graphql-jdbc-driver-${jar.version}-jar-with-dependencies.jar + -DgroupId=com.hasura + -DartifactId=graphql-jdbc-driver + -Dversion=${jar.version} + -Dpackaging=jar + + + + + + + + maven-assembly-plugin + + + + com.hasura.SQLHttpServer + + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + \ No newline at end of file diff --git a/calcite-rs-jni/sqlengine/src/main/java/com/hasura/SQLHttpServer.java b/calcite-rs-jni/sqlengine/src/main/java/com/hasura/SQLHttpServer.java index 5a74e27..69fae3a 100644 --- a/calcite-rs-jni/sqlengine/src/main/java/com/hasura/SQLHttpServer.java +++ b/calcite-rs-jni/sqlengine/src/main/java/com/hasura/SQLHttpServer.java @@ -47,11 +47,26 @@ public static void main(String[] args) throws IOException { HttpServer server = HttpServer.create(new InetSocketAddress(PORT), 0); server.createContext("/sql", new SQLHandler()); server.createContext("/v1/sql", new SQLHandler()); + server.createContext("/health", new HealthHandler()); // add this line server.setExecutor(null); server.start(); System.out.println("Server started on port " + PORT); } + static class HealthHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + JSONObject json = new JSONObject(); + json.put("health", "OK"); + byte[] response = json.toString().getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json"); + exchange.sendResponseHeaders(200, response.length); + try (OutputStream os = exchange.getResponseBody()) { + os.write(response); + } + } + } + static class SQLHandler implements HttpHandler { @Override public void handle(HttpExchange exchange) throws IOException {