Skip to content

Commit 960a032

Browse files
delete-returning (#557)
Summary: - Support for `DELETE RETURNING` - Synchronous response publication **only**. - Added robot test `Delete Returning Simple Projection`. - Added robot test `Delete Returning Star`. - Added robot test `Delete Await Returning Generates Error`. - Added robot test `Delete Await Not Returning Functions as Expected`. - Added operation type variation to `/*+ AWAIT */` robot tests.
1 parent abc27ff commit 960a032

File tree

7 files changed

+200
-17
lines changed

7 files changed

+200
-17
lines changed

internal/stackql/parserutil/parser_util.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,18 @@ func extractInsertReturningColumnNames(
137137
return colNames, err
138138
}
139139

140+
func ExtractDeleteReturningColumnNames(
141+
deleteStmt *sqlparser.Delete,
142+
starColumns []string,
143+
formatter sqlparser.NodeFormatter,
144+
) ([]ColumnHandle, error) {
145+
return extractInsertReturningColumnNames(
146+
deleteStmt.SelectExprs,
147+
starColumns,
148+
formatter,
149+
)
150+
}
151+
140152
func ExtractUpdateReturningColumnNames(
141153
updateStmt *sqlparser.Update,
142154
starColumns []string,

internal/stackql/planbuilder/plan_builder.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/stackql/any-sdk/pkg/streaming"
1414
"github.com/stackql/stackql/internal/stackql/acid/txn_context"
1515
"github.com/stackql/stackql/internal/stackql/astanalysis/routeanalysis"
16+
"github.com/stackql/stackql/internal/stackql/drm"
1617
"github.com/stackql/stackql/internal/stackql/handler"
1718
"github.com/stackql/stackql/internal/stackql/internal_data_transfer/builder_input"
1819
"github.com/stackql/stackql/internal/stackql/internal_data_transfer/internaldto"
@@ -608,13 +609,18 @@ func (pgb *standardPlanGraphBuilder) handleDelete(pbi planbuilderinput.PlanBuild
608609
return err
609610
}
610611
insertCtx := primitiveGenerator.GetPrimitiveComposer().GetInsertPreparedStatementCtx()
612+
var selectCtx drm.PreparedStatementCtx
613+
if len(node.SelectExprs) > 0 {
614+
selectCtx = primitiveGenerator.GetPrimitiveComposer().GetSelectPreparedStatementCtx()
615+
}
611616
isPhysicalTable := tbl.IsPhysicalTable()
612617
var bldr primitivebuilder.Builder
613618
if !isPhysicalTable {
614619
bldr = primitivebuilder.NewDelete(
615620
pgb.planGraphHolder,
616621
handlerCtx,
617622
insertCtx,
623+
selectCtx,
618624
node,
619625
tbl,
620626
nil,

internal/stackql/primitivebuilder/delete.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,20 @@ type Delete struct {
1919
handlerCtx handler.HandlerContext
2020
drmCfg drm.Config
2121
root primitivegraph.PrimitiveNode
22+
tail primitivegraph.PrimitiveNode
2223
tbl tablemetadata.ExtendedTableMetadata
2324
node sqlparser.SQLNode
2425
commentDirectives sqlparser.CommentDirectives
2526
isAwait bool
2627
insertCtx drm.PreparedStatementCtx
28+
selectCtx drm.PreparedStatementCtx
2729
}
2830

2931
func NewDelete(
3032
graph primitivegraph.PrimitiveGraphHolder,
3133
handlerCtx handler.HandlerContext,
3234
insertCtx drm.PreparedStatementCtx,
35+
selectCtx drm.PreparedStatementCtx,
3336
node sqlparser.SQLNode,
3437
tbl tablemetadata.ExtendedTableMetadata,
3538
commentDirectives sqlparser.CommentDirectives,
@@ -44,6 +47,7 @@ func NewDelete(
4447
commentDirectives: commentDirectives,
4548
isAwait: isAwait,
4649
insertCtx: insertCtx,
50+
selectCtx: selectCtx,
4751
}
4852
}
4953

@@ -52,7 +56,7 @@ func (ss *Delete) GetRoot() primitivegraph.PrimitiveNode {
5256
}
5357

5458
func (ss *Delete) GetTail() primitivegraph.PrimitiveNode {
55-
return ss.root
59+
return ss.tail
5660
}
5761

5862
func (ss *Delete) Build() error {
@@ -104,6 +108,22 @@ func (ss *Delete) Build() error {
104108
graph := ss.graph
105109
insertNode := graph.CreatePrimitiveNode(deletePrimitive)
106110
ss.root = insertNode
107-
111+
ss.tail = insertNode
112+
if ss.selectCtx != nil {
113+
selectionBldr := NewSingleSelect(
114+
ss.graph,
115+
handlerCtx,
116+
ss.selectCtx,
117+
[]tableinsertioncontainer.TableInsertionContainer{insertContainer},
118+
nil,
119+
streaming.NewNopMapStream(),
120+
)
121+
err = selectionBldr.Build()
122+
if err != nil {
123+
return err
124+
}
125+
ss.graph.NewDependency(ss.tail, selectionBldr.GetRoot(), 1.0)
126+
ss.tail = selectionBldr.GetTail()
127+
}
108128
return nil
109129
}

internal/stackql/primitivegenerator/statement_analyzer.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,6 +1301,7 @@ func (pb *standardPrimitiveGenerator) inferHeirarchyAndPersist(
13011301
return err
13021302
}
13031303

1304+
//nolint:funlen,gocognit // acceptable for now
13041305
func (pb *standardPrimitiveGenerator) analyzeDelete(
13051306
pbi planbuilderinput.PlanBuilderInput,
13061307
) error {
@@ -1331,6 +1332,13 @@ func (pb *standardPrimitiveGenerator) analyzeDelete(
13311332
if err != nil {
13321333
return err
13331334
}
1335+
1336+
if pb.PrimitiveComposer.IsAwait() && !method.IsAwaitable() {
1337+
return fmt.Errorf("method %s is not awaitable", method.GetName())
1338+
}
1339+
if pb.PrimitiveComposer.IsAwait() && len(node.SelectExprs) > 0 {
1340+
return fmt.Errorf("returning from asynchronous delete is disallowed")
1341+
}
13341342
svc, err := tbl.GetService()
13351343
if err != nil {
13361344
return err
@@ -1358,20 +1366,35 @@ func (pb *standardPrimitiveGenerator) analyzeDelete(
13581366
if err != nil {
13591367
return err
13601368
}
1369+
columnHandles := []parserutil.ColumnHandle{}
1370+
if len(node.SelectExprs) > 0 {
1371+
if pb.PrimitiveComposer.IsAwait() {
1372+
return fmt.Errorf("delete with returning not allowed in await mode")
1373+
}
1374+
starColumns, _ := methodAnalysisOutput.GetOrderedStarColumnsNames()
1375+
columnHandles, err = parserutil.ExtractDeleteReturningColumnNames(
1376+
node,
1377+
starColumns,
1378+
handlerCtx.GetASTFormatter(),
1379+
)
1380+
if err != nil {
1381+
return err
1382+
}
1383+
}
13611384
err = pb.analyzeUnaryAction(
13621385
pbi,
13631386
handlerCtx,
13641387
node,
13651388
node.Where,
13661389
tbl,
1367-
[]parserutil.ColumnHandle{},
1390+
columnHandles,
13681391
methodAnalysisOutput,
13691392
)
13701393
if err != nil {
13711394
return err
13721395
}
13731396
pb.PrimitiveComposer.SetTable(node, tbl)
1374-
return err
1397+
return nil
13751398
}
13761399

13771400
func (pb *standardPrimitiveGenerator) analyzeDescribe(pbi planbuilderinput.PlanBuilderInput) error {

test/python/stackql_test_tooling/flask/gcp/app.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ def _extrapolate_target_from_operation(operation_name: str, project_name: str, h
8787
if project_name == 'mutable-project' and operation_name == 'operation-100000000003-10000000003-10000003-10000003':
8888
firewall_name = 'updatable-firewall'
8989
return f'https://{host_name}:1080/compute/v1/projects/{ project_name }/global/firewalls/{ firewall_name }'
90+
if project_name == 'mutable-project' and operation_name == 'operation-100000000004-10000000004-10000004-10000004':
91+
firewall_name = 'deletable-firewall'
92+
return f'https://{host_name}:1080/compute/v1/projects/{ project_name }/global/firewalls/{ firewall_name }'
9093
raise ValueError(f"Unsupported operation name: {operation_name} for project: {project_name}")
9194

9295
@app.route('/compute/v1/projects/<project_name>/global/operations/<operation_name>', methods=['GET'])
@@ -103,7 +106,7 @@ def projects_testing_project_global_operation_detail(project_name: str, operatio
103106
project_name=project_name,
104107
host_name=host_name,
105108
kind='compute#operation',
106-
operation_type='insert',
109+
operation_type='insert' if operation_name == 'operation-100000000001-10000000001-10000001-10000001' else 'update' if operation_name == 'operation-100000000002-10000000002-10000002-10000002' else 'delete' if operation_name == 'operation-100000000004-10000000004-10000004-10000004' else 'patch',
107110
progress=100,
108111
end_time='2025-07-05T19:43:34.491-07:00',
109112
), 200, {'Content-Type': 'application/json'}
@@ -318,7 +321,7 @@ def projects_testing_project_global_firewalls_replace(project_name: str, firewal
318321
project_name=project_name,
319322
host_name=host_name,
320323
kind='compute#operation',
321-
operation_type='put',
324+
operation_type='update',
322325
progress=0,
323326
), 200, {'Content-Type': 'application/json'}
324327

@@ -348,6 +351,29 @@ def projects_testing_project_global_firewalls_update(project_name: str, firewall
348351
progress=0,
349352
), 200, {'Content-Type': 'application/json'}
350353

354+
@app.route('/compute/v1/projects/<project_name>/global/firewalls/<firewall_name>', methods=['DELETE'])
355+
def projects_testing_project_global_firewalls_delete(project_name: str, firewall_name: str):
356+
_permitted_combinations = (('mutable-project', 'deletable-firewall'),)
357+
if (project_name, firewall_name) not in _permitted_combinations:
358+
return '{"msg": "Disallowed"}', 500, {'Content-Type': 'application/json'}
359+
operation_id = '1000000000004'
360+
operation_name = 'operation-100000000004-10000000004-10000004-10000004'
361+
host_name = 'host.docker.internal' if _IS_DOCKER else 'localhost'
362+
target_link = f'https://{host_name}:1080/compute/v1/projects/{ project_name }/global/firewalls/{ firewall_name }'
363+
if not project_name:
364+
return '{"msg": "Invalid request: project not supplied"}', 400, {'Content-Type': 'application/json'}
365+
return render_template(
366+
'global-operation.jinja.json',
367+
target_link=target_link,
368+
operation_id=operation_id,
369+
operation_name=operation_name,
370+
project_name=project_name,
371+
host_name=host_name,
372+
kind='compute#operation',
373+
operation_type='delete',
374+
progress=0,
375+
), 200, {'Content-Type': 'application/json'}
376+
351377
@app.route('/compute/v1/projects/<project_name>/global/firewalls/<firewall_name>', methods=['GET'])
352378
def projects_testing_project_global_firewalls_some_other_firewall(project_name: str, firewall_name: str):
353379
_permitted_combinations = (('testing-project', 'some-other-firewall'), ('mutable-project', 'updatable-firewall'), ('mutable-project', 'replacable-firewall'))

test/python/stackql_test_tooling/flask/gcp/templates/global-operation.jinja.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"kind": "{{ kind if kind else 'compute#operation' }}",
33
"id": "{{ operation_id if operation_id else '1000000000001' }}",
44
"name": "{{ operation_name }}",
5-
"operationType": "insert",
5+
"operationType": "{{ operation_type }}",
66
"targetLink": "{{ target_link }}",
77
"targetId": "{{ target_id if target_id else '2000000000002' }}",
88
"status": "{{ 'DONE' if progress and progress > 99 else 'RUNNING' }}",

0 commit comments

Comments
 (0)