Skip to content

Commit fbc11fc

Browse files
fangbofangbo
andauthored
feat: support ddl for branch operation (#576)
## Summary This PR adds DDL support for branch operations in Lance Spark. Previously, branch-related operations did not support DDL workflows properly. With this change, users can perform DDL on branches more consistently, improving branch management usability and aligning branch behavior with expected table operation semantics. ## Changes - Add DDL support for branch operations - Enable branch-related workflows to work with DDL statements - Improve consistency of branch management behavior in Lance Spark ## Motivation Branch operations are an important part of versioned data workflows. Supporting DDL on branches makes these workflows more complete and practical, especially for users who need to manage schema or table-related changes in isolated branches before merging. --------- Co-authored-by: fangbo <fangbo.0511@bytedance.com>
1 parent 7333940 commit fbc11fc

21 files changed

Lines changed: 1462 additions & 9 deletions

File tree

docs/src/operations/ddl/.pages

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,8 @@ nav:
1212
- drop-table.md
1313
- create-index.md
1414
- show-indexes.md
15+
- create-branch.md
16+
- drop-branch.md
17+
- show-branches.md
1518
- optimize.md
1619
- vacuum.md
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# CREATE BRANCH
2+
3+
Create a named branch for a Lance table.
4+
5+
!!! warning "Spark Extension Required"
6+
This feature requires the Lance Spark SQL extension to be enabled. See [Spark SQL Extensions](../../config.md#spark-sql-extensions) for configuration details.
7+
8+
## Overview
9+
10+
The `CREATE BRANCH` command creates a new branch that points to a specific table state. A branch can be created from:
11+
12+
- the latest version of `main`
13+
- a specific version on `main`
14+
- the head of another branch
15+
- a specific version on another branch
16+
- a tag
17+
18+
Creating a branch records a new reference in table metadata. It does not duplicate the table's data files.
19+
20+
## Syntax
21+
22+
=== "SQL"
23+
24+
```sql
25+
ALTER TABLE <table> CREATE BRANCH [IF NOT EXISTS] <branch_name>;
26+
27+
ALTER TABLE <table> CREATE BRANCH [IF NOT EXISTS] <branch_name>
28+
AS OF VERSION <version>;
29+
30+
ALTER TABLE <table> CREATE BRANCH [IF NOT EXISTS] <branch_name>
31+
AS OF BRANCH <source_branch>;
32+
33+
ALTER TABLE <table> CREATE BRANCH [IF NOT EXISTS] <branch_name>
34+
AS OF BRANCH <source_branch> VERSION <version>;
35+
36+
ALTER TABLE <table> CREATE BRANCH [IF NOT EXISTS] <branch_name>
37+
AS OF TAG <tag_name>;
38+
```
39+
40+
If the `AS OF` clause is omitted, the new branch is created from the latest version of `main`.
41+
42+
## Examples
43+
44+
### Create a branch from the latest `main`
45+
46+
=== "SQL"
47+
```sql
48+
ALTER TABLE lance.db.users CREATE BRANCH feature_x;
49+
```
50+
51+
### Create a branch from a specific `main` version
52+
53+
=== "SQL"
54+
```sql
55+
ALTER TABLE lance.db.users CREATE BRANCH IF NOT EXISTS snapshot_v5
56+
AS OF VERSION 5;
57+
```
58+
59+
### Create a branch from another branch head
60+
61+
=== "SQL"
62+
```sql
63+
ALTER TABLE lance.db.users CREATE BRANCH experiment_b
64+
AS OF BRANCH experiment_a;
65+
```
66+
67+
### Create a branch from a specific version on another branch
68+
69+
=== "SQL"
70+
```sql
71+
ALTER TABLE lance.db.users CREATE BRANCH experiment_b_v3
72+
AS OF BRANCH experiment_a VERSION 3;
73+
```
74+
75+
### Create a branch from a tag
76+
77+
=== "SQL"
78+
```sql
79+
ALTER TABLE lance.db.users CREATE BRANCH release_fix
80+
AS OF TAG release_candidate;
81+
```
82+
83+
## Output
84+
85+
The `CREATE BRANCH` command returns:
86+
87+
| Column | Type | Description |
88+
|--------|--------|----------------------------|
89+
| `name` | String | The name of the new branch |
90+
91+
## Notes and Limitations
92+
93+
- `CREATE BRANCH` is implemented as a Spark SQL extension command.
94+
- The referenced table must be a Lance table.
95+
- Creating a branch from a non-existent branch, tag, or version returns an error.
96+
- The branch name is returned even when the command result contains only a single row.
97+
98+
## See Also
99+
100+
- [SHOW BRANCHES](./show-branches.md)
101+
- [DROP BRANCH](./drop-branch.md)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# DROP BRANCH
2+
3+
Remove a branch from a Lance table.
4+
5+
!!! warning "Spark Extension Required"
6+
This feature requires the Lance Spark SQL extension to be enabled. See [Spark SQL Extensions](../../config.md#spark-sql-extensions) for configuration details.
7+
8+
## Overview
9+
10+
The `DROP BRANCH` command deletes a named branch reference from a Lance table. This removes the branch metadata entry, but does not rewrite data on `main` or on any other existing branch.
11+
12+
## Syntax
13+
14+
=== "SQL"
15+
```sql
16+
ALTER TABLE <table> DROP BRANCH [IF EXISTS] <branch_name>;
17+
```
18+
19+
## Example
20+
21+
=== "SQL"
22+
```sql
23+
ALTER TABLE lance.db.users DROP BRANCH IF EXISTS feature_x;
24+
```
25+
26+
## Output
27+
28+
The `DROP BRANCH` command returns:
29+
30+
| Column | Type | Description |
31+
|--------|--------|--------------------------------|
32+
| `name` | String | The name of the dropped branch |
33+
34+
## Notes and Limitations
35+
36+
- `DROP BRANCH` is implemented as a Spark SQL extension command.
37+
- The target table must be a Lance table.
38+
- `IF EXISTS` can be used when dropping a branch conditionally.
39+
- Use [SHOW BRANCHES](./show-branches.md) to inspect existing branches before deleting one.
40+
41+
## See Also
42+
43+
- [CREATE BRANCH](./create-branch.md)
44+
- [SHOW BRANCHES](./show-branches.md)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# SHOW BRANCHES
2+
3+
List branches defined on a Lance table.
4+
5+
!!! warning "Spark Extension Required"
6+
This feature requires the Lance Spark SQL extension to be enabled. See [Spark SQL Extensions](../../config.md#spark-sql-extensions) for configuration details.
7+
8+
## Overview
9+
10+
The `SHOW BRANCHES` command returns one row for each branch on a Lance table. It is useful for inspecting branch lineage, verifying branch creation, and understanding which version each branch points to.
11+
12+
## Syntax
13+
14+
=== "SQL"
15+
```sql
16+
SHOW BRANCHES FROM <table>;
17+
SHOW BRANCHES IN <table>;
18+
SHOW BRANCH FROM <table>;
19+
SHOW BRANCH IN <table>;
20+
```
21+
22+
Both plural and singular forms are accepted. Both `FROM` and `IN` are accepted.
23+
24+
## Examples
25+
26+
### List all branches on a table
27+
28+
=== "SQL"
29+
```sql
30+
SHOW BRANCHES FROM lance.db.users;
31+
```
32+
33+
### Use the singular alias
34+
35+
=== "SQL"
36+
```sql
37+
SHOW BRANCH IN lance.db.users;
38+
```
39+
40+
## Output
41+
42+
The `SHOW BRANCHES` command returns the following columns:
43+
44+
| Column | Type | Description |
45+
|--------|------|----------------------------------------------------------------------------------|
46+
| `name` | String | Branch name |
47+
| `parent_branch` | String | Source branch name if the branch was created from another branch; otherwise NULL |
48+
| `parent_version` | Long | Version used as the branch starting point |
49+
| `created_at` | Long | Branch creation timestamp as returned by Lance metadata |
50+
| `manifest_size` | Integer | Manifest size recorded for the branch |
51+
52+
## Notes
53+
54+
- `SHOW BRANCHES` is implemented as a Spark SQL extension command.
55+
- The target table must be a Lance table.
56+
- The result includes one row per branch currently registered in the table metadata.
57+
- Use [CREATE BRANCH](./create-branch.md) to add a branch and [DROP BRANCH](./drop-branch.md) to remove one.
58+
59+
## See Also
60+
61+
- [CREATE BRANCH](./create-branch.md)
62+
- [DROP BRANCH](./drop-branch.md)

lance-spark-3.4_2.12/src/main/scala/org/apache/spark/sql/catalyst/parser/extensions/LanceSqlExtensionsAstBuilder.scala

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
*/
1414
package org.apache.spark.sql.catalyst.parser.extensions
1515

16+
import org.antlr.v4.runtime.ParserRuleContext
1617
import org.apache.spark.sql.catalyst.analysis.{UnresolvedIdentifier, UnresolvedRelation}
17-
import org.apache.spark.sql.catalyst.parser.ParserInterface
18-
import org.apache.spark.sql.catalyst.plans.logical.{AddColumnsBackfill, AddIndex, LanceDropIndex, LanceNamedArgument, LogicalPlan, Optimize, SetUnenforcedPrimaryKey, ShowIndexes, UpdateColumnsBackfill, Vacuum}
18+
import org.apache.spark.sql.catalyst.parser.{ParseException, ParserInterface}
19+
import org.apache.spark.sql.catalyst.plans.logical.{AddColumnsBackfill, AddIndex, LanceCreateBranch, LanceDropBranch, LanceDropIndex, LanceNamedArgument, LanceShowBranches, LogicalPlan, Optimize, SetUnenforcedPrimaryKey, ShowIndexes, UpdateColumnsBackfill, Vacuum}
1920
import org.lance.spark.utils.{FieldPathUtils, ParserUtils}
2021

2122
import scala.collection.JavaConverters._
@@ -115,6 +116,71 @@ class LanceSqlExtensionsAstBuilder(delegate: ParserInterface)
115116
LanceDropIndex(table, indexName)
116117
}
117118

119+
override def visitCreateBranchRefMain(ctx: LanceSqlExtensionsParser.CreateBranchRefMainContext)
120+
: LanceCreateBranch = {
121+
val table = UnresolvedIdentifier(visitMultipartIdentifier(ctx.multipartIdentifier()))
122+
val branchName = cleanIdentifier(ctx.branchName.getText)
123+
val ifNotExists = ctx.EXISTS() != null
124+
if (ctx.refMainVersion == null)
125+
LanceCreateBranch(table, branchName, org.lance.Ref.ofMain(), ifNotExists)
126+
else LanceCreateBranch(
127+
table,
128+
branchName,
129+
org.lance.Ref.ofMain(_parseVersion(ctx, ctx.refMainVersion.getText)),
130+
ifNotExists)
131+
}
132+
133+
override def visitCreateBranchRefBranch(
134+
ctx: LanceSqlExtensionsParser.CreateBranchRefBranchContext): LanceCreateBranch = {
135+
val table = UnresolvedIdentifier(visitMultipartIdentifier(ctx.multipartIdentifier()))
136+
val branchName = cleanIdentifier(ctx.branchName.getText)
137+
val refBranchName = cleanIdentifier(ctx.refBranchName.getText)
138+
val ifNotExists = ctx.EXISTS() != null
139+
if (ctx.refBranchVersion == null)
140+
LanceCreateBranch(table, branchName, org.lance.Ref.ofBranch(refBranchName), ifNotExists)
141+
else LanceCreateBranch(
142+
table,
143+
branchName,
144+
org.lance.Ref.ofBranch(refBranchName, _parseVersion(ctx, ctx.refBranchVersion.getText)),
145+
ifNotExists)
146+
}
147+
148+
private def _parseVersion(ctx: ParserRuleContext, value: String): Long = {
149+
try {
150+
java.lang.Long.valueOf(value)
151+
} catch {
152+
case _: NumberFormatException =>
153+
throw new ParseException(
154+
errorClass = "INVALID_TYPED_LITERAL",
155+
messageParameters = Map(
156+
"valueType" -> "LONG",
157+
"value" -> value),
158+
ctx)
159+
}
160+
}
161+
162+
override def visitCreateBranchRefTag(ctx: LanceSqlExtensionsParser.CreateBranchRefTagContext)
163+
: LanceCreateBranch = {
164+
val table = UnresolvedIdentifier(visitMultipartIdentifier(ctx.multipartIdentifier()))
165+
val branchName = cleanIdentifier(ctx.branchName.getText)
166+
val refTagName = cleanIdentifier(ctx.refTagName.getText)
167+
val ifNotExists = ctx.EXISTS() != null
168+
LanceCreateBranch(table, branchName, org.lance.Ref.ofTag(refTagName), ifNotExists)
169+
}
170+
171+
override def visitDropBranch(ctx: LanceSqlExtensionsParser.DropBranchContext): LanceDropBranch = {
172+
val table = UnresolvedIdentifier(visitMultipartIdentifier(ctx.multipartIdentifier()))
173+
val branchName = cleanIdentifier(ctx.branchName.getText)
174+
val ifExists = ctx.EXISTS() != null
175+
LanceDropBranch(table, branchName, ifExists)
176+
}
177+
178+
override def visitShowBranches(ctx: LanceSqlExtensionsParser.ShowBranchesContext)
179+
: LanceShowBranches = {
180+
val table = UnresolvedIdentifier(visitMultipartIdentifier(ctx.multipartIdentifier()))
181+
LanceShowBranches(table)
182+
}
183+
118184
override def visitSetUnenforcedPrimaryKey(
119185
ctx: LanceSqlExtensionsParser.SetUnenforcedPrimaryKeyContext): SetUnenforcedPrimaryKey = {
120186
val table = UnresolvedIdentifier(visitMultipartIdentifier(ctx.multipartIdentifier()))
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package org.lance.spark.branch;
15+
16+
public class BranchDDLTest extends BaseBranchDDLTest {}

lance-spark-3.4_2.12/src/test/java/org/lance/spark/update/LanceSqlExtensionsAstBuilderTest.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,67 @@ public void testVacuumWithBacktickedTableName() {
189189
assertEquals(
190190
List.of("my-catalog", "my-table"), JavaConverters.seqAsJavaList(table.nameParts()));
191191
}
192+
193+
@Test
194+
public void testCreateBranchFromMain() {
195+
LanceSqlExtensionsParser parser =
196+
createParser(
197+
"ALTER TABLE `my-catalog`.`my-table` CREATE BRANCH IF NOT EXISTS `branch-a` AS OF VERSION 7");
198+
org.apache.spark.sql.catalyst.plans.logical.LanceCreateBranch plan =
199+
(org.apache.spark.sql.catalyst.plans.logical.LanceCreateBranch)
200+
astBuilder.visitSingleStatement(parser.singleStatement());
201+
202+
UnresolvedIdentifier table = (UnresolvedIdentifier) plan.table();
203+
assertEquals(
204+
List.of("my-catalog", "my-table"), JavaConverters.seqAsJavaList(table.nameParts()));
205+
assertEquals("branch-a", plan.branchName());
206+
assertEquals(true, plan.ifNotExists());
207+
assertTrue(plan.ref().getBranchName().isEmpty());
208+
assertEquals(7, plan.ref().getVersionNumber().get());
209+
}
210+
211+
@Test
212+
public void testCreateBranchFromBranch() {
213+
LanceSqlExtensionsParser parser =
214+
createParser(
215+
"ALTER TABLE `my-catalog`.`my-table` CREATE BRANCH IF NOT EXISTS `branch-a` AS OF BRANCH `source-branch` VERSION 7");
216+
org.apache.spark.sql.catalyst.plans.logical.LanceCreateBranch plan =
217+
(org.apache.spark.sql.catalyst.plans.logical.LanceCreateBranch)
218+
astBuilder.visitSingleStatement(parser.singleStatement());
219+
220+
UnresolvedIdentifier table = (UnresolvedIdentifier) plan.table();
221+
assertEquals(
222+
List.of("my-catalog", "my-table"), JavaConverters.seqAsJavaList(table.nameParts()));
223+
assertEquals("branch-a", plan.branchName());
224+
assertEquals(true, plan.ifNotExists());
225+
assertEquals("source-branch", plan.ref().getBranchName().get());
226+
assertEquals(7, plan.ref().getVersionNumber().get());
227+
}
228+
229+
@Test
230+
public void testDropBranch() {
231+
LanceSqlExtensionsParser parser =
232+
createParser("ALTER TABLE `my-catalog`.`my-table` DROP BRANCH IF EXISTS `branch-a`");
233+
org.apache.spark.sql.catalyst.plans.logical.LanceDropBranch plan =
234+
(org.apache.spark.sql.catalyst.plans.logical.LanceDropBranch)
235+
astBuilder.visitSingleStatement(parser.singleStatement());
236+
237+
UnresolvedIdentifier table = (UnresolvedIdentifier) plan.table();
238+
assertEquals(
239+
List.of("my-catalog", "my-table"), JavaConverters.seqAsJavaList(table.nameParts()));
240+
assertEquals("branch-a", plan.branchName());
241+
assertEquals(true, plan.ifExists());
242+
}
243+
244+
@Test
245+
public void testShowBranch() {
246+
LanceSqlExtensionsParser parser = createParser("SHOW BRANCH IN `my-catalog`.`my-table`");
247+
org.apache.spark.sql.catalyst.plans.logical.LanceShowBranches plan =
248+
(org.apache.spark.sql.catalyst.plans.logical.LanceShowBranches)
249+
astBuilder.visitSingleStatement(parser.singleStatement());
250+
251+
UnresolvedIdentifier table = (UnresolvedIdentifier) plan.table();
252+
assertEquals(
253+
List.of("my-catalog", "my-table"), JavaConverters.seqAsJavaList(table.nameParts()));
254+
}
192255
}

0 commit comments

Comments
 (0)