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+ // 'metadata' table fields
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 = "creation_time" ;
62+ private static final String USER_FIELD = "user" ;
63+ private static final String SYSTEM_FIELD = "system" ;
64+
65+ // 'metadata_props' table fields
66+ private static final String NESTED_NAME_FIELD = "props_name" ;
67+ private static final String NESTED_SCOPE_FIELD = "props_scope" ;
68+ private static final String NESTED_VALUE_FIELD = "props_value" ;
69+
70+ private String instanceId = "instance" ;
71+ private String projectId = "project" ;
72+ private String databaseId = "database" ;
73+
74+ private Spanner spanner ;
75+ private DatabaseAdminClient adminClient ;
76+
3677 @ Override
3778 public void initialize (MetadataStorageContext context ) throws Exception {
38- throw new IOException ("NOT IMPLEMENTED" );
79+ Map <String , String > cConf = context .getProperties ();
80+
81+ projectId = Objects .requireNonNull (cConf .get (projectId ));
82+ instanceId = Objects .requireNonNull (cConf .get (projectId ));
83+ databaseId = Objects .requireNonNull (cConf .get (projectId ));
84+
85+ SpannerOptions .Builder builder = SpannerOptions .newBuilder ().setProjectId (projectId );
86+ SpannerOptions options = builder .build ();
87+
88+ this .spanner = options .getService ();
89+ this .adminClient = spanner .getDatabaseAdminClient ();
90+
91+ LOG .info ("SpannerMetadataStorage initialized." );
3992 }
4093
4194 @ Override
42- public void close () {
95+ public String getName () {
96+ return "gcp-spanner" ;
4397 }
4498
4599 @ Override
46100 public void createIndex () throws IOException {
47- throw new IOException ("NOT IMPLEMENTED" );
101+ getCreateTableDDLStatement ();
102+ LOG .info ("Index creation completed." );
48103 }
49104
50- @ Override
51- public String getName () {
52- return "gcp-spanner" ;
105+ private void getCreateTableDDLStatement () throws IOException {
106+ List <String > DDLStatements = new ArrayList <>();
107+ DDLStatements .add (getCreateMetadataTableDDLStatement ());
108+ DDLStatements .add (getCreateMetadataPropsTableDDLStatement ());
109+ DDLStatements .addAll (getAllSearchIndexDDLStatements ());
110+ executeDDLStatements (DDLStatements );
111+ }
112+
113+ /**
114+ * <p>Key features of the schema:
115+ * <ul>
116+ * <li>**`metadata_id`:** An Unique which is used to identify the various
117+ * components of the pipeline metadata.
118+ * </li>
119+ * <li>**`namespace`:** It contains the namespace associated with a given entity.
120+ * </li>
121+ * <li>**`entity_type`:** It contains the type of entity.
122+ * </li>
123+ * <li>**`name`:** Name of the entity.
124+ * </li>
125+ * <li>**`created`:** creation time of the entity.</li>
126+ * <li>**`user`:** user scope related properties and tags</li>
127+ * <li>**`system`:**system scope related properties and tags </li>
128+ * <li>**`metadata_column`:** contains all the metadata related </li>
129+ * <li>user_tokens, system_tokens and text_tokens are the Tokenlists of User,
130+ * System on which we create search indexes and use them for user, system
131+ * and null scope searches respectively.</li>
132+ * </ul>
133+ * </p>
134+ * TODO(CDAP-21174): Add the TTl policies for cleanups.
135+ */
136+ private String getCreateMetadataTableDDLStatement () {
137+ return String .format (
138+ "CREATE TABLE IF NOT EXISTS %s (" + // metadata
139+ "%s STRING(MAX) NOT NULL," + // metadata_id
140+ "%s STRING(MAX) NOT NULL," + // namespace
141+ "%s STRING(MAX) NOT NULL," + // entity_type
142+ "%s STRING(MAX) NOT NULL," + // name
143+ "%s INT64," + // creation-time
144+ "%s STRING(MAX)," + // user
145+ "%s STRING(MAX)," + // system
146+ "%s JSON," + // metadata_column
147+ "%s INT64 NOT NULL," + // version
148+ "user_tokens TOKENLIST AS " + // user-tokens list
149+ "(TOKENIZE_SUBSTRING(%s, support_relative_search=>TRUE)) HIDDEN," +
150+ "system_tokens TOKENLIST AS " + // system-tokens list
151+ "(TOKENIZE_SUBSTRING(%s, support_relative_search=>TRUE)) HIDDEN," +
152+ "text_tokens TOKENLIST AS " + // text-tokens list
153+ "(TOKENLIST_CONCAT([User_Tokens, System_Tokens])) HIDDEN," +
154+ ") PRIMARY KEY (%s) " , // metadata_id
155+ METADATA_TABLE ,
156+ METADATA_ID_FIELD ,
157+ NAMESPACE_FIELD ,
158+ TYPE_FIELD ,
159+ NAME_FIELD ,
160+ CREATED_FIELD ,
161+ USER_FIELD ,
162+ SYSTEM_FIELD ,
163+ METADATA_COLUMN_FIELD ,
164+ VERSION ,
165+ USER_FIELD ,
166+ SYSTEM_FIELD ,
167+ METADATA_ID_FIELD
168+ );
169+ }
170+
171+ /**
172+ * <p>Key features of the schema:
173+ * <ul>
174+ * <li>**`metadata_id`:**An Unique which is used to identify the various
175+ * components of the pipeline metadata.
176+ * </li>
177+ * <li>**`namespace`:** It contains the namespace associated with a given entity.
178+ * </li>
179+ * <li>**`entity_type`:** It contains the type of entity.
180+ * </li>
181+ * <li>**`props_name`:** Contains the name of the properties/tags associated to an
182+ * entries in metadata table.
183+ * </li>
184+ * <li>**`props_scope`:** Contains the scope of the properties/tags associated to an
185+ * entries in metadata table.
186+ * </li>
187+ * <li>**`props_value`:** Contains the value of the properties/tags associated to an
188+ * entries in metadata table.
189+ * <li>value_tokens is the Tokenlist of Props_Value Column on which we create search
190+ * indexes and use of key:value type searches.
191+ * respectively.</li>
192+ * </ul>
193+ * </p>
194+ */
195+ private String getCreateMetadataPropsTableDDLStatement () {
196+ return String .format (
197+ "CREATE TABLE IF NOT EXISTS %s (" +
198+ "%s STRING(MAX) NOT NULL," + // metadata_id
199+ "%s STRING(MAX) NOT NULL," + // namespace
200+ "%s STRING(MAX) NOT NULL," + // entity_type
201+ "%s STRING(MAX) NOT NULL," + // name
202+ "%s STRING(MAX)," + // scope
203+ "%s STRING(MAX)," + // value
204+ "value_tokens TOKENLIST AS " + // value-tokens list
205+ "(TOKENIZE_SUBSTRING(%s, support_relative_search=>TRUE)) HIDDEN," +
206+ ") PRIMARY KEY (%s, %s, %s) ," + // metadata_id, name, scope
207+ "INTERLEAVE IN PARENT %s ON DELETE CASCADE" ,
208+ METADATA_PROPS_TABLE ,
209+ METADATA_ID_FIELD ,
210+ NAMESPACE_FIELD ,
211+ TYPE_FIELD ,
212+ NESTED_NAME_FIELD ,
213+ NESTED_SCOPE_FIELD ,
214+ NESTED_VALUE_FIELD ,
215+ NESTED_VALUE_FIELD ,
216+ METADATA_ID_FIELD ,
217+ NESTED_NAME_FIELD ,
218+ NESTED_SCOPE_FIELD ,
219+ METADATA_TABLE
220+ );
221+ }
222+
223+ private List <String > getAllSearchIndexDDLStatements () {
224+ List <String > DDLStatements = new ArrayList <>();
225+
226+ // Create SEARCH INDEX on the Tokenized column(user_tokens) of user column.
227+ DDLStatements .add (String .format ("CREATE SEARCH INDEX UserNgramIndex ON %s(user_tokens)" , METADATA_TABLE ));
228+
229+ // Creates SEARCH INDEX on the Tokenized column(system_tokens) of system column.
230+ DDLStatements .add (String .format ("CREATE SEARCH INDEX SystemNgramIndex ON %s(system_tokens)" , METADATA_TABLE ));
231+
232+ // Creates SEARCH INDEX on the Tokenized column(text_tokens) of user and system column.
233+ DDLStatements .add (String .format ("CREATE SEARCH INDEX TextNgramIndex ON %s(text_tokens)" , METADATA_TABLE ));
234+
235+ // Creates SEARCH INDEX on the Tokenized column(value_tokens) of props_value column.
236+ DDLStatements .add (String .format ("CREATE SEARCH INDEX ValueNgramIndex ON %s(value_tokens)" , METADATA_PROPS_TABLE ));
237+
238+ return DDLStatements ;
239+ }
240+
241+ /**
242+ * Creates the necessary metadata tables and associated search indexes in the database.
243+ * This method orchestrates the creation of the core metadata table, metadata_props table
244+ * and search indexes to facilitate efficient querying of user,system, text, and value-based
245+ * data within the metadata.
246+ * TODO (CDAP-21176): Checking additional Schema updates for schema backward compatibility.
247+ */
248+ private void executeDDLStatements (List <String > DDLStatements ) throws IOException {
249+ if (DDLStatements .isEmpty ()) {
250+ LOG .debug ("No DDL statements to execute" );
251+ return ;
252+ }
253+ try {
254+ Uninterruptibles .getUninterruptibly (
255+ adminClient .updateDatabaseDdl (instanceId ,
256+ databaseId , DDLStatements , null ));
257+ } catch (ExecutionException e ) {
258+ Throwable cause = e .getCause ();
259+ if (cause instanceof SpannerException
260+ && ((SpannerException ) cause ).getErrorCode () == ErrorCode .FAILED_PRECONDITION ) {
261+ LOG .debug ("Concurrent statement execution error: " , e );
262+ } else {
263+ throw new IOException (e );
264+ }
265+ }
53266 }
54267
55268 @ Override
56269 public void dropIndex () throws IOException {
57- throw new IOException ("NOT IMPLEMENTED" );
270+ List <String > statements = new ArrayList <>();
271+ statements .add (String .format ("DROP TABLE IF EXISTS %s" , METADATA_TABLE ));
272+ statements .add (String .format ("DROP TABLE IF EXISTS %s" , METADATA_PROPS_TABLE ));
273+ executeDDLStatements (statements );
274+ LOG .info ("Metadata Tables dropped successfully." );
58275 }
59276
60277 @ Override
@@ -77,5 +294,14 @@ public Metadata read(Read read) throws IOException {
77294 public SearchResponse search (SearchRequest request ) throws IOException {
78295 throw new IOException ("NOT IMPLEMENTED" );
79296 }
297+
298+ @ Override
299+ public synchronized void close () {
300+ if (spanner == null ) {
301+ return ;
302+ }
303+ spanner .close ();
304+ spanner = null ;
305+ }
80306}
81307
0 commit comments