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)),
     })

Reply via email to