1616
1717package io .cdap .cdap .metadata .spanner ;
1818
19+ import com .google .cloud .spanner .DatabaseAdminClient ;
20+ import com .google .cloud .spanner .ErrorCode ;
21+ import com .google .cloud .spanner .Spanner ;
22+ import com .google .cloud .spanner .SpannerException ;
23+ import com .google .cloud .spanner .SpannerOptions ;
24+ import com .google .common .util .concurrent .Uninterruptibles ;
1925import io .cdap .cdap .spi .metadata .Metadata ;
2026import io .cdap .cdap .spi .metadata .MetadataChange ;
2127import io .cdap .cdap .spi .metadata .MetadataMutation ;
2531import io .cdap .cdap .spi .metadata .Read ;
2632import io .cdap .cdap .spi .metadata .SearchRequest ;
2733import io .cdap .cdap .spi .metadata .SearchResponse ;
34+ import org .slf4j .Logger ;
35+ import org .slf4j .LoggerFactory ;
2836import java .io .IOException ;
37+ import java .util .ArrayList ;
2938import java .util .List ;
39+ import java .util .Map ;
40+ import java .util .Objects ;
41+ import java .util .concurrent .ExecutionException ;
3042
3143/**
3244 * A metadata storage provider that delegates to Spanner.
3345 */
3446public class SpannerMetadataStorage implements MetadataStorage {
3547
48+ private static final Logger LOG = LoggerFactory .getLogger (SpannerMetadataStorage .class );
49+
50+ // Metadata table names
51+ private static final String METADATA_TABLE = "metadata" ;
52+ private static final String METADATA_PROPS_TABLE = "metadata_props" ;
53+
54+ public static final class MetadataTable {
55+ private static final String METADATA_ID_FIELD = "metadata_id" ;
56+ private static final String METADATA_COLUMN_FIELD = "metadata_column" ;
57+ private static final String NAMESPACE_FIELD = "namespace" ;
58+ private static final String TYPE_FIELD = "entity_type" ;
59+ private static final String NAME_FIELD = "name" ;
60+ private static final String VERSION = "version" ;
61+ private static final String CREATED_FIELD = "create_time" ;
62+ private static final String USER_FIELD = "user" ;
63+ private static final String SYSTEM_FIELD = "system" ;
64+ }
65+
66+ public static final class MetadataPropsTable {
67+ private static final String METADATA_ID_FIELD = "metadata_id" ;
68+ private static final String NAMESPACE_FIELD = "namespace" ;
69+ private static final String TYPE_FIELD = "entity_type" ;
70+ private static final String NESTED_NAME_FIELD = "name" ;
71+ private static final String NESTED_SCOPE_FIELD = "scope" ;
72+ private static final String NESTED_VALUE_FIELD = "value" ;
73+ }
74+
75+ private String instanceId ;
76+ private String projectId ;
77+ private String databaseId ;
78+
79+ private Spanner spanner ;
80+ private DatabaseAdminClient adminClient ;
81+
3682 @ Override
3783 public void initialize (MetadataStorageContext context ) throws Exception {
38- throw new IOException ("NOT IMPLEMENTED" );
84+ Map <String , String > properties = context .getProperties ();
85+
86+ this .projectId = Objects .requireNonNull (properties .get ("project" ));
87+ this .instanceId = Objects .requireNonNull (properties .get ("instance" ));
88+ this .databaseId = Objects .requireNonNull (properties .get ("database" ));
89+
90+ this .spanner = SpannerOptions .newBuilder ().setProjectId (projectId ).build ().getService ();
91+ this .adminClient = spanner .getDatabaseAdminClient ();
92+
93+ LOG .info ("SpannerMetadataStorage initialized." );
3994 }
4095
4196 @ Override
42- public void close () {
97+ public String getName () {
98+ return "gcp-spanner" ;
4399 }
44100
45101 @ Override
46102 public void createIndex () throws IOException {
47- throw new IOException ("NOT IMPLEMENTED" );
103+ getCreateTableDDLStatement ();
104+ LOG .info ("Index creation completed." );
48105 }
49106
50- @ Override
51- public String getName () {
52- return "gcp-spanner" ;
107+ private void getCreateTableDDLStatement () throws IOException {
108+ List <String > ddlStatements = new ArrayList <>();
109+ ddlStatements .add (getCreateMetadataTableDDLStatement ());
110+ ddlStatements .add (getCreateMetadataPropsTableDDLStatement ());
111+ ddlStatements .addAll (getAllSearchIndexDDLStatements ());
112+ executeDDLStatements (ddlStatements );
113+ }
114+
115+ /**
116+ * <p>Key features of the schema:
117+ * <ul>
118+ * <li>**`metadata_id`:** An Unique which is used to identify the various
119+ * components of the pipeline metadata.
120+ * </li>
121+ * <li>**`namespace`:** It contains the namespace associated with a given entity.
122+ * </li>
123+ * <li>**`entity_type`:** It contains the type of entity.
124+ * </li>
125+ * <li>**`name`:** Name of the entity.
126+ * </li>
127+ * <li>**`created`:** creation time of the entity.</li>
128+ * <li>**`user`:** user scope related properties and tags</li>
129+ * <li>**`system`:**system scope related properties and tags </li>
130+ * <li>**`metadata_column`:** contains all the metadata related </li>
131+ * <li>user_tokens, system_tokens and text_tokens are the Tokenlists of User,
132+ * System on which we create search indexes and use them for user, system
133+ * and null scope searches respectively.</li>
134+ * </ul>
135+ * </p>
136+ * TODO(CDAP-21174): Add the TTl policies for cleanups.
137+ */
138+ private String getCreateMetadataTableDDLStatement () {
139+ return String .format (
140+ "CREATE TABLE IF NOT EXISTS %s (" + // metadata
141+ "%s STRING(MAX) NOT NULL," + // metadata_id
142+ "%s STRING(MAX) NOT NULL," + // namespace
143+ "%s STRING(MAX) NOT NULL," + // entity_type
144+ "%s STRING(MAX) NOT NULL," + // name
145+ "%s INT64," + // create_time
146+ "%s STRING(MAX)," + // user
147+ "%s STRING(MAX)," + // system
148+ "%s JSON," + // metadata_column
149+ "%s INT64 NOT NULL," + // version
150+ "user_tokens TOKENLIST AS " + // user_tokens list
151+ "(TOKENIZE_SUBSTRING(%s, support_relative_search=>TRUE)) HIDDEN," +
152+ "system_tokens TOKENLIST AS " + // system_tokens list
153+ "(TOKENIZE_SUBSTRING(%s, support_relative_search=>TRUE)) HIDDEN," +
154+ "text_tokens TOKENLIST AS " + // text_tokens list
155+ "(TOKENLIST_CONCAT([User_Tokens, System_Tokens])) HIDDEN," +
156+ ") PRIMARY KEY (%s) " , // metadata_id
157+ METADATA_TABLE ,
158+ MetadataTable .METADATA_ID_FIELD ,
159+ MetadataTable .NAMESPACE_FIELD ,
160+ MetadataTable .TYPE_FIELD ,
161+ MetadataTable .NAME_FIELD ,
162+ MetadataTable .CREATED_FIELD ,
163+ MetadataTable .USER_FIELD ,
164+ MetadataTable .SYSTEM_FIELD ,
165+ MetadataTable .METADATA_COLUMN_FIELD ,
166+ MetadataTable .VERSION ,
167+ MetadataTable .USER_FIELD ,
168+ MetadataTable .SYSTEM_FIELD ,
169+ MetadataTable .METADATA_ID_FIELD
170+ );
171+ }
172+
173+ /**
174+ * <p>Key features of the schema:
175+ * <ul>
176+ * <li>**`metadata_id`:**An Unique which is used to identify the various
177+ * components of the pipeline metadata.
178+ * </li>
179+ * <li>**`namespace`:** It contains the namespace associated with a given entity.
180+ * </li>
181+ * <li>**`entity_type`:** It contains the type of entity.
182+ * </li>
183+ * <li>**`props_name`:** Contains the name of the properties/tags associated to an
184+ * entries in metadata table.
185+ * </li>
186+ * <li>**`props_scope`:** Contains the scope of the properties/tags associated to an
187+ * entries in metadata table.
188+ * </li>
189+ * <li>**`props_value`:** Contains the value of the properties/tags associated to an
190+ * entries in metadata table.
191+ * <li>value_tokens is the Tokenlist of Props_Value Column on which we create search
192+ * indexes and use of key:value type searches.
193+ * respectively.</li>
194+ * </ul>
195+ * </p>
196+ */
197+ private String getCreateMetadataPropsTableDDLStatement () {
198+ return String .format (
199+ "CREATE TABLE IF NOT EXISTS %s (" +
200+ "%s STRING(MAX) NOT NULL," + // metadata_id
201+ "%s STRING(MAX) NOT NULL," + // namespace
202+ "%s STRING(MAX) NOT NULL," + // entity_type
203+ "%s STRING(MAX) NOT NULL," + // name
204+ "%s STRING(MAX)," + // scope
205+ "%s STRING(MAX)," + // value
206+ "value_tokens TOKENLIST AS " + // value_tokens list
207+ "(TOKENIZE_SUBSTRING(%s, support_relative_search=>TRUE)) HIDDEN," +
208+ ") PRIMARY KEY (%s, %s, %s) ," + // metadata_id, name, scope
209+ "INTERLEAVE IN PARENT %s ON DELETE CASCADE" ,
210+ METADATA_PROPS_TABLE ,
211+ MetadataPropsTable .METADATA_ID_FIELD ,
212+ MetadataPropsTable .NAMESPACE_FIELD ,
213+ MetadataPropsTable .TYPE_FIELD ,
214+ MetadataPropsTable .NESTED_NAME_FIELD ,
215+ MetadataPropsTable .NESTED_SCOPE_FIELD ,
216+ MetadataPropsTable .NESTED_VALUE_FIELD ,
217+ MetadataPropsTable .NESTED_VALUE_FIELD ,
218+ MetadataPropsTable .METADATA_ID_FIELD ,
219+ MetadataPropsTable .NESTED_NAME_FIELD ,
220+ MetadataPropsTable .NESTED_SCOPE_FIELD ,
221+ METADATA_TABLE
222+ );
223+ }
224+
225+ private List <String > getAllSearchIndexDDLStatements () {
226+ List <String > ddlStatements = new ArrayList <>();
227+
228+ // Create SEARCH INDEX on the Tokenized column(user_tokens) of user column.
229+ ddlStatements .add (String .format ("CREATE SEARCH INDEX UserNgramIndex ON %s(user_tokens)" , METADATA_TABLE ));
230+
231+ // Creates SEARCH INDEX on the Tokenized column(system_tokens) of system column.
232+ ddlStatements .add (String .format ("CREATE SEARCH INDEX SystemNgramIndex ON %s(system_tokens)" , METADATA_TABLE ));
233+
234+ // Creates SEARCH INDEX on the Tokenized column(text_tokens) of user and system column.
235+ ddlStatements .add (String .format ("CREATE SEARCH INDEX TextNgramIndex ON %s(text_tokens)" , METADATA_TABLE ));
236+
237+ // Creates SEARCH INDEX on the Tokenized column(value_tokens) of props_value column.
238+ ddlStatements .add (String .format ("CREATE SEARCH INDEX ValueNgramIndex ON %s(value_tokens)" , METADATA_PROPS_TABLE ));
239+
240+ return ddlStatements ;
241+ }
242+
243+ /**
244+ * Creates the necessary metadata tables and associated search indexes in the database.
245+ * This method orchestrates the creation of the core metadata table, metadata_props table
246+ * and search indexes to facilitate efficient querying of user,system, text, and value-based
247+ * data within the metadata.
248+ * TODO (CDAP-21176): Checking additional Schema updates for schema backward compatibility.
249+ */
250+ private void executeDDLStatements (List <String > ddlStatements ) throws IOException {
251+ if (ddlStatements .isEmpty ()) {
252+ LOG .debug ("No ddl statements to execute" );
253+ return ;
254+ }
255+ try {
256+ Uninterruptibles .getUninterruptibly (
257+ adminClient .updateDatabaseDdl (instanceId ,
258+ databaseId , ddlStatements , null ));
259+ } catch (ExecutionException e ) {
260+ Throwable cause = e .getCause ();
261+ if (cause instanceof SpannerException
262+ && ((SpannerException ) cause ).getErrorCode () == ErrorCode .FAILED_PRECONDITION ) {
263+ LOG .debug ("Concurrent statement execution error: " , e );
264+ } else {
265+ throw new IOException (e );
266+ }
267+ }
53268 }
54269
55270 @ Override
56271 public void dropIndex () throws IOException {
57- throw new IOException ("NOT IMPLEMENTED" );
272+ List <String > statements = new ArrayList <>();
273+ statements .add (String .format ("DROP TABLE IF EXISTS %s" , METADATA_TABLE ));
274+ statements .add (String .format ("DROP TABLE IF EXISTS %s" , METADATA_PROPS_TABLE ));
275+ executeDDLStatements (statements );
276+ LOG .info ("Metadata Tables dropped successfully." );
58277 }
59278
60279 @ Override
@@ -77,5 +296,13 @@ public Metadata read(Read read) throws IOException {
77296 public SearchResponse search (SearchRequest request ) throws IOException {
78297 throw new IOException ("NOT IMPLEMENTED" );
79298 }
299+
300+ @ Override
301+ public synchronized void close () {
302+ if (spanner != null ) {
303+ spanner .close ();
304+ spanner = null ;
305+ }
306+ }
80307}
81308
0 commit comments