@@ -1737,18 +1737,14 @@ public static String createIndexIfNotExists(Connection conn, String schemaName,
17371737 // create index
17381738 // Construct JSON path expression
17391739 // data->'address'->'city'->>'street'
1740-
1741- var jsonPathExpression = PGKeyUtil .getFormattedKey (fieldName );
1742-
1743- if (SUPPORTED_INDEX_FIELD_TYPE .contains (indexOption .fieldType )){
1744- jsonPathExpression = "(%s)::%s" .formatted (jsonPathExpression , indexOption .fieldType );
1745- }
1740+ var jsonPathExpression = buildIndexExpression (fieldName , indexOption );
1741+ var indexMethod = getIndexMethod (indexOption );
17461742
17471743 var createIndexSQL = """
17481744 CREATE %s INDEX IF NOT EXISTS %s
1749- ON %s.%s ((%s));
1745+ ON %s.%s USING %s ((%s));
17501746 """
1751- .formatted (indexOption .unique ? "UNIQUE" : "" , indexName , schemaName , tableName , jsonPathExpression );
1747+ .formatted (indexOption .unique ? "UNIQUE" : "" , indexName , schemaName , tableName , indexMethod , jsonPathExpression );
17521748
17531749 stmt .execute (createIndexSQL );
17541750 if (log .isInfoEnabled ()) {
@@ -1997,24 +1993,14 @@ public static String createIndexIfNotExist4MultiFields(Connection conn, String s
19971993 return "" ;
19981994 }
19991995
2000- // Construct composite JSON path expression e.g. ( ((data->>'lastName')), (((data->>'age')::integer)) )
2001- var jsonPathExpression = fields .stream ()
2002- .map (field -> {
2003- var path = PGKeyUtil .getFormattedKey (field .fieldName );
2004- if (SUPPORTED_INDEX_FIELD_TYPE .contains (field .fieldType .toString ())) {
2005- return "((%s)::%s)" .formatted (path , field .fieldType );
2006- } else {
2007- // default to text if not a supported castable type
2008- return "(%s)" .formatted (path );
2009- }
2010- })
2011- .collect (Collectors .joining (", " ));
1996+ var indexMethod = getIndexMethod (indexOption );
1997+ var jsonPathExpression = buildIndexExpression (fields , indexOption );
20121998
20131999 var createIndexSQL = """
20142000 CREATE %s INDEX IF NOT EXISTS %s
2015- ON %s.%s (%s);
2001+ ON %s.%s USING %s (%s);
20162002 """
2017- .formatted (indexOption .unique ? "UNIQUE" : "" , indexName , schemaName , tableName , jsonPathExpression );
2003+ .formatted (indexOption .unique ? "UNIQUE" : "" , indexName , schemaName , tableName , indexMethod , jsonPathExpression );
20182004
20192005 stmt .execute (createIndexSQL );
20202006 if (log .isInfoEnabled ()) {
@@ -2041,6 +2027,95 @@ public static String createIndexIfNotExist4MultiFields(Connection conn, String s
20412027 return "%s.%s" .formatted (schemaName , indexName );
20422028 }
20432029
2030+ /**
2031+ * Resolves the PostgreSQL index method keyword from {@link IndexOption}.
2032+ *
2033+ * <p>
2034+ * The returned value is used directly in {@code CREATE INDEX ... USING <method>}.
2035+ * </p>
2036+ *
2037+ * @param indexOption the index option containing the desired method
2038+ * @return the PostgreSQL index method keyword, e.g. {@code BTREE} or {@code GIN}
2039+ */
2040+ static String getIndexMethod (IndexOption indexOption ) {
2041+ Checker .checkNotNull (indexOption , "indexOption" );
2042+ Checker .checkNotNull (indexOption .indexMethod , "indexMethod" );
2043+ return indexOption .indexMethod .name ();
2044+ }
2045+
2046+ /**
2047+ * Builds the PostgreSQL index expression for a single field.
2048+ *
2049+ * <p>
2050+ * For BTREE indexes this returns the existing scalar expression, optionally with a cast
2051+ * such as {@code ((data->>'age')::numeric)}.
2052+ * For GIN indexes this returns a JSONB expression such as {@code data->'targetIdList'} so
2053+ * PostgreSQL can create an expression GIN index on the JSON sub-document.
2054+ * </p>
2055+ *
2056+ * @param fieldName the field name or JSON path under the {@code data} column
2057+ * @param indexOption the index option that controls the method and expression shape
2058+ * @return the SQL expression used inside the index definition
2059+ */
2060+ static String buildIndexExpression (String fieldName , IndexOption indexOption ) {
2061+ Checker .checkNotBlank (fieldName , "fieldName" );
2062+ Checker .checkNotNull (indexOption , "indexOption" );
2063+
2064+ if (IndexOption .IndexMethod .GIN .equals (indexOption .indexMethod )) {
2065+ Checker .check (!indexOption .unique , "GIN index does not support unique=true" );
2066+ Checker .check ("text" .equalsIgnoreCase (indexOption .fieldType ) || "jsonb" .equalsIgnoreCase (indexOption .fieldType ),
2067+ "GIN index does not support fieldType cast. use default fieldType/text for jsonb expression indexes" );
2068+ return PGKeyUtil .getFormattedKey4JsonWithAlias (fieldName , DATA );
2069+ }
2070+
2071+ var jsonPathExpression = PGKeyUtil .getFormattedKey (fieldName );
2072+ if (SUPPORTED_INDEX_FIELD_TYPE .contains (indexOption .fieldType )) {
2073+ return "(%s)::%s" .formatted (jsonPathExpression , indexOption .fieldType );
2074+ }
2075+ return jsonPathExpression ;
2076+ }
2077+
2078+ /**
2079+ * Builds the PostgreSQL index expression list for typed index creation.
2080+ *
2081+ * <p>
2082+ * BTREE supports the existing multi-field scalar expressions.
2083+ * GIN is intentionally limited to a single JSONB field so callers can create expression
2084+ * indexes such as {@code USING GIN ((data->'targetIdList'))} for array/object queries.
2085+ * </p>
2086+ *
2087+ * @param fields the fields to be indexed
2088+ * @param indexOption the index option that controls the method and validation rules
2089+ * @return the SQL expression list used inside the index definition
2090+ */
2091+ static String buildIndexExpression (List <PGIndexField > fields , IndexOption indexOption ) {
2092+ Checker .check (CollectionUtils .isNotEmpty (fields ), "fields cannot be empty" );
2093+ Checker .checkNotNull (indexOption , "indexOption" );
2094+
2095+ if (IndexOption .IndexMethod .GIN .equals (indexOption .indexMethod )) {
2096+ Checker .check (!indexOption .unique , "GIN index does not support unique=true" );
2097+ Checker .check (fields .size () == 1 , "GIN index currently only supports a single field" );
2098+
2099+ var field = fields .get (0 );
2100+ Checker .check (field .fieldType == PGFieldType .TEXT || field .fieldType == PGFieldType .JSONB ,
2101+ "GIN index does not support fieldType cast. use PGFieldType.TEXT/JSONB or omit fieldType" );
2102+ return "(%s)" .formatted (PGKeyUtil .getFormattedKey4JsonWithAlias (field .fieldName , DATA ));
2103+ }
2104+
2105+ // Construct composite JSON path expression e.g. ( ((data->>'lastName')), (((data->>'age')::integer)) )
2106+ return fields .stream ()
2107+ .map (field -> {
2108+ var path = PGKeyUtil .getFormattedKey (field .fieldName );
2109+ if (SUPPORTED_INDEX_FIELD_TYPE .contains (field .fieldType .toString ())) {
2110+ return "((%s)::%s)" .formatted (path , field .fieldType );
2111+ } else {
2112+ // default to text if not a supported castable type
2113+ return "(%s)" .formatted (path );
2114+ }
2115+ })
2116+ .collect (Collectors .joining (", " ));
2117+ }
2118+
20442119 /**
20452120 * Sets the parameters for the given {@link PreparedStatement} with the given
20462121 * parameters. The parameters are expected to be in the same order as the
0 commit comments