This is an automated email from the ASF dual-hosted git repository.
lidavidm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-adbc.git
The following commit(s) were added to refs/heads/main by this push:
new 3e7f03586 feat(javascript): make driver optional when uri or profile
is provided (#4537)
3e7f03586 is described below
commit 3e7f03586244c5109a4169768a9f846def4fa3fd
Author: Kent Wu <[email protected]>
AuthorDate: Sun Jul 19 19:16:05 2026 -0400
feat(javascript): make driver optional when uri or profile is provided
(#4537)
### Summary
Allows creating `AdbcDatabase` without an explicit `driver` option when
`databaseOptions.uri` contains a URI that identifies the driver, or when
`databaseOptions.profile` specifies a connection profile
Closes #4533
---
javascript/__test__/profile.spec.ts | 184 ++++++++++++++++++++++++++++++++++--
javascript/binding.d.ts | 20 ++--
javascript/lib/index.ts | 4 +-
javascript/lib/types.ts | 51 ++++++----
javascript/src/client.rs | 114 ++++++++++++++++++----
javascript/src/lib.rs | 12 +--
6 files changed, 317 insertions(+), 68 deletions(-)
diff --git a/javascript/__test__/profile.spec.ts
b/javascript/__test__/profile.spec.ts
index 3fcab571e..0315dfcd9 100644
--- a/javascript/__test__/profile.spec.ts
+++ b/javascript/__test__/profile.spec.ts
@@ -20,10 +20,24 @@ import assert from 'node:assert/strict'
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
import { join, isAbsolute } from 'node:path'
import { tmpdir } from 'node:os'
-import { AdbcDatabase } from '../lib/index.js'
+import { AdbcDatabase, AdbcError } from '../lib/index.js'
const testLib = process.env.ADBC_DRIVER_MANAGER_TEST_LIB
+function tomlPath(p: string): string {
+ return p.replaceAll('\\', '/')
+}
+
+function writeProfileToml(dir: string, name: string, driver: string, options:
Record<string, string> = {}): void {
+ const optLines = Object.entries(options)
+ .map(([k, v]) => `${k} = "${tomlPath(v)}"`)
+ .join('\n')
+ writeFileSync(
+ join(dir, `${name}.toml`),
+ `profile_version = 1\ndriver =
"${tomlPath(driver)}"\n\n[Options]\n${optLines}\n`,
+ )
+}
+
test('profile: load database from profile:// URI', async () => {
const driver = testLib ?? 'sqlite'
const tmpDir = mkdtempSync(join(tmpdir(), 'adbc-profile-test-'))
@@ -38,12 +52,7 @@ test('profile: load database from profile:// URI', async ()
=> {
await setupConn.close()
await setupDb.close()
- // TOML requires forward slashes — backslashes are escape sequences
- const toml = (p: string) => p.replaceAll('\\', '/')
- writeFileSync(
- join(tmpDir, 'test_sqlite.toml'),
- `profile_version = 1\ndriver = "${toml(driver)}"\n\n[Options]\nuri =
"${toml(dbPath)}"\n`,
- )
+ writeProfileToml(tmpDir, 'test_sqlite', driver, { uri: dbPath })
const db = new AdbcDatabase({
driver: 'profile://test_sqlite',
@@ -67,9 +76,7 @@ test('profile: load database from profile:// URI', async ()
=> {
// test env var is set to an absolute path (e.g. on Windows CI).
test('profile: load database from sqlite: URI', { skip: testLib !== undefined
&& isAbsolute(testLib) }, async () => {
const driver = testLib ?? 'sqlite'
- const db = new AdbcDatabase({
- driver: `${driver}::memory:`,
- })
+ const db = new AdbcDatabase({ driver: `${driver}::memory:` })
const conn = await db.connect()
await conn.query('SELECT 1 AS n')
@@ -77,3 +84,160 @@ test('profile: load database from sqlite: URI', { skip:
testLib !== undefined &&
await conn.close()
await db.close()
})
+
+test(
+ 'profile: omit driver, load from URI in databaseOptions.uri',
+ { skip: testLib !== undefined && isAbsolute(testLib) },
+ async () => {
+ const driver = testLib ?? 'sqlite'
+ const db = new AdbcDatabase({
+ databaseOptions: { uri: `${driver}::memory:` },
+ })
+ const conn = await db.connect()
+
+ await conn.query('SELECT 1 AS n')
+
+ await conn.close()
+ await db.close()
+ },
+)
+
+test('profile: omit driver, load from profile:// URI in databaseOptions.uri',
async () => {
+ const driver = testLib ?? 'sqlite'
+ const tmpDir = mkdtempSync(join(tmpdir(), 'adbc-profile-test-'))
+ try {
+ writeProfileToml(tmpDir, 'my_profile', driver)
+
+ const db = new AdbcDatabase({
+ databaseOptions: { uri: 'profile://my_profile' },
+ profileSearchPaths: [tmpDir],
+ })
+ const conn = await db.connect()
+
+ await conn.query('SELECT 1 AS n')
+
+ await conn.close()
+ await db.close()
+ } finally {
+ rmSync(tmpDir, { recursive: true })
+ }
+})
+
+test('profile: omit driver, load from bare profile name in
databaseOptions.profile', async () => {
+ const driver = testLib ?? 'sqlite'
+ const tmpDir = mkdtempSync(join(tmpdir(), 'adbc-profile-test-'))
+ try {
+ writeProfileToml(tmpDir, 'my_profile', driver)
+
+ const db = new AdbcDatabase({
+ databaseOptions: { profile: 'my_profile' },
+ profileSearchPaths: [tmpDir],
+ })
+ const conn = await db.connect()
+
+ await conn.query('SELECT 1 AS n')
+
+ await conn.close()
+ await db.close()
+ } finally {
+ rmSync(tmpDir, { recursive: true })
+ }
+})
+
+test('profile: driver + databaseOptions.profile loads from profile', async ()
=> {
+ const driver = tomlPath(testLib ?? 'sqlite')
+ const tmpDir = mkdtempSync(join(tmpdir(), 'adbc-profile-test-'))
+ try {
+ writeProfileToml(tmpDir, 'my_profile', driver)
+
+ const db = new AdbcDatabase({
+ driver,
+ databaseOptions: { profile: 'my_profile' },
+ profileSearchPaths: [tmpDir],
+ })
+ const conn = await db.connect()
+
+ await conn.query('SELECT 1 AS n')
+
+ await conn.close()
+ await db.close()
+ } finally {
+ rmSync(tmpDir, { recursive: true })
+ }
+})
+
+test('profile: driver + databaseOptions.uri profile:// loads from profile',
async () => {
+ const driver = tomlPath(testLib ?? 'sqlite')
+ const tmpDir = mkdtempSync(join(tmpdir(), 'adbc-profile-test-'))
+ try {
+ writeProfileToml(tmpDir, 'my_profile', driver)
+
+ const db = new AdbcDatabase({
+ driver,
+ databaseOptions: { uri: 'profile://my_profile' },
+ profileSearchPaths: [tmpDir],
+ })
+ const conn = await db.connect()
+
+ await conn.query('SELECT 1 AS n')
+
+ await conn.close()
+ await db.close()
+ } finally {
+ rmSync(tmpDir, { recursive: true })
+ }
+})
+
+test('profile: error when profile key and profile:// uri are both provided',
async () => {
+ assert.throws(
+ () =>
+ new AdbcDatabase({
+ databaseOptions: { profile: 'p1', uri: 'profile://p2' },
+ }),
+ (err: unknown) => {
+ assert.ok(err instanceof Error)
+ assert.ok(err.message.includes('mutually exclusive'), `unexpected
message: ${err.message}`)
+ return true
+ },
+ )
+})
+
+test('profile: driver disagreeing with profile errors', async () => {
+ const driver = tomlPath(testLib ?? 'sqlite')
+ const tmpDir = mkdtempSync(join(tmpdir(), 'adbc-profile-test-'))
+ try {
+ writeProfileToml(tmpDir, 'my_profile', driver)
+
+ assert.throws(
+ () =>
+ new AdbcDatabase({
+ driver: 'some_other_driver',
+ databaseOptions: { profile: 'my_profile' },
+ profileSearchPaths: [tmpDir],
+ }),
+ (err: unknown) => {
+ assert.ok(err instanceof AdbcError)
+ assert.strictEqual(err.code, 'InvalidArguments')
+ assert.strictEqual(
+ err.message,
+ `profile specifies driver \`${tomlPath(driver)}\` which does not
match requested driver \`some_other_driver\``,
+ )
+ return true
+ },
+ )
+ } finally {
+ rmSync(tmpDir, { recursive: true })
+ }
+})
+
+test('profile: error when driver is omitted and neither uri nor profile is
present', async () => {
+ assert.throws(
+ // @ts-expect-error — intentionally invalid: neither uri nor profile
present
+ () => new AdbcDatabase({ databaseOptions: { username: 'foo' } }),
+ (err: unknown) => {
+ assert.ok(err instanceof Error)
+ assert.ok(err.message.includes('driver is required'), `unexpected
message: ${err.message}`)
+ return true
+ },
+ )
+})
diff --git a/javascript/binding.d.ts b/javascript/binding.d.ts
index 2740d1655..ddbc8b345 100644
--- a/javascript/binding.d.ts
+++ b/javascript/binding.d.ts
@@ -14,7 +14,7 @@ export declare class NativeAdbcConnection {
export type _NativeAdbcConnection = NativeAdbcConnection
export declare class NativeAdbcDatabase {
- constructor(opts: ConnectOptions)
+ constructor(opts: NativeConnectOptions)
connect(options?: Record<string, string> | undefined | null):
Promise<unknown>
close(): void
}
@@ -40,15 +40,6 @@ export declare class NativeAdbcStatement {
}
export type _NativeAdbcStatement = NativeAdbcStatement
-export interface ConnectOptions {
- driver: string
- entrypoint?: string
- manifestSearchPaths?: Array<string>
- profileSearchPaths?: Array<string>
- loadFlags?: number
- databaseOptions?: Record<string, string>
-}
-
export declare function crateVersion(): string
export declare function defaultAdbcVersion(): string
@@ -69,3 +60,12 @@ export interface GetTableSchemaOptions {
dbSchema?: string
tableName: string
}
+
+export interface NativeConnectOptions {
+ driver?: string
+ entrypoint?: string
+ manifestSearchPaths?: Array<string>
+ profileSearchPaths?: Array<string>
+ loadFlags?: number
+ databaseOptions?: Record<string, string>
+}
diff --git a/javascript/lib/index.ts b/javascript/lib/index.ts
index 72fec32d1..4a8437b8f 100644
--- a/javascript/lib/index.ts
+++ b/javascript/lib/index.ts
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-import { NativeAdbcDatabase, NativeAdbcConnection, NativeAdbcStatement } from
'../binding.js'
+import { NativeAdbcDatabase, NativeAdbcConnection, NativeAdbcStatement, type
NativeConnectOptions } from '../binding.js'
import type {
AdbcDatabase as AdbcDatabaseInterface,
@@ -87,7 +87,7 @@ export class AdbcDatabase implements AdbcDatabaseInterface {
constructor(options: ConnectOptions) {
try {
- this._inner = new NativeAdbcDatabase(options)
+ this._inner = new NativeAdbcDatabase(options as unknown as
NativeConnectOptions)
} catch (e) {
throw AdbcError.fromError(e)
}
diff --git a/javascript/lib/types.ts b/javascript/lib/types.ts
index 6a6b3ba12..effc9cd8f 100644
--- a/javascript/lib/types.ts
+++ b/javascript/lib/types.ts
@@ -106,21 +106,7 @@ export type InfoCode = (typeof InfoCode)[keyof typeof
InfoCode]
*
* These options configure how the ADBC driver is loaded and how the initial
connection is established.
*/
-export interface ConnectOptions {
- /**
- * Driver to load. Accepts any of the following forms:
- * - Short name: `"sqlite"`, `"postgresql"` — the driver manager searches
for a matching
- * manifest file (e.g. `sqlite.toml`) in the configured directories, then
falls back to
- * `LD_LIBRARY_PATH` / `PATH`.
- * - Absolute path to a shared library: `"/usr/lib/libadbc_driver_sqlite.so"`
- * - Absolute path to a driver manifest `.toml` file (with or without the
`.toml` extension).
- * - Relative path (only valid when {@link LoadFlags.AllowRelativePaths} is
set).
- * - URI-style string: `"sqlite:file::memory:"`,
`"postgresql://user:pass@host/db"` — the
- * driver name is the URI scheme and the remainder is passed as the
connection URI.
- * - Connection profile URI: `"profile://my_profile"` — loads a named
profile from a
- * `.toml` file found in {@link profileSearchPaths} or the default search
directories.
- */
- driver: string
+interface ConnectOptionsBase {
/**
* Name of the entrypoint function (optional).
* If not provided, ADBC will attempt to guess the entrypoint symbol name
based on the driver name.
@@ -142,13 +128,38 @@ export interface ConnectOptions {
* Defaults to {@link LoadFlags.Default} (all search locations enabled) when
omitted.
*/
loadFlags?: number
- /**
- * Database-specific options.
- * Key-value pairs passed to the driver during database initialization
(e.g., "uri", "username").
- */
- databaseOptions?: Record<string, string>
}
+export type ConnectOptions =
+ | (ConnectOptionsBase & {
+ /**
+ * Driver to load. Accepts any of the following forms:
+ * - Short name: `"sqlite"`, `"postgresql"` — the driver manager
searches for a matching
+ * manifest file (e.g. `sqlite.toml`) in the configured directories,
then falls back to
+ * `LD_LIBRARY_PATH` / `PATH`.
+ * - Absolute path to a shared library:
`"/usr/lib/libadbc_driver_sqlite.so"`
+ * - Absolute path to a driver manifest `.toml` file (with or without
the `.toml` extension).
+ * - Relative path (only valid when {@link LoadFlags.AllowRelativePaths}
is set).
+ * - URI-style string: `"sqlite:file::memory:"`,
`"postgresql://user:pass@host/db"` — the
+ * driver name is the URI scheme and the remainder is passed as the
connection URI.
+ * - Connection profile URI: `"profile://my_profile"` — loads a named
profile from a
+ * `.toml` file found in {@link profileSearchPaths} or the default
search directories.
+ */
+ driver: string
+ /** Database-specific options passed to the driver during
initialization. */
+ databaseOptions?: Record<string, string>
+ })
+ | (ConnectOptionsBase & {
+ /**
+ * Database-specific options passed to the driver during initialization.
+ *
+ * When `driver` is omitted, either `uri` or `profile` is required:
+ * - `uri` — passed directly to the driver manager (e.g.
`"sqlite::memory:"`, `"profile://name"`)
+ * - `profile` — bare connection profile name; the profile file
specifies the driver
+ */
+ databaseOptions: ({ uri: string } | { profile: string }) &
Record<string, string>
+ })
+
/**
* Ingestion modes for the `ingest` convenience method.
*
diff --git a/javascript/src/client.rs b/javascript/src/client.rs
index 632413168..5bb4b5b0a 100644
--- a/javascript/src/client.rs
+++ b/javascript/src/client.rs
@@ -22,11 +22,15 @@ use std::sync::mpsc;
use adbc_core::{
Connection, Database, Driver, LOAD_FLAG_DEFAULT, Optionable, Statement,
+ error::{Error as AdbcError, Status},
options::{
AdbcVersion, InfoCode, ObjectDepth, OptionConnection, OptionDatabase,
OptionStatement,
OptionValue,
},
};
+use adbc_driver_manager::profile::{
+ ConnectionProfile, ConnectionProfileProvider, FilesystemProfileProvider,
+};
use adbc_driver_manager::{ManagedConnection, ManagedDatabase, ManagedDriver,
ManagedStatement};
use arrow_array::RecordBatchReader;
use arrow_ipc::reader::StreamReader;
@@ -46,7 +50,7 @@ pub enum ClientError {
pub type Result<T> = std::result::Result<T, ClientError>;
pub struct ConnectOptions {
- pub driver: String,
+ pub driver: Option<String>,
pub entrypoint: Option<String>,
pub manifest_search_paths: Option<Vec<String>>,
pub profile_search_paths: Option<Vec<String>>,
@@ -87,34 +91,104 @@ impl AdbcDatabaseCore {
.profile_search_paths
.map(|paths| paths.into_iter().map(PathBuf::from).collect());
- let database_opts = opts.database_options.map(map_database_options);
+ let mut raw_opts = opts.database_options.unwrap_or_default();
+ let profile_uri: Option<String> = if let Some(profile) =
raw_opts.remove("profile") {
+ if raw_opts
+ .get("uri")
+ .is_some_and(|u| u.starts_with("profile://"))
+ {
+ return Err(ClientError::Other(
+ "multiple profile sources: databaseOptions.profile and a profile://
URI in databaseOptions.uri are mutually exclusive"
+ .to_string(),
+ ));
+ }
+ Some(format!("profile://{profile}"))
+ } else if raw_opts
+ .get("uri")
+ .is_some_and(|u| u.starts_with("profile://"))
+ {
+ Some(raw_opts.remove("uri").unwrap())
+ } else {
+ None
+ };
+
+ let database = if let Some(ref uri) = profile_uri {
+ let provider =
FilesystemProfileProvider::new_with_search_paths(profile_search_paths);
+
+ // If driver is also specified, validate it agrees with the profile's
driver.
+ // The C driver manager errors on disagreement; we replicate that here
until
+ // the Rust driver manager gains native support for this validation.
+ if let Some(ref driver) = opts.driver {
+ let profile_name = uri.trim_start_matches("profile://");
+ let profile = provider.clone().get_profile(profile_name)?;
+ let (profile_driver, _) = profile.get_driver_name()?;
+ if !driver.is_empty() && driver != profile_driver {
+ return Err(ClientError::Adbc(AdbcError::with_message_and_status(
+ format!(
+ "profile specifies driver `{profile_driver}` which does not
match requested driver `{driver}`"
+ ),
+ Status::InvalidArguments,
+ )));
+ }
+ }
- let database = if opts.driver.contains(':') {
- let provider =
adbc_driver_manager::profile::FilesystemProfileProvider::new_with_search_paths(
- profile_search_paths,
- );
- // URI-style ("sqlite:file::memory:") or profile URI
("profile://my_profile")
ManagedDatabase::from_uri_with_profile_provider(
- &opts.driver,
+ uri,
entrypoint.as_deref(),
version,
load_flags,
manifest_search_paths,
provider,
- database_opts.into_iter().flatten(),
+ map_database_options(raw_opts),
)?
} else {
- // Short name ("sqlite") or path ("/usr/lib/libadbc_driver_sqlite.so")
- let mut driver = ManagedDriver::load_from_name(
- &opts.driver,
- entrypoint.as_deref(),
- version,
- load_flags,
- manifest_search_paths,
- )?;
- match database_opts {
- Some(db_opts) => driver.new_database_with_opts(db_opts)?,
- None => driver.new_database()?,
+ match opts.driver {
+ Some(ref driver) if driver.contains(':') => {
+ let provider =
+
adbc_driver_manager::profile::FilesystemProfileProvider::new_with_search_paths(
+ profile_search_paths,
+ );
+ ManagedDatabase::from_uri_with_profile_provider(
+ driver,
+ entrypoint.as_deref(),
+ version,
+ load_flags,
+ manifest_search_paths,
+ provider,
+ map_database_options(raw_opts),
+ )?
+ }
+ Some(ref driver) => {
+ let mut drv = ManagedDriver::load_from_name(
+ driver,
+ entrypoint.as_deref(),
+ version,
+ load_flags,
+ manifest_search_paths,
+ )?;
+ drv.new_database_with_opts(map_database_options(raw_opts))?
+ }
+ None => {
+ let Some(uri) = raw_opts.remove("uri") else {
+ return Err(ClientError::Other(
+ "driver is required unless databaseOptions.uri or
databaseOptions.profile is provided"
+ .to_string(),
+ ));
+ };
+ let provider =
+
adbc_driver_manager::profile::FilesystemProfileProvider::new_with_search_paths(
+ profile_search_paths,
+ );
+ ManagedDatabase::from_uri_with_profile_provider(
+ &uri,
+ entrypoint.as_deref(),
+ version,
+ load_flags,
+ manifest_search_paths,
+ provider,
+ map_database_options(raw_opts),
+ )?
+ }
}
};
diff --git a/javascript/src/lib.rs b/javascript/src/lib.rs
index 92bee8663..38b35d26b 100644
--- a/javascript/src/lib.rs
+++ b/javascript/src/lib.rs
@@ -141,8 +141,8 @@ pub fn default_load_flags() -> u32 {
// Options
#[napi(object)]
-pub struct ConnectOptions {
- pub driver: String,
+pub struct _NativeConnectOptions {
+ pub driver: Option<String>,
pub entrypoint: Option<String>,
pub manifest_search_paths: Option<Vec<String>>,
pub profile_search_paths: Option<Vec<String>>,
@@ -150,8 +150,8 @@ pub struct ConnectOptions {
pub database_options: Option<HashMap<String, String>>,
}
-impl From<ConnectOptions> for CoreConnectOptions {
- fn from(opts: ConnectOptions) -> Self {
+impl From<_NativeConnectOptions> for CoreConnectOptions {
+ fn from(opts: _NativeConnectOptions) -> Self {
Self {
driver: opts.driver,
entrypoint: opts.entrypoint,
@@ -213,8 +213,8 @@ pub struct _NativeAdbcDatabase {
#[napi]
impl _NativeAdbcDatabase {
#[napi(constructor)]
- pub fn new(opts: ConnectOptions) -> Result<Self> {
- let db = CoreDatabase::new(opts.into()).map_err(to_napi_err)?;
+ pub fn new(env: Env, opts: _NativeConnectOptions) -> Result<Self> {
+ let db = CoreDatabase::new(opts.into()).map_err(|e| sync_adbc_err(e,
env))?;
Ok(Self {
inner: Some(Arc::new(db)),
})