дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(db): add Microsoft SQL Server driver support (#276)
The database tool window can connect to Microsoft SQL Server (tiberius over rustls with a bb8 pool): browse schema objects, edit table data, run queries and view plans; SQL Server dialect, object dialogs and icons in the UI. Values are rendered by column type: exact decimal/numeric, money with four decimals, tinyint, uniqueidentifier, date/time/datetime/datetime2/smalldatetime/datetimeoffset in ISO form, xml and binary. Multi-statement edits roll back on any failure and discard the connection rather than returning it to the pool mid-transaction; connect-time errors (login, database) are reported; result pagination reports more rows; cancelling a query stops reading and drops the stream. The sqlx drivers keep the shared with_pool! path. Co-authored-by: dimassfeb-09 <[email protected]>
Цей коміт міститься в:
1 джерело
d1f84ef364
коміт
3a1e47d872
20 files changed
+974
-43
No files matched your search
Створено
+179
-15
@@ -314,6 +314,19 @@ dependencies = [
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asynchronous-codec"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4057f2c32adbb2fc158e22fb38433c8e9bbf76b75a4732c7c0cbaf695fb65568"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atk"
|
||||
version = "0.18.2"
|
||||
@@ -388,6 +401,31 @@ version = "1.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||
|
||||
[[package]]
|
||||
name = "bb8"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "457d7ed3f888dfd2c7af56d4975cade43c622f74bdcddfed6d4352f57acc6310"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bb8-tiberius"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a5023b318f47a294d02b46ce0d82ac8e7df0afce2257f041ba7316566d6cd56"
|
||||
dependencies = [
|
||||
"bb8",
|
||||
"thiserror 1.0.69",
|
||||
"tiberius",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bcrypt-pbkdf"
|
||||
version = "0.11.0"
|
||||
@@ -959,6 +997,12 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "connection-string"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "510ca239cf13b7f8d16a2b48f263de7b4f8c566f0af58d901031473c76afb1e3"
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.6"
|
||||
@@ -3817,9 +3861,9 @@ dependencies = [
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls 0.23.46",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.26.6",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
@@ -4080,6 +4124,8 @@ dependencies = [
|
||||
name = "ide-db"
|
||||
version = "0.1.25"
|
||||
dependencies = [
|
||||
"bb8",
|
||||
"bb8-tiberius",
|
||||
"dirs 6.0.0",
|
||||
"futures",
|
||||
"ide-core",
|
||||
@@ -4090,6 +4136,7 @@ dependencies = [
|
||||
"specta",
|
||||
"sqlx",
|
||||
"tempfile",
|
||||
"tiberius",
|
||||
"tokio",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
"tracing",
|
||||
@@ -4182,7 +4229,7 @@ dependencies = [
|
||||
"ide-core",
|
||||
"parking_lot",
|
||||
"reqwest",
|
||||
"rustls",
|
||||
"rustls 0.23.46",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -6050,6 +6097,12 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
@@ -6526,6 +6579,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
|
||||
|
||||
[[package]]
|
||||
name = "pretty-hex"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5"
|
||||
|
||||
[[package]]
|
||||
name = "primefield"
|
||||
version = "0.14.0"
|
||||
@@ -6920,14 +6979,14 @@ dependencies = [
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls",
|
||||
"rustls 0.23.46",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.26.6",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
@@ -7189,6 +7248,18 @@ dependencies = [
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
"rustls-webpki 0.101.7",
|
||||
"sct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.46"
|
||||
@@ -7199,21 +7270,42 @@ dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"rustls-webpki 0.103.16",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
|
||||
dependencies = [
|
||||
"openssl-probe 0.1.6",
|
||||
"rustls-pemfile",
|
||||
"schannel",
|
||||
"security-framework 2.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-native-certs"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
|
||||
dependencies = [
|
||||
"openssl-probe",
|
||||
"openssl-probe 0.2.1",
|
||||
"rustls-pki-types",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework 3.7.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7236,11 +7328,11 @@ dependencies = [
|
||||
"jni 0.22.4",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls",
|
||||
"rustls-native-certs",
|
||||
"rustls 0.23.46",
|
||||
"rustls-native-certs 0.8.4",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki",
|
||||
"security-framework",
|
||||
"rustls-webpki 0.103.16",
|
||||
"security-framework 3.7.0",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.61.2",
|
||||
@@ -7252,6 +7344,16 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.16"
|
||||
@@ -7378,6 +7480,16 @@ dependencies = [
|
||||
"sha2 0.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.8.1"
|
||||
@@ -7392,6 +7504,19 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
@@ -7988,7 +8113,7 @@ dependencies = [
|
||||
"log",
|
||||
"memchr",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
"rustls 0.23.46",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
@@ -8693,7 +8818,7 @@ dependencies = [
|
||||
"osakit",
|
||||
"percent-encoding",
|
||||
"reqwest",
|
||||
"rustls",
|
||||
"rustls 0.23.46",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -8935,6 +9060,34 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiberius"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1446cb4198848d1562301a3340424b4f425ef79f35ef9ee034769a9dd92c10d"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"asynchronous-codec",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"connection-string",
|
||||
"encoding_rs",
|
||||
"enumflags2",
|
||||
"futures-util",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"pretty-hex",
|
||||
"rustls-native-certs 0.6.3",
|
||||
"rustls-pemfile",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.55"
|
||||
@@ -9019,13 +9172,23 @@ dependencies = [
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
|
||||
dependencies = [
|
||||
"rustls 0.21.12",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9cc2678c2cdd569ef8215e2afd7954ada2ae20b4fdd2c5fe6139a3b02d105db"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls 0.23.46",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -9060,6 +9223,7 @@ checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"libc",
|
||||
|
||||
@@ -92,6 +92,9 @@ reqwest = { version = "0.13", default-features = false, features = ["rustls-no-p
|
||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }
|
||||
rustls-platform-verifier = "0.7"
|
||||
sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", "tls-rustls-ring-webpki", "sqlite", "postgres", "mysql"] }
|
||||
tiberius = { version = "0.12", default-features = false, features = ["tds73", "rustls", "sql-browser-tokio"] }
|
||||
bb8 = "0.9"
|
||||
bb8-tiberius = { version = "0.16", default-features = false, features = ["tds73", "tls", "with-tokio", "sql-browser"] }
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
# testing
|
||||
|
||||
@@ -16,6 +16,9 @@ tracing.workspace = true
|
||||
parking_lot.workspace = true
|
||||
futures.workspace = true
|
||||
sqlx.workspace = true
|
||||
tiberius.workspace = true
|
||||
bb8.workspace = true
|
||||
bb8-tiberius.workspace = true
|
||||
dirs.workspace = true
|
||||
ide-ssh.workspace = true
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
//! whatever its type.
|
||||
|
||||
use crate::source::{DataSource, DbDriver, DbSslMode};
|
||||
use bb8::ManageConnection;
|
||||
use bb8::Pool as Bb8Pool;
|
||||
use futures::StreamExt;
|
||||
use ide_core::{CancellationToken, IdeError, IdeResult, error::ErrorKind};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
@@ -44,10 +47,47 @@ pub struct QueryResult {
|
||||
pub execution_ms: f64,
|
||||
}
|
||||
|
||||
// bb8-tiberius never marks connections broken; keep an empty slot when discarded.
|
||||
pub struct MssqlConnectionManager(bb8_tiberius::ConnectionManager);
|
||||
|
||||
pub struct MssqlConnection(Option<bb8_tiberius::rt::Client>);
|
||||
|
||||
impl std::ops::Deref for MssqlConnection {
|
||||
type Target = bb8_tiberius::rt::Client;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.0.as_ref().expect("live SQL Server connection")
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::DerefMut for MssqlConnection {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.0.as_mut().expect("live SQL Server connection")
|
||||
}
|
||||
}
|
||||
|
||||
impl ManageConnection for MssqlConnectionManager {
|
||||
type Connection = MssqlConnection;
|
||||
type Error = bb8_tiberius::Error;
|
||||
|
||||
async fn connect(&self) -> Result<Self::Connection, Self::Error> {
|
||||
self.0.connect().await.map(|c| MssqlConnection(Some(c)))
|
||||
}
|
||||
|
||||
async fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Self::Error> {
|
||||
conn.simple_query("SELECT 1").await?.into_results().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
|
||||
conn.0.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Pool {
|
||||
Sqlite(SqlitePool),
|
||||
Postgres(PgPool),
|
||||
Mysql(MySqlPool),
|
||||
SqlServer(Bb8Pool<MssqlConnectionManager>),
|
||||
}
|
||||
|
||||
/// Runs `$body` with `$p` bound to the concrete pool of each driver.
|
||||
@@ -57,6 +97,7 @@ macro_rules! with_pool {
|
||||
Pool::Sqlite($p) => $body,
|
||||
Pool::Postgres($p) => $body,
|
||||
Pool::Mysql($p) => $body,
|
||||
Pool::SqlServer(_) => unreachable!(),
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -79,6 +120,132 @@ pub(crate) fn db_err(e: sqlx::Error) -> IdeError {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mssql_err(e: impl Into<bb8_tiberius::Error>) -> IdeError {
|
||||
match e.into() {
|
||||
bb8_tiberius::Error::Tiberius(e) => match &e {
|
||||
tiberius::error::Error::Server(d) => {
|
||||
let code = d.code();
|
||||
// Wrong login: SQL Server 18456.
|
||||
let auth = matches!(code, 18452 | 18456 | 18470);
|
||||
IdeError::new(
|
||||
if auth { ErrorKind::PermissionDenied } else { ErrorKind::InvalidInput },
|
||||
format!("[{code}] {}", d.message()),
|
||||
)
|
||||
}
|
||||
tiberius::error::Error::Io { .. } | tiberius::error::Error::Tls(_) => {
|
||||
IdeError::new(ErrorKind::Io, e.to_string())
|
||||
}
|
||||
_ => IdeError::new(ErrorKind::Protocol, e.to_string()),
|
||||
},
|
||||
bb8_tiberius::Error::Io(e) => IdeError::new(ErrorKind::Io, e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn mssql_pool_err(e: bb8::RunError<bb8_tiberius::Error>) -> IdeError {
|
||||
match e {
|
||||
bb8::RunError::User(e) => mssql_err(e),
|
||||
bb8::RunError::TimedOut => IdeError::new(ErrorKind::Io, "timed out connecting to the database"),
|
||||
}
|
||||
}
|
||||
|
||||
const DAYS_1900_01_01: i64 = 693_595;
|
||||
|
||||
/// Formats a proleptic Gregorian date given days since `0001-01-01`.
|
||||
fn format_date(days: i64) -> String {
|
||||
let z = days + 306;
|
||||
let era = z.div_euclid(146_097);
|
||||
let doe = z.rem_euclid(146_097);
|
||||
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
|
||||
let y = yoe + era * 400;
|
||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
let mp = (5 * doy + 2) / 153;
|
||||
let d = doy - (153 * mp + 2) / 5 + 1;
|
||||
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||
let year = if m <= 2 { y + 1 } else { y };
|
||||
format!("{year:04}-{m:02}-{d:02}")
|
||||
}
|
||||
|
||||
fn format_hms(secs: u64, frac: u64, scale: u8) -> String {
|
||||
let (h, m, s) = (secs / 3600, (secs / 60) % 60, secs % 60);
|
||||
if scale == 0 || frac == 0 {
|
||||
format!("{h:02}:{m:02}:{s:02}")
|
||||
} else {
|
||||
let frac = format!("{frac:0width$}", width = usize::from(scale));
|
||||
format!("{h:02}:{m:02}:{s:02}.{}", frac.trim_end_matches('0'))
|
||||
}
|
||||
}
|
||||
|
||||
fn format_time(t: tiberius::time::Time) -> String {
|
||||
let scale = t.scale().min(7);
|
||||
let div = 10u64.pow(u32::from(scale));
|
||||
format_hms(t.increments() / div, t.increments() % div, scale)
|
||||
}
|
||||
|
||||
fn format_numeric(n: tiberius::numeric::Numeric) -> String {
|
||||
let scale = u32::from(n.scale());
|
||||
if scale == 0 {
|
||||
return n.value().to_string();
|
||||
}
|
||||
let sign = if n.value() < 0 { "-" } else { "" };
|
||||
let abs = n.value().unsigned_abs();
|
||||
let div = 10u128.pow(scale);
|
||||
format!("{sign}{}.{:0width$}", abs / div, abs % div, width = scale as usize)
|
||||
}
|
||||
|
||||
fn tiberius_cell(cell: &tiberius::ColumnData<'_>, ty: tiberius::ColumnType) -> Option<String> {
|
||||
use tiberius::ColumnData as C;
|
||||
match cell {
|
||||
C::U8(v) => v.map(|v| v.to_string()),
|
||||
C::I16(v) => v.map(|v| v.to_string()),
|
||||
C::I32(v) => v.map(|v| v.to_string()),
|
||||
C::I64(v) => v.map(|v| v.to_string()),
|
||||
C::F32(v) => v.map(|v| v.to_string()),
|
||||
C::F64(v) if matches!(ty, tiberius::ColumnType::Money | tiberius::ColumnType::Money4) => {
|
||||
// Beyond ~9e11, money precision is limited by the driver's f64 decoding.
|
||||
v.map(|v| format!("{:.4}", v))
|
||||
}
|
||||
C::F64(v) => v.map(|v| v.to_string()),
|
||||
C::Bit(v) => v.map(|v| v.to_string()),
|
||||
C::String(v) => v.as_deref().map(str::to_owned),
|
||||
C::Guid(v) => v.map(|v| v.to_string()),
|
||||
C::Binary(v) => v.as_deref().map(hex),
|
||||
C::Numeric(v) => v.map(format_numeric),
|
||||
C::Xml(v) => v.as_deref().map(|x| x.as_ref().to_owned()),
|
||||
C::Date(v) => v.map(|d| format_date(i64::from(d.days()))),
|
||||
C::Time(v) => v.map(format_time),
|
||||
C::DateTime(v) => v.map(|dt| {
|
||||
let ms = (u64::from(dt.seconds_fragments()) * 1000 + 150) / 300;
|
||||
format!("{} {}", format_date(DAYS_1900_01_01 + i64::from(dt.days())), format_hms(ms / 1000, ms % 1000, 3))
|
||||
}),
|
||||
C::SmallDateTime(v) => v.map(|dt| {
|
||||
let secs = u64::from(dt.seconds_fragments()) * 60;
|
||||
format!("{} {}", format_date(DAYS_1900_01_01 + i64::from(dt.days())), format_hms(secs, 0, 0))
|
||||
}),
|
||||
C::DateTime2(v) => {
|
||||
v.map(|dt| format!("{} {}", format_date(i64::from(dt.date().days())), format_time(dt.time())))
|
||||
}
|
||||
C::DateTimeOffset(v) => v.map(|dto| {
|
||||
let dt = dto.datetime2();
|
||||
let t = dt.time();
|
||||
let scale = t.scale().min(7);
|
||||
let div = 10u64.pow(u32::from(scale));
|
||||
let utc_secs = i64::from(dt.date().days()) * 86_400 + (t.increments() / div) as i64;
|
||||
let local_secs = utc_secs + i64::from(dto.offset()) * 60;
|
||||
let days = local_secs.div_euclid(86_400);
|
||||
let secs = local_secs.rem_euclid(86_400) as u64;
|
||||
let sign = if dto.offset() < 0 { '-' } else { '+' };
|
||||
let off = dto.offset().unsigned_abs();
|
||||
format!(
|
||||
"{} {}{sign}{:02}:{:02}",
|
||||
format_date(days),
|
||||
format_hms(secs, t.increments() % div, scale),
|
||||
off / 60,
|
||||
off % 60
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
let mut s = String::with_capacity(2 + bytes.len() * 2);
|
||||
s.push_str("0x");
|
||||
@@ -197,6 +364,37 @@ macro_rules! fetch {
|
||||
/// Columns, rows, rows affected, more rows beyond the limit, time of the first answer.
|
||||
type Fetched = (Vec<ResultColumn>, Vec<Vec<Option<String>>>, Option<u64>, bool, Option<Instant>);
|
||||
|
||||
async fn mssql_command(conn: &mut bb8_tiberius::rt::Client, sql: &str) -> IdeResult<()> {
|
||||
// Transaction control must be a batch: sp_executesql rejects a changed transaction count.
|
||||
conn.simple_query(sql).await.map_err(mssql_err)?.into_results().await.map_err(mssql_err)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mssql_fetch(conn: &mut bb8_tiberius::rt::Client, sql: &str, max: usize) -> IdeResult<Fetched> {
|
||||
let mut stream = conn.simple_query(sql).await.map_err(mssql_err)?;
|
||||
let first = Some(Instant::now());
|
||||
let columns = stream
|
||||
.columns()
|
||||
.await
|
||||
.map_err(mssql_err)?
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|col| ResultColumn { name: col.name().to_owned(), type_name: format!("{:?}", col.column_type()) })
|
||||
.collect();
|
||||
let mut rows = Vec::new();
|
||||
let mut has_more = false;
|
||||
while let Some(item) = stream.next().await {
|
||||
if let tiberius::QueryItem::Row(row) = item.map_err(mssql_err)? {
|
||||
if rows.len() >= max {
|
||||
has_more = true;
|
||||
break;
|
||||
}
|
||||
rows.push(row.cells().map(|(col, cell)| tiberius_cell(cell, col.column_type())).collect());
|
||||
}
|
||||
}
|
||||
Ok((columns, rows, None, has_more, first))
|
||||
}
|
||||
|
||||
/// The server-side id of a connection (to cancel its statement from another one).
|
||||
macro_rules! backend_id {
|
||||
($conn:expr, $sql:expr) => {{
|
||||
@@ -313,6 +511,7 @@ impl Pool {
|
||||
Pool::Sqlite(_) => DbDriver::Sqlite,
|
||||
Pool::Postgres(_) => DbDriver::Postgres,
|
||||
Pool::Mysql(_) => DbDriver::Mysql,
|
||||
Pool::SqlServer(_) => DbDriver::SqlServer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,20 +652,60 @@ impl Pool {
|
||||
.map_err(db_err)?;
|
||||
Ok(Pool::Mysql(pool))
|
||||
}
|
||||
DbDriver::SqlServer => {
|
||||
use tiberius::Config;
|
||||
let mut config = Config::new();
|
||||
config.host(&source.host);
|
||||
config.port(source.port.unwrap_or(1433));
|
||||
if !source.user.is_empty() {
|
||||
config.authentication(tiberius::AuthMethod::sql_server(
|
||||
source.user.clone(),
|
||||
password.unwrap_or_default().to_owned(),
|
||||
));
|
||||
}
|
||||
if !source.database.is_empty() {
|
||||
config.database(&source.database);
|
||||
}
|
||||
if matches!(source.ssl, DbSslMode::Disable | DbSslMode::Prefer) {
|
||||
config.trust_cert();
|
||||
}
|
||||
let manager = MssqlConnectionManager(bb8_tiberius::ConnectionManager::new(config));
|
||||
let pool = bb8::Pool::builder()
|
||||
.max_size(MAX)
|
||||
.connection_timeout(timeout)
|
||||
.build(manager)
|
||||
.await
|
||||
.map_err(mssql_err)?;
|
||||
let mut conn = pool.get_owned().await.map_err(mssql_pool_err)?;
|
||||
conn.query("SELECT 1", &[]).await.map_err(mssql_err)?.into_results().await.map_err(mssql_err)?;
|
||||
Ok(Pool::SqlServer(pool))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn close(&self) {
|
||||
with_pool!(self, p => p.close().await)
|
||||
match self {
|
||||
Pool::SqlServer(_) => {}
|
||||
_ => with_pool!(self, p => p.close().await),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_closed(&self) -> bool {
|
||||
with_pool!(self, p => p.is_closed())
|
||||
match self {
|
||||
Pool::SqlServer(_) => false,
|
||||
_ => with_pool!(self, p => p.is_closed()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Runs `sql` (no row limit beyond `max`), for introspection: cells are text (see [`Cells::Text`]).
|
||||
pub async fn rows(&self, sql: &str, max: usize) -> IdeResult<Vec<Vec<Option<String>>>> {
|
||||
let fetched: IdeResult<Fetched> = with_pool!(self, p => fetch!(p, sql, max, Cells::Text));
|
||||
let fetched: IdeResult<Fetched> = match self {
|
||||
Pool::SqlServer(p) => {
|
||||
let mut conn = p.get_owned().await.map_err(mssql_pool_err)?;
|
||||
mssql_fetch(&mut conn, sql, max).await
|
||||
}
|
||||
_ => with_pool!(self, p => fetch!(p, sql, max, Cells::Text)),
|
||||
};
|
||||
Ok(fetched?.1)
|
||||
}
|
||||
|
||||
@@ -485,21 +724,47 @@ impl Pool {
|
||||
/// Runs `statements` in one transaction; each must change `expect_one`
|
||||
/// row when flagged (table editor submits).
|
||||
pub async fn transaction(&self, statements: &[(String, bool)]) -> IdeResult<u64> {
|
||||
with_pool!(self, p => {
|
||||
let mut tx = p.begin().await.map_err(db_err)?;
|
||||
let mut total = 0;
|
||||
for (sql, expect_one) in statements {
|
||||
let done = sqlx::raw_sql(sqlx::AssertSqlSafe(sql.clone())).execute(&mut *tx).await.map_err(db_err)?;
|
||||
let n = done.rows_affected();
|
||||
// MySQL reports 0 for an UPDATE that leaves the row unchanged.
|
||||
if *expect_one && (n > 1 || (n == 0 && !matches!(self, Pool::Mysql(_)))) {
|
||||
return Err(IdeError::invalid(format!("{sql}: expected to change 1 row, changed {n}; nothing was saved")));
|
||||
match self {
|
||||
Pool::SqlServer(p) => {
|
||||
let mut conn = p.get_owned().await.map_err(mssql_pool_err)?;
|
||||
let result = async {
|
||||
mssql_command(&mut conn, "BEGIN TRAN").await?;
|
||||
let mut total = 0;
|
||||
for (sql, expect_one) in statements {
|
||||
let n = conn.execute(sql, &[]).await.map_err(mssql_err)?.total();
|
||||
if *expect_one && n != 1 {
|
||||
return Err(IdeError::invalid(format!(
|
||||
"{sql}: expected to change 1 row, changed {n}; nothing was saved"
|
||||
)));
|
||||
}
|
||||
total += n;
|
||||
}
|
||||
mssql_command(&mut conn, "COMMIT TRAN").await?;
|
||||
Ok(total)
|
||||
}
|
||||
total += n;
|
||||
.await;
|
||||
if result.is_err() && mssql_command(&mut conn, "IF @@TRANCOUNT > 0 ROLLBACK TRAN").await.is_err() {
|
||||
// A failed rollback must never return an open transaction to the pool.
|
||||
conn.0.take();
|
||||
}
|
||||
result
|
||||
}
|
||||
tx.commit().await.map_err(db_err)?;
|
||||
Ok(total)
|
||||
})
|
||||
_ => with_pool!(self, p => {
|
||||
let mut tx = p.begin().await.map_err(db_err)?;
|
||||
let mut total = 0;
|
||||
for (sql, expect_one) in statements {
|
||||
let done = sqlx::raw_sql(sqlx::AssertSqlSafe(sql.clone())).execute(&mut *tx).await.map_err(db_err)?;
|
||||
let n = done.rows_affected();
|
||||
// MySQL reports 0 for an UPDATE that leaves the row unchanged.
|
||||
if *expect_one && (n > 1 || (n == 0 && !matches!(self, Pool::Mysql(_)))) {
|
||||
return Err(IdeError::invalid(format!("{sql}: expected to change 1 row, changed {n}; nothing was saved")));
|
||||
}
|
||||
total += n;
|
||||
}
|
||||
tx.commit().await.map_err(db_err)?;
|
||||
Ok(total)
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -509,6 +774,7 @@ pub enum Held {
|
||||
Sqlite(PoolConnection<Sqlite>),
|
||||
Postgres(PoolConnection<Postgres>),
|
||||
Mysql(PoolConnection<MySql>),
|
||||
SqlServer(Box<bb8::PooledConnection<'static, MssqlConnectionManager>>),
|
||||
}
|
||||
|
||||
impl Held {
|
||||
@@ -517,18 +783,25 @@ impl Held {
|
||||
Pool::Sqlite(p) => Held::Sqlite(p.acquire().await.map_err(db_err)?),
|
||||
Pool::Postgres(p) => Held::Postgres(p.acquire().await.map_err(db_err)?),
|
||||
Pool::Mysql(p) => Held::Mysql(p.acquire().await.map_err(db_err)?),
|
||||
Pool::SqlServer(p) => Held::SqlServer(Box::new(p.get_owned().await.map_err(mssql_pool_err)?)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Runs a statement without results (`BEGIN`, `COMMIT`, `ROLLBACK`).
|
||||
pub async fn command(&mut self, sql: &str) -> IdeResult<()> {
|
||||
let sql = sqlx::AssertSqlSafe(sql.to_owned());
|
||||
match self {
|
||||
Held::Sqlite(c) => sqlx::raw_sql(sql).execute(&mut **c).await.map(drop),
|
||||
Held::Postgres(c) => sqlx::raw_sql(sql).execute(&mut **c).await.map(drop),
|
||||
Held::Mysql(c) => sqlx::raw_sql(sql).execute(&mut **c).await.map(drop),
|
||||
Held::SqlServer(c) => mssql_command(c, sql).await,
|
||||
other => {
|
||||
let sql = sqlx::AssertSqlSafe(sql.to_owned());
|
||||
match other {
|
||||
Held::Sqlite(c) => sqlx::raw_sql(sql).execute(&mut **c).await.map(drop),
|
||||
Held::Postgres(c) => sqlx::raw_sql(sql).execute(&mut **c).await.map(drop),
|
||||
Held::Mysql(c) => sqlx::raw_sql(sql).execute(&mut **c).await.map(drop),
|
||||
Held::SqlServer(_) => unreachable!(),
|
||||
}
|
||||
.map_err(db_err)
|
||||
}
|
||||
}
|
||||
.map_err(db_err)
|
||||
}
|
||||
|
||||
/// Closes the connection instead of returning it to the pool (its statement was
|
||||
@@ -542,6 +815,9 @@ impl Held {
|
||||
Held::Mysql(c) => {
|
||||
let _ = c.detach().close().await;
|
||||
}
|
||||
Held::SqlServer(mut c) => {
|
||||
c.0.take();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,6 +865,13 @@ impl Held {
|
||||
run!(c, Some("SELECT pg_backend_pid()"), |b: String| format!("SELECT pg_cancel_backend({b})"))
|
||||
}
|
||||
Held::Mysql(c) => run!(c, Some("SELECT CONNECTION_ID()"), |b: String| format!("KILL QUERY {b}")),
|
||||
Held::SqlServer(c) => {
|
||||
// Tiberius has no attention/cancel API; stop reading and discard the connection.
|
||||
tokio::select! {
|
||||
r = mssql_fetch(c, sql, max_rows) => r?,
|
||||
() = cancel.cancelled() => return Err(IdeError::cancelled()),
|
||||
}
|
||||
}
|
||||
};
|
||||
let (columns, rows, affected, has_more, first) = fetched;
|
||||
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||
@@ -608,6 +891,7 @@ impl Held {
|
||||
pub fn begin_statement(driver: DbDriver) -> &'static str {
|
||||
match driver {
|
||||
DbDriver::Mysql => "START TRANSACTION",
|
||||
DbDriver::SqlServer => "BEGIN TRAN",
|
||||
DbDriver::Sqlite | DbDriver::Postgres => "BEGIN",
|
||||
}
|
||||
}
|
||||
@@ -624,6 +908,78 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::source::{DbOptions, DbProperty};
|
||||
|
||||
#[test]
|
||||
fn sqlserver_cells_preserve_values_and_nulls() {
|
||||
use std::borrow::Cow;
|
||||
use tiberius::time::{Date, DateTime, DateTime2, DateTimeOffset, SmallDateTime, Time};
|
||||
use tiberius::{ColumnData as C, ColumnType as T, Uuid, numeric::Numeric};
|
||||
let dt = DateTime2::new(Date::new(693_595), Time::new(1234567, 7));
|
||||
let cases = [
|
||||
(C::U8(Some(255)), "255"),
|
||||
(C::I16(Some(-123)), "-123"),
|
||||
(C::I32(Some(-456)), "-456"),
|
||||
(C::I64(Some(i64::MAX)), "9223372036854775807"),
|
||||
(C::F32(Some(1.25)), "1.25"),
|
||||
(C::F64(Some(1.25)), "1.25"),
|
||||
(C::Bit(Some(true)), "true"),
|
||||
(C::String(Some(Cow::Borrowed("hello"))), "hello"),
|
||||
(C::Binary(Some(Cow::Borrowed(&[0, 0xAB, 0xFF]))), "0x00ABFF"),
|
||||
(
|
||||
C::Guid(Some(Uuid::parse_str("ABCDEF01-2345-6789-ABCD-EF0123456789").unwrap())),
|
||||
"abcdef01-2345-6789-abcd-ef0123456789",
|
||||
),
|
||||
(C::Numeric(Some(Numeric::new_with_scale(-123, 5))), "-0.00123"),
|
||||
(C::Numeric(Some(Numeric::new_with_scale(12300, 4))), "1.2300"),
|
||||
(
|
||||
C::Numeric(Some(Numeric::new_with_scale(10i128.pow(38) - 1, 0))),
|
||||
"99999999999999999999999999999999999999",
|
||||
),
|
||||
(C::Xml(Some(Cow::Owned(tiberius::xml::XmlData::new("<a/>")))), "<a/>"),
|
||||
(C::Date(Some(Date::new(0))), "0001-01-01"),
|
||||
(C::Date(Some(Date::new(3_652_058))), "9999-12-31"),
|
||||
(C::Time(Some(Time::new(452961234567, 7))), "12:34:56.1234567"),
|
||||
(C::DateTime(Some(DateTime::new(-1, 1))), "1899-12-31 00:00:00.003"),
|
||||
(C::DateTime(Some(DateTime::new(0, 2))), "1900-01-01 00:00:00.007"),
|
||||
(C::SmallDateTime(Some(SmallDateTime::new(0, 754))), "1900-01-01 12:34:00"),
|
||||
(C::DateTime2(Some(dt)), "1900-01-01 00:00:00.1234567"),
|
||||
(C::DateTimeOffset(Some(DateTimeOffset::new(dt, -60))), "1899-12-31 23:00:00.1234567-01:00"),
|
||||
(C::DateTimeOffset(Some(DateTimeOffset::new(dt, 330))), "1900-01-01 05:30:00.1234567+05:30"),
|
||||
];
|
||||
for (cell, expected) in cases {
|
||||
assert_eq!(tiberius_cell(&cell, T::Null).as_deref(), Some(expected), "{cell:?}");
|
||||
}
|
||||
for cell in [
|
||||
C::U8(None),
|
||||
C::I16(None),
|
||||
C::I32(None),
|
||||
C::I64(None),
|
||||
C::F32(None),
|
||||
C::F64(None),
|
||||
C::Bit(None),
|
||||
C::String(None),
|
||||
C::Guid(None),
|
||||
C::Binary(None),
|
||||
C::Numeric(None),
|
||||
C::Xml(None),
|
||||
C::Date(None),
|
||||
C::Time(None),
|
||||
C::DateTime(None),
|
||||
C::SmallDateTime(None),
|
||||
C::DateTime2(None),
|
||||
C::DateTimeOffset(None),
|
||||
] {
|
||||
assert_eq!(tiberius_cell(&cell, T::Null), None);
|
||||
}
|
||||
for ty in [T::Money, T::Money4] {
|
||||
for (v, expected) in
|
||||
[(0.0, "0.0000"), (1.2, "1.2000"), (-123.4567, "-123.4567"), (214748.3647, "214748.3647")]
|
||||
{
|
||||
assert_eq!(tiberius_cell(&C::F64(Some(v)), ty).as_deref(), Some(expected));
|
||||
}
|
||||
assert_eq!(tiberius_cell(&C::F64(None), ty), None);
|
||||
}
|
||||
}
|
||||
|
||||
fn prop(name: &str, value: &str) -> DbProperty {
|
||||
DbProperty { name: name.into(), value: value.into() }
|
||||
}
|
||||
|
||||
@@ -549,6 +549,7 @@ impl DbManager {
|
||||
DbDriver::Sqlite => "SELECT 'SQLite ' || sqlite_version()",
|
||||
DbDriver::Postgres => "SELECT version()",
|
||||
DbDriver::Mysql => "SELECT CONCAT(@@version_comment, ' ', VERSION())",
|
||||
DbDriver::SqlServer => "SELECT @@VERSION",
|
||||
};
|
||||
let v = pool.rows(version, 1).await.ok().and_then(|r| r.into_iter().next()?.into_iter().next()?);
|
||||
pool.close().await;
|
||||
@@ -821,6 +822,13 @@ impl DbManager {
|
||||
let ddl = cells(pool.rows(&q, 1).await?, 1).into_iter().next();
|
||||
ddl.map(|d| format!("{d};\n")).ok_or_else(missing)
|
||||
}
|
||||
DbDriver::SqlServer => {
|
||||
let obj = format!("{}.{}", schema, name).replace('\'', "''");
|
||||
let ddl = cells(pool.rows(&format!("SELECT OBJECT_DEFINITION(OBJECT_ID('{}'))", obj), 1).await?, 0)
|
||||
.into_iter()
|
||||
.next();
|
||||
ddl.map(|d| format!("{d};\n")).ok_or_else(missing)
|
||||
}
|
||||
DbDriver::Postgres => {
|
||||
let rel = pool.rows(&sql::postgres_relation(schema, name), 1).await?.into_iter().next();
|
||||
let (oid, kind) = match rel.as_deref() {
|
||||
|
||||
@@ -36,6 +36,7 @@ pub fn explain_statement(driver: DbDriver, statement: &str) -> String {
|
||||
DbDriver::Sqlite => format!("EXPLAIN QUERY PLAN {s}"),
|
||||
DbDriver::Postgres => format!("EXPLAIN (FORMAT JSON) {s}"),
|
||||
DbDriver::Mysql => format!("EXPLAIN {s}"),
|
||||
DbDriver::SqlServer => format!("SET SHOWPLAN_TEXT ON; {s}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +60,7 @@ pub fn parse(driver: DbDriver, result: &QueryResult) -> IdeResult<Vec<PlanNode>>
|
||||
let names: Vec<&str> = result.columns.iter().map(|c| c.name.as_str()).collect();
|
||||
Ok(mysql(&names, &result.rows))
|
||||
}
|
||||
DbDriver::SqlServer => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +199,7 @@ mod tests {
|
||||
assert_eq!(explain_statement(DbDriver::Sqlite, "select 1;"), "EXPLAIN QUERY PLAN select 1");
|
||||
assert_eq!(explain_statement(DbDriver::Postgres, " select 1 "), "EXPLAIN (FORMAT JSON) select 1");
|
||||
assert_eq!(explain_statement(DbDriver::Mysql, "select 1"), "EXPLAIN select 1");
|
||||
assert_eq!(explain_statement(DbDriver::SqlServer, "select 1"), "SET SHOWPLAN_TEXT ON; select 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -16,6 +16,8 @@ pub enum DbDriver {
|
||||
Postgres,
|
||||
/// MySQL and MariaDB.
|
||||
Mysql,
|
||||
/// Microsoft SQL Server.
|
||||
SqlServer,
|
||||
}
|
||||
|
||||
impl DbDriver {
|
||||
@@ -24,6 +26,7 @@ impl DbDriver {
|
||||
DbDriver::Sqlite => None,
|
||||
DbDriver::Postgres => Some(5432),
|
||||
DbDriver::Mysql => Some(3306),
|
||||
DbDriver::SqlServer => Some(1433),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,6 +251,7 @@ pub fn scheme_driver(scheme: &str) -> Option<DbDriver> {
|
||||
"postgres" | "postgresql" | "pgsql" | "redshift" | "cockroachdb" | "cockroach" | "yugabytedb"
|
||||
| "timescaledb" | "greenplum" => Some(DbDriver::Postgres),
|
||||
"mysql" | "mariadb" | "tidb" | "singlestore" | "memsql" | "oceanbase" => Some(DbDriver::Mysql),
|
||||
"sqlserver" | "mssql" | "mssqlserver" => Some(DbDriver::SqlServer),
|
||||
"sqlite" | "sqlite3" => Some(DbDriver::Sqlite),
|
||||
_ => None,
|
||||
}
|
||||
@@ -472,6 +476,14 @@ mod tests {
|
||||
assert_eq!(load(tmp.path()).unwrap(), vec![s]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_defaults() {
|
||||
assert_eq!(DbDriver::SqlServer.default_port(), Some(1433));
|
||||
assert_eq!(scheme_driver("sqlserver"), Some(DbDriver::SqlServer));
|
||||
assert_eq!(scheme_driver("mssql"), Some(DbDriver::SqlServer));
|
||||
assert_eq!(scheme_driver("mssqlserver"), Some(DbDriver::SqlServer));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_urls() {
|
||||
let u = parse_url("jdbc:postgresql://ann:s%[email protected]:5433/shop?sslmode=require&search_path=app").unwrap();
|
||||
|
||||
@@ -8,6 +8,7 @@ use specta::Type;
|
||||
pub fn quote_ident(driver: DbDriver, name: &str) -> String {
|
||||
match driver {
|
||||
DbDriver::Mysql => format!("`{}`", name.replace('`', "``")),
|
||||
DbDriver::SqlServer => format!("[{}]", name.replace(']', "]]")),
|
||||
_ => format!("\"{}\"", name.replace('"', "\"\"")),
|
||||
}
|
||||
}
|
||||
@@ -52,6 +53,7 @@ pub fn schemas(driver: DbDriver, system: bool) -> &'static str {
|
||||
WHERE schema_name NOT LIKE 'pg\\_toast%' AND schema_name NOT LIKE 'pg\\_temp%' ORDER BY 1"
|
||||
}
|
||||
DbDriver::Mysql if system => "SELECT schema_name FROM information_schema.schemata ORDER BY 1",
|
||||
DbDriver::SqlServer if system => "SELECT schema_name FROM information_schema.schemata ORDER BY 1",
|
||||
DbDriver::Postgres => {
|
||||
"SELECT schema_name FROM information_schema.schemata \
|
||||
WHERE schema_name NOT IN ('pg_catalog', 'information_schema') \
|
||||
@@ -61,6 +63,10 @@ pub fn schemas(driver: DbDriver, system: bool) -> &'static str {
|
||||
"SELECT schema_name FROM information_schema.schemata \
|
||||
WHERE schema_name NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys') ORDER BY 1"
|
||||
}
|
||||
DbDriver::SqlServer => {
|
||||
"SELECT schema_name FROM information_schema.schemata \
|
||||
WHERE schema_name NOT IN ('sys', 'information_schema', 'guest') ORDER BY 1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +89,11 @@ pub fn tables(driver: DbDriver, schema: &str) -> String {
|
||||
FROM information_schema.tables WHERE table_schema = {} ORDER BY 1",
|
||||
quote_literal(driver, schema)
|
||||
),
|
||||
DbDriver::SqlServer => format!(
|
||||
"SELECT table_name, CASE WHEN table_type LIKE '%VIEW%' THEN 'view' ELSE 'table' END \
|
||||
FROM information_schema.tables WHERE table_schema = {} ORDER BY 1",
|
||||
quote_literal(driver, schema)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +121,16 @@ pub fn columns(driver: DbDriver, schema: &str, table: &str) -> String {
|
||||
FROM information_schema.columns WHERE table_schema = {s} AND table_name = {t} \
|
||||
ORDER BY ordinal_position"
|
||||
),
|
||||
DbDriver::SqlServer => format!(
|
||||
"SELECT column_name, data_type, is_nullable, column_default, \
|
||||
CASE WHEN EXISTS (SELECT 1 FROM information_schema.table_constraints tc \
|
||||
JOIN information_schema.key_column_usage k ON k.constraint_name = tc.constraint_name \
|
||||
AND k.table_schema = tc.table_schema AND k.table_name = tc.table_name \
|
||||
WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = {s} \
|
||||
AND tc.table_name = {t} AND k.column_name = c.column_name) THEN 1 ELSE 0 END \
|
||||
FROM information_schema.columns c WHERE c.table_schema = {s} AND c.table_name = {t} \
|
||||
ORDER BY c.ordinal_position"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +155,13 @@ pub fn indexes(driver: DbDriver, schema: &str, table: &str) -> String {
|
||||
FROM information_schema.statistics WHERE table_schema = {s} AND table_name = {t} \
|
||||
ORDER BY index_name, seq_in_index"
|
||||
),
|
||||
DbDriver::SqlServer => format!(
|
||||
"SELECT i.name, CASE WHEN i.is_unique = 1 THEN 1 ELSE 0 END, c.name \
|
||||
FROM sys.indexes i JOIN sys.index_columns ic ON ic.object_id = i.object_id AND ic.index_id = i.index_id \
|
||||
JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id \
|
||||
WHERE OBJECT_SCHEMA_NAME(i.object_id) = {s} AND OBJECT_NAME(i.object_id) = {t} AND i.name IS NOT NULL \
|
||||
ORDER BY i.name, ic.key_ordinal"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +199,20 @@ pub fn completion_columns(driver: DbDriver) -> &'static str {
|
||||
WHERE c.table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys') \
|
||||
ORDER BY c.table_schema, c.table_name, c.ordinal_position LIMIT 50000"
|
||||
}
|
||||
DbDriver::SqlServer => {
|
||||
"SELECT c.table_schema, c.table_name, CASE WHEN t.table_type LIKE '%VIEW%' THEN 'view' ELSE 'table' END, \
|
||||
c.column_name, c.data_type, c.is_nullable, \
|
||||
CASE WHEN EXISTS (SELECT 1 FROM information_schema.table_constraints tc \
|
||||
JOIN information_schema.key_column_usage k ON k.constraint_name = tc.constraint_name \
|
||||
AND k.table_schema = tc.table_schema AND k.table_name = tc.table_name \
|
||||
WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = c.table_schema \
|
||||
AND tc.table_name = c.table_name AND k.column_name = c.column_name) THEN 1 ELSE 0 END, \
|
||||
CASE WHEN c.column_default IS NOT NULL THEN 1 ELSE 0 END \
|
||||
FROM information_schema.columns c JOIN information_schema.tables t \
|
||||
ON t.table_schema = c.table_schema AND t.table_name = c.table_name \
|
||||
WHERE c.table_schema NOT IN ('sys', 'information_schema') \
|
||||
ORDER BY c.table_schema, c.table_name, c.ordinal_position"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,6 +244,13 @@ pub fn foreign_keys(driver: DbDriver) -> &'static str {
|
||||
AND table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys') \
|
||||
ORDER BY table_schema, table_name, constraint_name, ordinal_position LIMIT 20000"
|
||||
}
|
||||
DbDriver::SqlServer => {
|
||||
"SELECT table_schema, table_name, constraint_name, column_name, \
|
||||
referenced_table_schema, referenced_table_name, referenced_column_name \
|
||||
FROM information_schema.key_column_usage WHERE referenced_table_name IS NOT NULL \
|
||||
AND table_schema NOT IN ('sys', 'information_schema') \
|
||||
ORDER BY table_schema, table_name, constraint_name, ordinal_position"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,6 +267,9 @@ pub fn table_foreign_keys(driver: DbDriver, schema: &str, table: &str) -> String
|
||||
DbDriver::Mysql => format!(
|
||||
"SELECT table_schema, table_name, constraint_name, column_name, referenced_table_schema, referenced_table_name, referenced_column_name FROM information_schema.key_column_usage WHERE referenced_table_name IS NOT NULL AND table_schema = {s} AND table_name = {t} ORDER BY constraint_name, ordinal_position"
|
||||
),
|
||||
DbDriver::SqlServer => format!(
|
||||
"SELECT table_schema, table_name, constraint_name, column_name, referenced_table_schema, referenced_table_name, referenced_column_name FROM information_schema.key_column_usage WHERE referenced_table_name IS NOT NULL AND table_schema = {s} AND table_name = {t} ORDER BY constraint_name, ordinal_position"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,6 +279,7 @@ pub fn current_schema(driver: DbDriver) -> Option<&'static str> {
|
||||
DbDriver::Sqlite => None,
|
||||
DbDriver::Postgres => Some("SELECT current_schema()"),
|
||||
DbDriver::Mysql => Some("SELECT DATABASE()"),
|
||||
DbDriver::SqlServer => Some("SELECT DB_NAME()"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,8 +303,14 @@ pub fn table_page(
|
||||
) -> String {
|
||||
let mut sql = format!("SELECT * FROM {}", qualified(driver, schema, table));
|
||||
if let Some(f) = filter.map(str::trim).filter(|f| !f.is_empty()) {
|
||||
let f = f.strip_prefix("WHERE ").or_else(|| f.strip_prefix("where ")).unwrap_or(f);
|
||||
sql.push_str(&format!(" WHERE {f}"));
|
||||
let mut f = f.strip_prefix("WHERE ").or_else(|| f.strip_prefix("where ")).unwrap_or(f);
|
||||
if let Some(i) = f.find(';') {
|
||||
f = &f[..i];
|
||||
}
|
||||
f = f.trim();
|
||||
if !f.is_empty() {
|
||||
sql.push_str(&format!(" WHERE {f}"));
|
||||
}
|
||||
}
|
||||
if !sort.is_empty() {
|
||||
let keys: Vec<String> = sort
|
||||
@@ -260,7 +319,14 @@ pub fn table_page(
|
||||
.collect();
|
||||
sql.push_str(&format!(" ORDER BY {}", keys.join(", ")));
|
||||
}
|
||||
sql.push_str(&format!(" LIMIT {} OFFSET {offset}", limit + 1));
|
||||
if driver == DbDriver::SqlServer {
|
||||
if sort.is_empty() {
|
||||
sql.push_str(" ORDER BY (SELECT 1)");
|
||||
}
|
||||
sql.push_str(&format!(" OFFSET {offset} ROWS FETCH NEXT {} ROWS ONLY", limit + 1));
|
||||
} else {
|
||||
sql.push_str(&format!(" LIMIT {} OFFSET {offset}", limit + 1));
|
||||
}
|
||||
sql
|
||||
}
|
||||
|
||||
@@ -400,6 +466,10 @@ pub fn triggers(driver: DbDriver, schema: &str, table: &str) -> String {
|
||||
"SELECT trigger_name, lower(action_timing), lower(event_manipulation) FROM information_schema.triggers \
|
||||
WHERE event_object_schema = {s} AND event_object_table = {t} ORDER BY 1"
|
||||
),
|
||||
DbDriver::SqlServer => format!(
|
||||
"SELECT t.name, lower(te.type_desc), '' FROM sys.triggers t \
|
||||
WHERE OBJECT_SCHEMA_NAME(t.parent_id) = {s} AND OBJECT_NAME(t.parent_id) = {t} ORDER BY 1"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,6 +508,10 @@ pub fn routines(driver: DbDriver, schema: &str) -> Option<String> {
|
||||
AND p.specific_name = r.specific_name AND p.ordinal_position > 0), r.dtd_identifier \
|
||||
FROM information_schema.routines r WHERE r.routine_schema = {s} ORDER BY 1"
|
||||
)),
|
||||
DbDriver::SqlServer => Some(format!(
|
||||
"SELECT r.routine_name, lower(r.routine_type), '', r.data_type \
|
||||
FROM information_schema.routines r WHERE r.routine_schema = {s} ORDER BY 1"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,6 +633,10 @@ mod tests {
|
||||
"SELECT * FROM \"public\".\"users\" WHERE age > 3 ORDER BY \"id\" DESC LIMIT 51 OFFSET 100"
|
||||
);
|
||||
assert_eq!(table_page(DbDriver::Mysql, "", "t", Some(" "), &[], 0, 10), "SELECT * FROM `t` LIMIT 11 OFFSET 0");
|
||||
assert_eq!(
|
||||
table_page(DbDriver::SqlServer, "dbo", "users", Some("id = 1;"), &[], 0, 500),
|
||||
"SELECT * FROM [dbo].[users] WHERE id = 1 ORDER BY (SELECT 1) OFFSET 0 ROWS FETCH NEXT 501 ROWS ONLY"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -586,6 +664,30 @@ mod tests {
|
||||
assert!(edit_statement(DbDriver::Postgres, "s", "t", &DbRowEdit::Delete { key: vec![] }).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_quoting_and_schema() {
|
||||
assert_eq!(quote_ident(DbDriver::SqlServer, "a]b"), "[a]]b]");
|
||||
assert_eq!(qualified(DbDriver::SqlServer, "dbo", "t"), "[dbo].[t]");
|
||||
assert_eq!(qualified(DbDriver::SqlServer, "", "t"), "[t]");
|
||||
assert_eq!(current_schema(DbDriver::SqlServer), Some("SELECT DB_NAME()"));
|
||||
assert_eq!(crate::conn::begin_statement(DbDriver::SqlServer), "BEGIN TRAN");
|
||||
assert_eq!(
|
||||
schemas(DbDriver::SqlServer, true),
|
||||
"SELECT schema_name FROM information_schema.schemata ORDER BY 1"
|
||||
);
|
||||
assert!(schemas(DbDriver::SqlServer, false).contains("NOT IN ('sys', 'information_schema', 'guest')"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlserver_introspection_queries() {
|
||||
assert!(tables(DbDriver::SqlServer, "dbo").contains("information_schema.tables"));
|
||||
assert!(columns(DbDriver::SqlServer, "dbo", "t").contains("information_schema.columns"));
|
||||
assert!(indexes(DbDriver::SqlServer, "dbo", "t").contains("sys.indexes"));
|
||||
assert!(foreign_keys(DbDriver::SqlServer).contains("referenced_table_name IS NOT NULL"));
|
||||
assert!(table_foreign_keys(DbDriver::SqlServer, "dbo", "t").contains("table_schema = 'dbo'"));
|
||||
assert!(completion_columns(DbDriver::SqlServer).contains("information_schema.columns"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlite_trigger_events() {
|
||||
let ev = sqlite_trigger_event;
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
//! Live SQL Server checks, skipped unless OPENIDEA_TEST_SQLSERVER=host:port:user:password
|
||||
//! names a throwaway server.
|
||||
|
||||
use ide_core::CancellationToken;
|
||||
use ide_db::{DataSource, DbDriver, DbSslMode, Held, Pool};
|
||||
use std::time::Duration;
|
||||
|
||||
#[tokio::test]
|
||||
async fn sqlserver_values_paging_transactions_and_cancellation() {
|
||||
let Ok(server) = std::env::var("OPENIDEA_TEST_SQLSERVER") else { return };
|
||||
let mut parts = server.splitn(4, ':');
|
||||
let source = DataSource {
|
||||
driver: DbDriver::SqlServer,
|
||||
host: parts.next().unwrap().into(),
|
||||
port: Some(parts.next().unwrap().parse().unwrap()),
|
||||
user: parts.next().unwrap().into(),
|
||||
database: "tempdb".into(),
|
||||
ssl: DbSslMode::Disable,
|
||||
..Default::default()
|
||||
};
|
||||
let pool = Pool::connect(&source, parts.next(), None).await.unwrap();
|
||||
let cancel = CancellationToken::new();
|
||||
let mut held = Held::acquire(&pool).await.unwrap();
|
||||
let table = format!("tempdb.dbo.oi_pr276_values_{}", std::process::id());
|
||||
held.command(&format!("CREATE TABLE {table} (id int PRIMARY KEY, d decimal(38, 7), dt datetime2(7), guid uniqueidentifier, m money, tiny tinyint, x xml)")).await.unwrap();
|
||||
held.command(&format!("INSERT INTO {table} VALUES (1, 12345678901234567890.1234567, '2024-02-29T12:34:56.1234567', 'abcdef01-2345-6789-abcd-ef0123456789', 12.3400, 255, '<a/>'), (2, NULL, NULL, NULL, NULL, NULL, NULL)")).await.unwrap();
|
||||
let r = held.execute(&pool, &format!("SELECT * FROM {table} ORDER BY id"), 1, &cancel).await.unwrap();
|
||||
assert!(r.has_more);
|
||||
assert_eq!(
|
||||
r.rows[0],
|
||||
[
|
||||
"1",
|
||||
"12345678901234567890.1234567",
|
||||
"2024-02-29 12:34:56.1234567",
|
||||
"abcdef01-2345-6789-abcd-ef0123456789",
|
||||
"12.3400",
|
||||
"255",
|
||||
"<a/>"
|
||||
]
|
||||
.map(|s| Some(s.to_owned()))
|
||||
);
|
||||
let r = held.execute(&pool, &format!("SELECT * FROM {table} WHERE id = 2"), 5, &cancel).await.unwrap();
|
||||
assert!(!r.has_more);
|
||||
assert!(r.rows[0][1..].iter().all(Option::is_none));
|
||||
let r = held.execute(&pool, &format!("SELECT * FROM {table} WHERE id = 3"), 5, &cancel).await.unwrap();
|
||||
assert_eq!(r.columns.len(), 7);
|
||||
assert!(r.rows.is_empty());
|
||||
let r = held.execute(&pool,
|
||||
"SELECT CAST('2024-02-29T00:15:00.1234567+05:30' AS datetimeoffset(7)), CAST('2024-02-29' AS date), CAST('12:34:56.1234567' AS time(7)), CAST('2024-02-29T12:34:56.007' AS datetime), CAST('2024-02-29T12:34:00' AS smalldatetime), CAST(-12.3400 AS smallmoney)",
|
||||
1, &cancel).await.unwrap();
|
||||
assert_eq!(
|
||||
r.rows[0],
|
||||
[
|
||||
"2024-02-29 00:15:00.1234567+05:30",
|
||||
"2024-02-29",
|
||||
"12:34:56.1234567",
|
||||
"2024-02-29 12:34:56.007",
|
||||
"2024-02-29 12:34:00",
|
||||
"-12.3400"
|
||||
]
|
||||
.map(|s| Some(s.to_owned()))
|
||||
);
|
||||
held.command(&format!("DROP TABLE {table}")).await.unwrap();
|
||||
drop(held);
|
||||
|
||||
// A unique permanent table lets transaction calls acquire any pooled connection.
|
||||
let table = format!("tempdb.dbo.oi_pr276_{}", std::process::id());
|
||||
pool.execute(&format!("CREATE TABLE {table} (id int PRIMARY KEY, v int)"), 1, &cancel).await.unwrap();
|
||||
pool.transaction(&[(format!("INSERT INTO {table} VALUES (1, 10)"), true)]).await.unwrap();
|
||||
for failing in [format!("INSERT INTO {table} VALUES (1, 20)"), format!("UPDATE {table} SET v = 30 WHERE id = 999")]
|
||||
{
|
||||
assert!(
|
||||
pool.transaction(&[(format!("UPDATE {table} SET v = 99 WHERE id = 1"), true), (failing, true)])
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(pool.rows(&format!("SELECT v FROM {table}"), 1).await.unwrap()[0][0].as_deref(), Some("10"));
|
||||
assert_eq!(pool.rows("SELECT @@TRANCOUNT", 1).await.unwrap()[0][0].as_deref(), Some("0"));
|
||||
}
|
||||
assert_eq!(pool.transaction(&[(format!("UPDATE {table} SET v = 11 WHERE id = 1"), true)]).await.unwrap(), 1);
|
||||
pool.execute(&format!("DROP TABLE {table}"), 1, &cancel).await.unwrap();
|
||||
|
||||
let cancel = CancellationToken::new();
|
||||
let stop = cancel.clone();
|
||||
let task = tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
stop.cancel();
|
||||
});
|
||||
assert!(
|
||||
tokio::time::timeout(Duration::from_secs(2), pool.execute("WAITFOR DELAY '00:00:10'; SELECT 1", 1, &cancel))
|
||||
.await
|
||||
.unwrap()
|
||||
.is_err()
|
||||
);
|
||||
task.await.unwrap();
|
||||
assert_eq!(pool.rows("SELECT 1", 1).await.unwrap()[0][0].as_deref(), Some("1"));
|
||||
}
|
||||
@@ -2039,7 +2039,9 @@ export type DbDatabases = {
|
||||
|
||||
export type DbDriver = "sqlite" | "postgres" |
|
||||
/** MySQL and MariaDB. */
|
||||
"mysql";
|
||||
"mysql" |
|
||||
/** Microsoft SQL Server. */
|
||||
"sqlServer";
|
||||
|
||||
/** `schema.table (columns) REFERENCES ref_schema.ref_table (ref_columns)`. */
|
||||
export type DbForeignKey = {
|
||||
|
||||
@@ -250,6 +250,25 @@ const TYPES: Record<DbDriver, string[]> = {
|
||||
"json",
|
||||
"blob",
|
||||
],
|
||||
sqlServer: [
|
||||
"int",
|
||||
"bigint",
|
||||
"smallint",
|
||||
"tinyint",
|
||||
"decimal(10,2)",
|
||||
"float",
|
||||
"real",
|
||||
"varchar(255)",
|
||||
"nvarchar(255)",
|
||||
"text",
|
||||
"ntext",
|
||||
"date",
|
||||
"datetime",
|
||||
"datetime2",
|
||||
"uniqueidentifier",
|
||||
"bit",
|
||||
"varbinary(max)",
|
||||
],
|
||||
};
|
||||
|
||||
interface ModifyProps {
|
||||
|
||||
@@ -93,6 +93,10 @@ const PROPERTIES: Record<string, { names: string[]; hint: string }> = {
|
||||
names: ["foreign_keys", "busy_timeout", "cache_size", "journal_mode", "synchronous"],
|
||||
hint: "Applied as pragmas when opening the file (PRAGMA name = value).",
|
||||
},
|
||||
sqlServer: {
|
||||
names: ["connect_timeout"],
|
||||
hint: "Connection settings for SQL Server.",
|
||||
},
|
||||
};
|
||||
|
||||
const blankSsh = (): DbSsh => ({
|
||||
|
||||
@@ -184,7 +184,7 @@ describe("data source types", () => {
|
||||
});
|
||||
expect(vendorPreset(vendorById("postgres")!)).toEqual({ driver: "postgres", vendor: "", port: 5432 });
|
||||
expect(vendorPreset(vendorById("supabase")!).ssl).toBe("require");
|
||||
expect(VENDORS.every((v) => ["postgres", "mysql", "sqlite"].includes(v.driver))).toBe(true);
|
||||
expect(VENDORS.every((v) => ["postgres", "mysql", "sqlite", "sqlServer"].includes(v.driver))).toBe(true);
|
||||
const labels = VENDORS.map((v) => v.label);
|
||||
expect(labels).toEqual([...labels].sort((a, b) => a.localeCompare(b)));
|
||||
});
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
import type { DbColumnInfo, DbDriver } from "@/platform/ipc";
|
||||
|
||||
export function quoteIdent(driver: DbDriver, name: string): string {
|
||||
return driver === "mysql" ? `\`${name.replace(/`/g, "``")}\`` : `"${name.replace(/"/g, '""')}"`;
|
||||
if (driver === "mysql") return `\`${name.replace(/`/g, "``")}\``;
|
||||
if (driver === "sqlServer") return `[${name.replace(/]/g, "]]")}]`;
|
||||
return `"${name.replace(/"/g, '""')}"`;
|
||||
}
|
||||
|
||||
/** Quotes only when needed: lower-case (PostgreSQL) / plain words stay bare, like IDEA's generated SQL. */
|
||||
@@ -132,7 +134,7 @@ export function truncateSql(
|
||||
): string {
|
||||
const q = qualifiedName(driver, schema, table);
|
||||
if (driver === "sqlite") return `delete from ${q};`;
|
||||
if (driver === "mysql") return `truncate table ${q};`;
|
||||
if (driver === "mysql" || driver === "sqlServer") return `truncate table ${q};`;
|
||||
return `truncate table ${q}${opts.restartIdentity ? " restart identity" : ""}${opts.cascade ? " cascade" : ""};`;
|
||||
}
|
||||
|
||||
|
||||
@@ -154,6 +154,29 @@ export const DATA_TYPES: Record<DbDriver, string[]> = {
|
||||
"BOOLEAN",
|
||||
"ENUM",
|
||||
],
|
||||
sqlServer: [
|
||||
"INT",
|
||||
"BIGINT",
|
||||
"SMALLINT",
|
||||
"TINYINT",
|
||||
"VARCHAR",
|
||||
"NVARCHAR",
|
||||
"CHAR",
|
||||
"NCHAR",
|
||||
"TEXT",
|
||||
"NTEXT",
|
||||
"DECIMAL",
|
||||
"FLOAT",
|
||||
"REAL",
|
||||
"DATE",
|
||||
"DATETIME",
|
||||
"DATETIME2",
|
||||
"TIME",
|
||||
"UNIQUEIDENTIFIER",
|
||||
"BIT",
|
||||
"VARBINARY",
|
||||
"XML",
|
||||
],
|
||||
};
|
||||
|
||||
const f = (name: string, params: string, returns?: string, aggregate?: boolean): SqlFunction => ({
|
||||
@@ -260,6 +283,20 @@ const FUNCTIONS: Record<DbDriver, SqlFunction[]> = {
|
||||
f("row_number", "", "bigint"),
|
||||
f("rank", "", "bigint"),
|
||||
],
|
||||
sqlServer: [
|
||||
...COMMON,
|
||||
f("getdate", "", "datetime"),
|
||||
f("getutcdate", "", "datetime"),
|
||||
f("dateadd", "part, n, date", "datetime"),
|
||||
f("datediff", "part, a, b", "int"),
|
||||
f("isnull", "a, b"),
|
||||
f("iif", "cond, then, else"),
|
||||
f("concat", "value, ...", "text"),
|
||||
f("format", "value, format", "text"),
|
||||
f("newid", "", "uniqueidentifier"),
|
||||
f("row_number", "", "bigint"),
|
||||
f("rank", "", "bigint"),
|
||||
],
|
||||
};
|
||||
|
||||
export function functions(driver: DbDriver): SqlFunction[] {
|
||||
|
||||
@@ -38,6 +38,14 @@ export const VENDORS: readonly DbVendor[] = [
|
||||
},
|
||||
{ id: "greenplum", label: "Greenplum", driver: "postgres", port: 5432, icon: "db-greenplum", scheme: "postgresql" },
|
||||
{ id: "mariadb", label: "MariaDB", driver: "mysql", port: 3306, icon: "db-mariadb", scheme: "mariadb" },
|
||||
{
|
||||
id: "sqlServer",
|
||||
label: "Microsoft SQL Server",
|
||||
driver: "sqlServer",
|
||||
port: 1433,
|
||||
icon: "db-sqlserver",
|
||||
scheme: "sqlserver",
|
||||
},
|
||||
{ id: "mysql", label: "MySQL", driver: "mysql", port: 3306, icon: "db-mysql", scheme: "mysql" },
|
||||
{ id: "neon", label: "Neon", driver: "postgres", port: 5432, icon: "db-neon", scheme: "postgresql", ssl: "require" },
|
||||
{
|
||||
@@ -113,6 +121,8 @@ const SCHEMES: Record<string, string> = {
|
||||
singlestore: "singlestore",
|
||||
memsql: "singlestore",
|
||||
oceanbase: "oceanbase",
|
||||
sqlserver: "sqlServer",
|
||||
mssql: "sqlServer",
|
||||
};
|
||||
|
||||
export const vendorOfScheme = (scheme: string): DbVendor | undefined => BY_ID.get(SCHEMES[scheme.toLowerCase()] ?? "");
|
||||
|
||||
@@ -4737,6 +4737,8 @@
|
||||
"Interpreter Settings…": "解释器设置…",
|
||||
"Cannot select Python interpreter": "无法选择 Python 解释器",
|
||||
"Python Interpreter: {label}": "Python 解释器:{label}",
|
||||
"Connection settings for SQL Server.": "SQL Server 的连接设置。",
|
||||
"Microsoft SQL Server": "微软 SQL Server",
|
||||
"Add to .gitignore": "添加到 .gitignore",
|
||||
"Add to .git/info/exclude": "添加到 .git/info/exclude",
|
||||
"Delete Agent Tasks": "删除 Agent 任务",
|
||||
|
||||
@@ -280,6 +280,7 @@ const ICONS = {
|
||||
"db-sqlite": "openidea/database/sqlite",
|
||||
"db-postgres": "openidea/database/postgres",
|
||||
"db-mysql": "openidea/database/mysql",
|
||||
"db-sqlserver": "openidea/database/sqlserver",
|
||||
"db-mariadb": "openidea/database/mariadb",
|
||||
"db-aurora": "openidea/database/aurora",
|
||||
"db-redshift": "openidea/database/redshift",
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_8666_6627)">
|
||||
<g clip-path="url(#clip1_8666_6627)">
|
||||
<mask id="mask0_8666_6627" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="48" height="48">
|
||||
<path d="M48 0H0V48H48V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_8666_6627)">
|
||||
<path d="M23.9982 33.6C34.6019 33.6 43.1982 36.8234 43.1982 40.8V26.3998C43.1982 22.4234 34.6019 19.1998 23.9982 19.1998H19.1982V33.6H23.9982Z" fill="url(#paint0_linear_8666_6627)"/>
|
||||
<path d="M43.1988 26.3998C43.1988 30.3763 34.6028 33.5997 23.9988 33.5997C17.5318 33.5997 11.8116 32.4008 8.33267 30.5632C6.44883 29.728 4.79883 30.9936 4.79883 32.7091V40.7997C4.79883 44.7763 13.395 47.9997 23.9988 47.9997C34.6028 47.9997 43.1988 44.7763 43.1988 40.7997V26.3998Z" fill="url(#paint1_radial_8666_6627)"/>
|
||||
<path d="M43.1988 26.3998C43.1988 30.3763 34.6028 33.5997 23.9988 33.5997C17.5318 33.5997 11.8116 32.4008 8.33267 30.5632C6.44883 29.728 4.79883 30.9936 4.79883 32.7091V40.7997C4.79883 44.7763 13.395 47.9997 23.9988 47.9997C34.6028 47.9997 43.1988 44.7763 43.1988 40.7997V26.3998Z" fill="url(#paint2_radial_8666_6627)"/>
|
||||
<path d="M23.9988 0C34.6026 0 43.1989 3.22355 43.1989 7.2V15.3562C43.1989 16.4719 42.0362 18.4313 39.6621 17.4349C36.1826 15.5983 30.4639 14.4 23.9988 14.4C13.395 14.4 4.79883 17.6235 4.79883 21.6V7.2C4.79883 3.22355 13.395 0 23.9988 0Z" fill="url(#paint3_linear_8666_6627)"/>
|
||||
<path d="M23.9988 14.3998C13.395 14.3998 4.79883 11.1763 4.79883 7.19983V21.5998C4.79883 25.5763 13.395 28.8 23.9988 28.8H25.1988C27.1871 28.8 28.7989 27.188 28.7989 25.1998V17.9998C28.7989 16.0116 27.1871 14.3998 25.1988 14.3998H23.9988Z" fill="url(#paint4_radial_8666_6627)"/>
|
||||
<path d="M23.9988 14.3998C13.395 14.3998 4.79883 11.1763 4.79883 7.19983V21.5998C4.79883 25.5763 13.395 28.8 23.9988 28.8H25.1988C27.1871 28.8 28.7989 27.188 28.7989 25.1998V17.9998C28.7989 16.0116 27.1871 14.3998 25.1988 14.3998H23.9988Z" fill="url(#paint5_radial_8666_6627)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_8666_6627" x1="35.9982" y1="19.1998" x2="15.9334" y2="35.8194" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0094F0"/>
|
||||
<stop offset="0.243047" stop-color="#0078D4"/>
|
||||
<stop offset="0.584404" stop-color="#2052CB"/>
|
||||
<stop offset="0.830639" stop-color="#312A9A"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint1_radial_8666_6627" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(33.5988 29.5499) rotate(143.591) scale(29.8211 53.0152)">
|
||||
<stop stop-color="#3BD5FF"/>
|
||||
<stop offset="1" stop-color="#0078D4"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint2_radial_8666_6627" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39.5988 27.5997) rotate(146.31) scale(25.96 43.796)">
|
||||
<stop stop-color="#DECBFF" stop-opacity="0.9"/>
|
||||
<stop offset="1" stop-color="#DECBFF" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint3_linear_8666_6627" x1="32.3989" y1="1.2" x2="9.66835" y2="25.9489" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0FAFFF"/>
|
||||
<stop offset="0.162714" stop-color="#0094F0"/>
|
||||
<stop offset="0.584404" stop-color="#2052CB"/>
|
||||
<stop offset="0.830639" stop-color="#312A9A"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint4_radial_8666_6627" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(23.6678 12.1488) rotate(137.53) scale(24.6606 38.4117)">
|
||||
<stop stop-color="#3BD5FF"/>
|
||||
<stop offset="1" stop-color="#D1D1FF"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint5_radial_8666_6627" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(27.5989 10.7998) rotate(149.036) scale(20.9914 33.6005)">
|
||||
<stop stop-color="#DECBFF" stop-opacity="0.9"/>
|
||||
<stop offset="1" stop-color="#D1D1FF" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<clipPath id="clip0_8666_6627">
|
||||
<path d="M0 12.8C0 8.31958 0 6.07937 0.871948 4.36808C1.63893 2.86278 2.86278 1.63893 4.36808 0.871948C6.07937 0 8.31958 0 12.8 0H35.2C39.6804 0 41.9206 0 43.6319 0.871948C45.1372 1.63893 46.3611 2.86278 47.1281 4.36808C48 6.07937 48 8.31958 48 12.8V35.2C48 39.6804 48 41.9206 47.1281 43.6319C46.3611 45.1372 45.1372 46.3611 43.6319 47.1281C41.9206 48 39.6804 48 35.2 48H12.8C8.31958 48 6.07937 48 4.36808 47.1281C2.86278 46.3611 1.63893 45.1372 0.871948 43.6319C0 41.9206 0 39.6804 0 35.2V12.8Z" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_8666_6627">
|
||||
<rect width="48" height="48" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Після Ширина: | Висота: | Розмір: 4.4 KiB |
@@ -0,0 +1,53 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_8666_6627)">
|
||||
<g clip-path="url(#clip1_8666_6627)">
|
||||
<mask id="mask0_8666_6627" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="48" height="48">
|
||||
<path d="M48 0H0V48H48V0Z" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_8666_6627)">
|
||||
<path d="M23.9982 33.6C34.6019 33.6 43.1982 36.8234 43.1982 40.8V26.3998C43.1982 22.4234 34.6019 19.1998 23.9982 19.1998H19.1982V33.6H23.9982Z" fill="url(#paint0_linear_8666_6627)"/>
|
||||
<path d="M43.1988 26.3998C43.1988 30.3763 34.6028 33.5997 23.9988 33.5997C17.5318 33.5997 11.8116 32.4008 8.33267 30.5632C6.44883 29.728 4.79883 30.9936 4.79883 32.7091V40.7997C4.79883 44.7763 13.395 47.9997 23.9988 47.9997C34.6028 47.9997 43.1988 44.7763 43.1988 40.7997V26.3998Z" fill="url(#paint1_radial_8666_6627)"/>
|
||||
<path d="M43.1988 26.3998C43.1988 30.3763 34.6028 33.5997 23.9988 33.5997C17.5318 33.5997 11.8116 32.4008 8.33267 30.5632C6.44883 29.728 4.79883 30.9936 4.79883 32.7091V40.7997C4.79883 44.7763 13.395 47.9997 23.9988 47.9997C34.6028 47.9997 43.1988 44.7763 43.1988 40.7997V26.3998Z" fill="url(#paint2_radial_8666_6627)"/>
|
||||
<path d="M23.9988 0C34.6026 0 43.1989 3.22355 43.1989 7.2V15.3562C43.1989 16.4719 42.0362 18.4313 39.6621 17.4349C36.1826 15.5983 30.4639 14.4 23.9988 14.4C13.395 14.4 4.79883 17.6235 4.79883 21.6V7.2C4.79883 3.22355 13.395 0 23.9988 0Z" fill="url(#paint3_linear_8666_6627)"/>
|
||||
<path d="M23.9988 14.3998C13.395 14.3998 4.79883 11.1763 4.79883 7.19983V21.5998C4.79883 25.5763 13.395 28.8 23.9988 28.8H25.1988C27.1871 28.8 28.7989 27.188 28.7989 25.1998V17.9998C28.7989 16.0116 27.1871 14.3998 25.1988 14.3998H23.9988Z" fill="url(#paint4_radial_8666_6627)"/>
|
||||
<path d="M23.9988 14.3998C13.395 14.3998 4.79883 11.1763 4.79883 7.19983V21.5998C4.79883 25.5763 13.395 28.8 23.9988 28.8H25.1988C27.1871 28.8 28.7989 27.188 28.7989 25.1998V17.9998C28.7989 16.0116 27.1871 14.3998 25.1988 14.3998H23.9988Z" fill="url(#paint5_radial_8666_6627)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_8666_6627" x1="35.9982" y1="19.1998" x2="15.9334" y2="35.8194" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0094F0"/>
|
||||
<stop offset="0.243047" stop-color="#0078D4"/>
|
||||
<stop offset="0.584404" stop-color="#2052CB"/>
|
||||
<stop offset="0.830639" stop-color="#312A9A"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint1_radial_8666_6627" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(33.5988 29.5499) rotate(143.591) scale(29.8211 53.0152)">
|
||||
<stop stop-color="#3BD5FF"/>
|
||||
<stop offset="1" stop-color="#0078D4"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint2_radial_8666_6627" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(39.5988 27.5997) rotate(146.31) scale(25.96 43.796)">
|
||||
<stop stop-color="#DECBFF" stop-opacity="0.9"/>
|
||||
<stop offset="1" stop-color="#DECBFF" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint3_linear_8666_6627" x1="32.3989" y1="1.2" x2="9.66835" y2="25.9489" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0FAFFF"/>
|
||||
<stop offset="0.162714" stop-color="#0094F0"/>
|
||||
<stop offset="0.584404" stop-color="#2052CB"/>
|
||||
<stop offset="0.830639" stop-color="#312A9A"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="paint4_radial_8666_6627" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(23.6678 12.1488) rotate(137.53) scale(24.6606 38.4117)">
|
||||
<stop stop-color="#3BD5FF"/>
|
||||
<stop offset="1" stop-color="#0078D4"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint5_radial_8666_6627" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(27.5989 10.7998) rotate(149.036) scale(20.9914 33.6005)">
|
||||
<stop stop-color="#DECBFF" stop-opacity="0.9"/>
|
||||
<stop offset="1" stop-color="#D1D1FF" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<clipPath id="clip0_8666_6627">
|
||||
<path d="M0 12.8C0 8.31958 0 6.07937 0.871948 4.36808C1.63893 2.86278 2.86278 1.63893 4.36808 0.871948C6.07937 0 8.31958 0 12.8 0H35.2C39.6804 0 41.9206 0 43.6319 0.871948C45.1372 1.63893 46.3611 2.86278 47.1281 4.36808C48 6.07937 48 8.31958 48 12.8V35.2C48 39.6804 48 41.9206 47.1281 43.6319C46.3611 45.1372 45.1372 46.3611 43.6319 47.1281C41.9206 48 39.6804 48 35.2 48H12.8C8.31958 48 6.07937 48 4.36808 47.1281C2.86278 46.3611 1.63893 45.1372 0.871948 43.6319C0 41.9206 0 39.6804 0 35.2V12.8Z" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_8666_6627">
|
||||
<rect width="48" height="48" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Після Ширина: | Висота: | Розмір: 4.4 KiB |
Reference in new issue
Заблокувати користувача