@@ -70,41 +70,113 @@ private[serverSidePlanning] trait ServerSidePlanningClientFactory {
7070}
7171
7272/**
73- * Registry for client factories. Can be configured for testing or to provide
74- * production implementations (e.g., IcebergRESTCatalogPlanningClientFactory) .
73+ * Registry for client factories. Automatically discovers and registers implementations
74+ * using Java's ServiceLoader mechanism at first access .
7575 *
76- * By default, no factory is registered. Production code should register an appropriate
77- * factory implementation before attempting to create clients.
76+ * When delta-iceberg JAR is on the classpath, IcebergRESTCatalogPlanningClientFactory
77+ * is automatically registered via META-INF/services discovery. Manual registration
78+ * using setFactory() is only needed for testing or to override the auto-discovered factory.
7879 */
7980private [serverSidePlanning] object ServerSidePlanningClientFactory {
8081 @ volatile private var registeredFactory : Option [ServerSidePlanningClientFactory ] = None
8182
83+ // ========== SERVICE LOADER AUTO-REGISTRATION ==========
84+ // This block runs ONCE when ServerSidePlanningClientFactory is first accessed.
85+ // It uses Java's ServiceLoader to discover and register factories declared in
86+ // META-INF/services/org.apache.spark.sql.delta.serverSidePlanning.ServerSidePlanningClientFactory
87+ {
88+ import java .util .ServiceLoader
89+ import scala .jdk .CollectionConverters ._
90+
91+ try {
92+ // Use ServiceLoader to discover all implementations on the classpath
93+ val loader = ServiceLoader .load(
94+ classOf [ServerSidePlanningClientFactory ],
95+ Thread .currentThread().getContextClassLoader)
96+
97+ // Convert Java Iterator to Scala and collect all factories
98+ val factories = loader.iterator().asScala.toList
99+
100+ if (factories.nonEmpty) {
101+ // Use the first discovered factory
102+ // In practice, there should only be one (IcebergRESTCatalogPlanningClientFactory)
103+ registeredFactory = Some (factories.head)
104+
105+ // Optional: Log successful registration for debugging
106+ // Uncomment for visibility during development
107+ // System.err.println(s"[Delta] Auto-registered ${factories.head.getClass.getName} " +
108+ // s"via ServiceLoader (found ${factories.size} implementation(s))")
109+
110+ // Optional: Warn if multiple implementations found
111+ if (factories.size > 1 ) {
112+ // scalastyle:off println
113+ System .err.println(
114+ s " [Delta] Warning: Multiple ServerSidePlanningClientFactory implementations found. " +
115+ s " Using ${factories.head.getClass.getName}. " +
116+ s " Others: ${factories.tail.map(_.getClass.getName).mkString(" , " )}" )
117+ // scalastyle:on println
118+ }
119+ } else {
120+ // No factories discovered - delta-iceberg not on classpath
121+ // This is fine, server-side planning just won't be available
122+ // System.err.println("[Delta] No ServerSidePlanningClientFactory found, FGAC disabled")
123+ }
124+
125+ } catch {
126+ case e : Exception =>
127+ // Unexpected error during service loading - log but don't fail
128+ // Delta should still work for non-FGAC tables
129+ // scalastyle:off println
130+ System .err.println(
131+ s " [Delta] Warning: Failed to auto-discover server-side planning factory: ${e.getMessage}" )
132+ // scalastyle:on println
133+ }
134+ }
135+ // ========== END SERVICE LOADER AUTO-REGISTRATION ==========
136+
82137 /**
83- * Set a factory for production use or testing.
138+ * Set a factory, overriding any auto-registered factory.
139+ * Primarily useful for testing or providing custom implementations.
84140 */
85141 private [serverSidePlanning] def setFactory (factory : ServerSidePlanningClientFactory ): Unit = {
86142 registeredFactory = Some (factory)
87143 }
88144
89145 /**
90- * Clear the registered factory.
146+ * Clear the registered factory. Primarily useful for testing to reset state between tests.
147+ * Note: This does NOT re-trigger ServiceLoader discovery. The factory must be manually
148+ * set again via setFactory() if needed.
91149 */
92150 private [serverSidePlanning] def clearFactory (): Unit = {
93151 registeredFactory = None
94152 }
95153
96154 /**
97155 * Get the currently registered factory.
98- * Throws IllegalStateException if no factory has been registered.
156+ * Throws IllegalStateException if no factory has been registered (either via ServiceLoader
157+ * auto-discovery or explicit setFactory() call).
99158 */
100159 def getFactory (): ServerSidePlanningClientFactory = {
101160 registeredFactory.getOrElse {
102161 throw new IllegalStateException (
103162 " No ServerSidePlanningClientFactory has been registered. " +
104- " Call ServerSidePlanningClientFactory.setFactory() to register an implementation." )
163+ " Ensure delta-iceberg JAR is on the classpath for auto-registration, " +
164+ " or call ServerSidePlanningClientFactory.setFactory() to register manually." )
105165 }
106166 }
107167
168+ /**
169+ * Check if a factory is currently registered (either via ServiceLoader or setFactory()).
170+ * Useful for testing or conditional logic.
171+ */
172+ def isFactoryRegistered (): Boolean = registeredFactory.isDefined
173+
174+ /**
175+ * Get information about the currently registered factory for debugging/logging.
176+ * Returns None if no factory is registered.
177+ */
178+ def getFactoryInfo (): Option [String ] = registeredFactory.map(_.getClass.getName)
179+
108180 /**
109181 * Convenience method to create a client from metadata using the registered factory.
110182 */
0 commit comments