1414 # Connect to Hive Metastore
1515 namespace = connect("hive2", {
1616 "uri": "thrift://localhost:9083",
17- "warehouse ": "/user/hive/warehouse",
17+ "root ": "/my/dir", # Or "s3://bucket/prefix"
1818 "ugi": "user:group1,group2" # Optional user/group info
1919 })
2020
4848
4949Configuration Properties:
5050 uri (str): Hive Metastore Thrift URI (e.g., "thrift://localhost:9083")
51- warehouse (str): Warehouse directory path (default: "/user/hive/warehouse" )
51+ root (str): Storage root location of the lakehouse on Hive catalog (default: current working directory )
5252 ugi (str): Optional User Group Information for authentication (format: "user:group1,group2")
53+ client.pool-size (int): Size of the HMS client connection pool (default: 3)
54+ storage.* (str): Additional storage configurations to access table
5355"""
5456from typing import Dict , List , Optional , Any
5557from urllib .parse import urlparse , unquote
120122
121123logger = logging .getLogger (__name__ )
122124
123- # Table properties used by Lance
124- LANCE_TABLE_TYPE = "lance.table_type"
125- LANCE_TABLE_FORMAT = "LANCE"
125+ # Table properties used by Lance (per hive.md specification)
126+ TABLE_TYPE_KEY = "table_type" # Case insensitive
127+ LANCE_TABLE_FORMAT = "lance" # Case insensitive
128+ MANAGED_BY_KEY = "managed_by" # Case insensitive, values: "storage" or "impl"
129+ VERSION_KEY = "version" # Numeric version number
126130EXTERNAL_TABLE = "EXTERNAL_TABLE"
127131
128132
@@ -181,8 +185,10 @@ def __init__(self, **properties):
181185
182186 Args:
183187 uri: The Hive Metastore URI (e.g., "thrift://localhost:9083")
188+ root: Storage root location of the lakehouse on Hive catalog (optional)
184189 ugi: User Group Information for authentication (optional, format: "user:group1,group2")
185- warehouse: The warehouse directory path (optional, defaults to Hive's warehouse)
190+ client.pool-size: Size of the HMS client connection pool (optional, default: 3)
191+ storage.*: Additional storage configurations to access table
186192 **properties: Additional configuration properties
187193 """
188194 if not HIVE_AVAILABLE :
@@ -193,7 +199,10 @@ def __init__(self, **properties):
193199
194200 self .uri = properties .get ("uri" , "thrift://localhost:9083" )
195201 self .ugi = properties .get ("ugi" )
196- self .warehouse = properties .get ("warehouse" , "/user/hive/warehouse" )
202+ self .root = properties .get ("root" , os .getcwd ())
203+ self .pool_size = int (properties .get ("client.pool-size" , "3" ))
204+ # Extract storage properties
205+ self .storage_properties = {k [8 :]: v for k , v in properties .items () if k .startswith ("storage." )}
197206
198207 # Create client
199208 self ._client = HiveMetastoreClient (self .uri , self .ugi )
@@ -207,23 +216,27 @@ def _normalize_identifier(self, identifier: List[str]) -> tuple:
207216 else :
208217 raise ValueError (f"Invalid identifier: { identifier } " )
209218
219+ def _is_root_namespace (self , identifier : Optional [List [str ]]) -> bool :
220+ """Check if the identifier refers to the root namespace."""
221+ return not identifier or len (identifier ) == 0
222+
210223 def _get_table_location (self , database : str , table : str ) -> str :
211224 """Get the location for a table."""
212- return os .path .join (self .warehouse , f"{ database } .db" , table )
225+ return os .path .join (self .root , f"{ database } .db" , table )
213226
214227 def list_namespaces (self , request : ListNamespacesRequest ) -> ListNamespacesResponse :
215228 """List all databases in the Hive Metastore."""
216229 try :
230+ # Only list namespaces if we're at the root level
231+ if not self ._is_root_namespace (request .id ):
232+ # Non-root namespaces don't have children in Hive2
233+ return ListNamespacesResponse (namespaces = [])
234+
217235 with self ._client as client :
218236 databases = client .get_all_databases ()
219- # Return just database names as strings
237+ # Return just database names as strings (excluding default)
220238 namespaces = [db for db in databases if db != "default" ]
221239
222- # Apply parent filter if specified
223- if request .id :
224- # In Hive, databases are flat, so parent filtering doesn't apply
225- namespaces = []
226-
227240 return ListNamespacesResponse (namespaces = namespaces )
228241 except Exception as e :
229242 logger .error (f"Failed to list namespaces: { e } " )
@@ -232,6 +245,16 @@ def list_namespaces(self, request: ListNamespacesRequest) -> ListNamespacesRespo
232245 def describe_namespace (self , request : DescribeNamespaceRequest ) -> DescribeNamespaceResponse :
233246 """Describe a database in the Hive Metastore."""
234247 try :
248+ # Handle root namespace
249+ if self ._is_root_namespace (request .id ):
250+ properties = {
251+ "location" : self .root ,
252+ "description" : "Root namespace (Hive Metastore)"
253+ }
254+ if self .ugi :
255+ properties ["ugi" ] = self .ugi
256+ return DescribeNamespaceResponse (properties = properties )
257+
235258 if len (request .id ) != 1 :
236259 raise ValueError (f"Invalid namespace identifier: { request .id } " )
237260
@@ -262,6 +285,10 @@ def describe_namespace(self, request: DescribeNamespaceRequest) -> DescribeNames
262285 def create_namespace (self , request : CreateNamespaceRequest ) -> CreateNamespaceResponse :
263286 """Create a new database in the Hive Metastore."""
264287 try :
288+ # Cannot create root namespace
289+ if self ._is_root_namespace (request .id ):
290+ raise ValueError ("Root namespace already exists" )
291+
265292 if len (request .id ) != 1 :
266293 raise ValueError (f"Invalid namespace identifier: { request .id } " )
267294
@@ -276,7 +303,7 @@ def create_namespace(self, request: CreateNamespaceRequest) -> CreateNamespaceRe
276303 database .ownerName = request .properties .get ("owner" , os .getenv ("USER" , "" ))
277304 database .locationUri = request .properties .get (
278305 "location" ,
279- os .path .join (self .warehouse , f"{ database_name } .db" )
306+ os .path .join (self .root , f"{ database_name } .db" )
280307 )
281308 database .parameters = {
282309 k : v for k , v in request .properties .items ()
@@ -296,6 +323,10 @@ def create_namespace(self, request: CreateNamespaceRequest) -> CreateNamespaceRe
296323 def drop_namespace (self , request : DropNamespaceRequest ) -> DropNamespaceResponse :
297324 """Drop a database from the Hive Metastore."""
298325 try :
326+ # Cannot drop root namespace
327+ if self ._is_root_namespace (request .id ):
328+ raise ValueError ("Cannot drop root namespace" )
329+
299330 if len (request .id ) != 1 :
300331 raise ValueError (f"Invalid namespace identifier: { request .id } " )
301332
@@ -321,6 +352,10 @@ def drop_namespace(self, request: DropNamespaceRequest) -> DropNamespaceResponse
321352 def namespace_exists (self , request : NamespaceExistsRequest ) -> None :
322353 """Check if a database exists in the Hive Metastore."""
323354 try :
355+ # Root namespace always exists
356+ if self ._is_root_namespace (request .id ):
357+ return
358+
324359 if len (request .id ) != 1 :
325360 raise ValueError (f"Invalid namespace identifier: { request .id } " )
326361
@@ -337,6 +372,10 @@ def namespace_exists(self, request: NamespaceExistsRequest) -> None:
337372 def list_tables (self , request : ListTablesRequest ) -> ListTablesResponse :
338373 """List tables in a database."""
339374 try :
375+ # Root namespace has no tables
376+ if self ._is_root_namespace (request .id ):
377+ return ListTablesResponse (tables = [])
378+
340379 if len (request .id ) != 1 :
341380 raise ValueError (f"Invalid namespace identifier: { request .id } " )
342381
@@ -350,11 +389,12 @@ def list_tables(self, request: ListTablesRequest) -> ListTablesResponse:
350389 for table_name in table_names :
351390 try :
352391 table = client .get_table (database_name , table_name )
353- # Check if it's a Lance table
354- if (table .parameters and
355- table .parameters .get (LANCE_TABLE_TYPE ) == LANCE_TABLE_FORMAT ):
356- # Return just table name, not full identifier
357- tables .append (table_name )
392+ # Check if it's a Lance table (case insensitive)
393+ if table .parameters :
394+ table_type = table .parameters .get (TABLE_TYPE_KEY , "" ).lower ()
395+ if table_type == LANCE_TABLE_FORMAT :
396+ # Return just table name, not full identifier
397+ tables .append (table_name )
358398 except Exception :
359399 # Skip tables we can't read
360400 continue
@@ -374,9 +414,11 @@ def describe_table(self, request: DescribeTableRequest) -> DescribeTableResponse
374414 with self ._client as client :
375415 table = client .get_table (database , table_name )
376416
377- # Check if it's a Lance table
378- if not (table .parameters and
379- table .parameters .get (LANCE_TABLE_TYPE ) == LANCE_TABLE_FORMAT ):
417+ # Check if it's a Lance table (case insensitive)
418+ if not table .parameters :
419+ raise ValueError (f"Table { request .id } is not a Lance table" )
420+ table_type = table .parameters .get (TABLE_TYPE_KEY , "" ).lower ()
421+ if table_type != LANCE_TABLE_FORMAT :
380422 raise ValueError (f"Table { request .id } is not a Lance table" )
381423
382424 # Get table location
@@ -458,14 +500,18 @@ def register_table(self, request: RegisterTableRequest) -> RegisterTableResponse
458500
459501 hive_table .sd = sd
460502
461- # Set table parameters
503+ # Set table parameters per hive.md specification
462504 hive_table .parameters = {
463- LANCE_TABLE_TYPE : LANCE_TABLE_FORMAT ,
464- "lance.version" : str (dataset .version ),
505+ TABLE_TYPE_KEY : LANCE_TABLE_FORMAT ,
506+ MANAGED_BY_KEY : request .properties .get (MANAGED_BY_KEY , "storage" ),
507+ VERSION_KEY : str (dataset .version ),
465508 "EXTERNAL" : "TRUE" ,
466509 }
467510 if request .properties :
468- hive_table .parameters .update (request .properties )
511+ # Add other properties but don't override the required ones
512+ for k , v in request .properties .items ():
513+ if k not in [TABLE_TYPE_KEY , MANAGED_BY_KEY , VERSION_KEY ]:
514+ hive_table .parameters [k ] = v
469515
470516 with self ._client as client :
471517 client .create_table (hive_table )
@@ -488,9 +534,11 @@ def table_exists(self, request: TableExistsRequest) -> None:
488534 with self ._client as client :
489535 table = client .get_table (database , table_name )
490536
491- # Check if it's a Lance table
492- if not (table .parameters and
493- table .parameters .get (LANCE_TABLE_TYPE ) == LANCE_TABLE_FORMAT ):
537+ # Check if it's a Lance table (case insensitive)
538+ if not table .parameters :
539+ raise ValueError (f"Table { request .id } is not a Lance table" )
540+ table_type = table .parameters .get (TABLE_TYPE_KEY , "" ).lower ()
541+ if table_type != LANCE_TABLE_FORMAT :
494542 raise ValueError (f"Table { request .id } is not a Lance table" )
495543 except Exception as e :
496544 if NoSuchObjectException and isinstance (e , NoSuchObjectException ):
@@ -507,8 +555,11 @@ def drop_table(self, request: DropTableRequest) -> DropTableResponse:
507555 # Get table to check if it's a Lance table
508556 table = client .get_table (database , table_name )
509557
510- if not (table .parameters and
511- table .parameters .get (LANCE_TABLE_TYPE ) == LANCE_TABLE_FORMAT ):
558+ # Check if it's a Lance table (case insensitive)
559+ if not table .parameters :
560+ raise ValueError (f"Table { request .id } is not a Lance table" )
561+ table_type = table .parameters .get (TABLE_TYPE_KEY , "" ).lower ()
562+ if table_type != LANCE_TABLE_FORMAT :
512563 raise ValueError (f"Table { request .id } is not a Lance table" )
513564
514565 # Drop the table (always delete data for Lance tables)
@@ -530,8 +581,11 @@ def deregister_table(self, request: DeregisterTableRequest) -> DeregisterTableRe
530581 # Get table to check if it's a Lance table
531582 table = client .get_table (database , table_name )
532583
533- if not (table .parameters and
534- table .parameters .get (LANCE_TABLE_TYPE ) == LANCE_TABLE_FORMAT ):
584+ # Check if it's a Lance table (case insensitive)
585+ if not table .parameters :
586+ raise ValueError (f"Table { request .id } is not a Lance table" )
587+ table_type = table .parameters .get (TABLE_TYPE_KEY , "" ).lower ()
588+ if table_type != LANCE_TABLE_FORMAT :
535589 raise ValueError (f"Table { request .id } is not a Lance table" )
536590
537591 location = table .sd .location if table .sd else None
0 commit comments