@@ -10,6 +10,65 @@ import io.airbyte.cdk.discover.EmittedField
1010import io.airbyte.cdk.output.sockets.toJson
1111import io.airbyte.cdk.util.Jsons
1212
13+ /* * Builds a WHERE clause from lower/upper bounds on checkpoint columns. */
14+ fun buildWhereClause (
15+ checkpointColumns : List <EmittedField >,
16+ lowerBound : List <JsonNode >? ,
17+ upperBound : List <JsonNode >? ,
18+ isLowerBoundIncluded : Boolean ,
19+ ): WhereNode {
20+ val zippedLowerBound: List <Pair <EmittedField , JsonNode >> =
21+ lowerBound?.let { checkpointColumns.zip(it) } ? : listOf ()
22+ val lowerBoundDisj: List <WhereClauseNode > =
23+ zippedLowerBound.mapIndexed { idx: Int , (gtCol: EmittedField , gtValue: JsonNode ) ->
24+ val lastLeaf: WhereClauseLeafNode =
25+ if (isLowerBoundIncluded && idx == checkpointColumns.size - 1 ) {
26+ GreaterOrEqual (gtCol, gtValue)
27+ } else {
28+ Greater (gtCol, gtValue)
29+ }
30+ And (
31+ zippedLowerBound.take(idx).map { (eqCol: EmittedField , eqValue: JsonNode ) ->
32+ Equal (eqCol, eqValue)
33+ } + listOf (lastLeaf),
34+ )
35+ }
36+ val zippedUpperBound: List <Pair <EmittedField , JsonNode >> =
37+ upperBound?.let { checkpointColumns.zip(it) } ? : listOf ()
38+ val upperBoundDisj: List <WhereClauseNode > =
39+ zippedUpperBound.mapIndexed { idx: Int , (leqCol: EmittedField , leqValue: JsonNode ) ->
40+ val lastLeaf: WhereClauseLeafNode =
41+ if (idx < zippedUpperBound.size - 1 ) {
42+ Lesser (leqCol, leqValue)
43+ } else {
44+ LesserOrEqual (leqCol, leqValue)
45+ }
46+ And (
47+ zippedUpperBound.take(idx).map { (eqCol: EmittedField , eqValue: JsonNode ) ->
48+ Equal (eqCol, eqValue)
49+ } + listOf (lastLeaf),
50+ )
51+ }
52+ // Don't create WHERE clauses when there are no bounds
53+ if (lowerBoundDisj.isEmpty() && upperBoundDisj.isEmpty()) {
54+ return NoWhere
55+ }
56+
57+ // Build WHERE clause components only for non-empty bounds
58+ val clauses = mutableListOf<WhereClauseNode >()
59+ if (lowerBoundDisj.isNotEmpty()) {
60+ clauses.add(Or (lowerBoundDisj))
61+ }
62+ if (upperBoundDisj.isNotEmpty()) {
63+ clauses.add(Or (upperBoundDisj))
64+ }
65+
66+ return when (clauses.size) {
67+ 1 -> Where (clauses.first())
68+ else -> Where (And (clauses))
69+ }
70+ }
71+
1372/* * Base class for default implementations of [JdbcPartition]. */
1473sealed class DefaultJdbcPartition (
1574 val selectQueryGenerator : SelectQueryGenerator ,
@@ -28,7 +87,7 @@ sealed class DefaultJdbcUnsplittablePartition(
2887 override val nonResumableQuery: SelectQuery
2988 get() = selectQueryGenerator.generate(nonResumableQuerySpec.optimize())
3089
31- val nonResumableQuerySpec = SelectQuerySpec (SelectColumns (stream.fields), from)
90+ open val nonResumableQuerySpec = SelectQuerySpec (SelectColumns (stream.fields), from)
3291
3392 override fun samplingQuery (sampleRateInvPow2 : Int ): SelectQuery {
3493 val sampleSize: Int = streamState.sharedState.maxSampleSize
@@ -117,59 +176,7 @@ sealed class DefaultJdbcSplittablePartition(
117176 }
118177
119178 val where: WhereNode
120- get() {
121- val zippedLowerBound: List <Pair <EmittedField , JsonNode >> =
122- lowerBound?.let { checkpointColumns.zip(it) } ? : listOf ()
123- val lowerBoundDisj: List <WhereClauseNode > =
124- zippedLowerBound.mapIndexed { idx: Int , (gtCol: EmittedField , gtValue: JsonNode ) ->
125- val lastLeaf: WhereClauseLeafNode =
126- if (isLowerBoundIncluded && idx == checkpointColumns.size - 1 ) {
127- GreaterOrEqual (gtCol, gtValue)
128- } else {
129- Greater (gtCol, gtValue)
130- }
131- And (
132- zippedLowerBound.take(idx).map { (eqCol: EmittedField , eqValue: JsonNode ) ->
133- Equal (eqCol, eqValue)
134- } + listOf (lastLeaf),
135- )
136- }
137- val zippedUpperBound: List <Pair <EmittedField , JsonNode >> =
138- upperBound?.let { checkpointColumns.zip(it) } ? : listOf ()
139- val upperBoundDisj: List <WhereClauseNode > =
140- zippedUpperBound.mapIndexed { idx: Int , (leqCol: EmittedField , leqValue: JsonNode )
141- ->
142- val lastLeaf: WhereClauseLeafNode =
143- if (idx < zippedUpperBound.size - 1 ) {
144- Lesser (leqCol, leqValue)
145- } else {
146- LesserOrEqual (leqCol, leqValue)
147- }
148- And (
149- zippedUpperBound.take(idx).map { (eqCol: EmittedField , eqValue: JsonNode ) ->
150- Equal (eqCol, eqValue)
151- } + listOf (lastLeaf),
152- )
153- }
154- // Don't create WHERE clauses when there are no bounds
155- if (lowerBoundDisj.isEmpty() && upperBoundDisj.isEmpty()) {
156- return NoWhere
157- }
158-
159- // Build WHERE clause components only for non-empty bounds
160- val clauses = mutableListOf<WhereClauseNode >()
161- if (lowerBoundDisj.isNotEmpty()) {
162- clauses.add(Or (lowerBoundDisj))
163- }
164- if (upperBoundDisj.isNotEmpty()) {
165- clauses.add(Or (upperBoundDisj))
166- }
167-
168- return when (clauses.size) {
169- 1 -> Where (clauses.first())
170- else -> Where (And (clauses))
171- }
172- }
179+ get() = buildWhereClause(checkpointColumns, lowerBound, upperBound, isLowerBoundIncluded)
173180
174181 open val isLowerBoundIncluded: Boolean = false
175182}
@@ -272,6 +279,56 @@ class DefaultJdbcSplittableSnapshotWithCursorPartition(
272279 )
273280}
274281
282+ class DefaultUnsplittableJdbcCursorIncrementalPartition (
283+ selectQueryGenerator : SelectQueryGenerator ,
284+ streamState : DefaultJdbcStreamState ,
285+ val cursor : EmittedField ,
286+ val cursorLowerBound : JsonNode ,
287+ val isLowerBoundIncluded : Boolean ,
288+ val explicitCursorUpperBound : JsonNode ? ,
289+ ) :
290+ JdbcCursorPartition <DefaultJdbcStreamState >,
291+ DefaultJdbcUnsplittablePartition (selectQueryGenerator, streamState) {
292+
293+ val cursorUpperBound: JsonNode
294+ get() = explicitCursorUpperBound ? : streamState.cursorUpperBound ? : Jsons .nullNode()
295+
296+ override val cursorUpperBoundQuery: SelectQuery
297+ get() = selectQueryGenerator.generate(cursorUpperBoundQuerySpec.optimize())
298+
299+ val cursorUpperBoundQuerySpec = SelectQuerySpec (SelectColumnMaxValue (cursor), from)
300+
301+ val lowerBound: List <JsonNode > = listOf (cursorLowerBound)
302+ val upperBound: List <JsonNode >
303+ get() = listOf (cursorUpperBound)
304+
305+ override val completeState: OpaqueStateValue
306+ get() =
307+ DefaultJdbcStreamStateValue .cursorIncrementalCheckpoint(
308+ cursor,
309+ cursorCheckpoint = cursorUpperBound,
310+ )
311+
312+ override val nonResumableQuerySpec: SelectQuerySpec
313+ get() = SelectQuerySpec (SelectColumns (stream.fields), from, where)
314+
315+ val checkpointColumns: List <EmittedField > = listOf (cursor)
316+ val where: WhereNode
317+ get() = buildWhereClause(checkpointColumns, lowerBound, upperBound, isLowerBoundIncluded)
318+
319+ override fun samplingQuery (sampleRateInvPow2 : Int ): SelectQuery {
320+ val sampleSize: Int = streamState.sharedState.maxSampleSize
321+ val querySpec =
322+ SelectQuerySpec (
323+ SelectColumns (stream.fields + checkpointColumns),
324+ FromSample (stream.name, stream.namespace, sampleRateInvPow2, sampleSize, where),
325+ NoWhere , // WHERE is already in FromSample, don't duplicate in outer query
326+ OrderBy (checkpointColumns),
327+ )
328+ return selectQueryGenerator.generate(querySpec.optimize())
329+ }
330+ }
331+
275332/* *
276333 * Default implementation of a [JdbcPartition] for a cursor incremental partition. These are always
277334 * splittable.
0 commit comments