Skip to content

Draft: Create a SequenceTracker for Postgres sequences. - #2986

Open
nicktobey wants to merge 1 commit into
mainfrom
nicktobey/sequences
Open

Draft: Create a SequenceTracker for Postgres sequences.#2986
nicktobey wants to merge 1 commit into
mainfrom
nicktobey/sequences

Conversation

@nicktobey

Copy link
Copy Markdown
Contributor

No description provided.

@github-actions

Copy link
Copy Markdown
Contributor
Main PR
covering_index_scan_postgres 2020.39/s ${\color{red}DNF}$
groupby_scan_postgres 110.69/s ${\color{red}DNF}$
index_join_postgres 668.30/s ${\color{red}DNF}$
index_join_scan_postgres 844.14/s ${\color{red}DNF}$
index_scan_postgres 34.93/s ${\color{red}DNF}$
oltp_delete_insert_postgres 826.32/s ${\color{red}DNF}$
oltp_insert 715.22/s ${\color{red}DNF}$
oltp_point_select 3486.46/s ${\color{red}DNF}$
oltp_read_only 3266.85/s ${\color{red}DNF}$
oltp_read_write 2354.41/s ${\color{red}DNF}$
oltp_update_index 766.51/s ${\color{red}DNF}$
oltp_update_non_index 819.44/s ${\color{red}DNF}$
oltp_write_only 1718.06/s ${\color{red}DNF}$
select_random_points 1940.74/s ${\color{red}DNF}$
select_random_ranges 1548.18/s ${\color{red}DNF}$
table_scan_postgres 32.62/s ${\color{red}DNF}$
types_delete_insert_postgres 774.84/s ${\color{red}DNF}$
types_table_scan_postgres 14.41/s ${\color{red}DNF}$

@itoqa

itoqa Bot commented Jul 29, 2026

Copy link
Copy Markdown

Ito QA test results
Commit: 112c24f: 15 test cases ran, 2 failed ❌, 12 passed ✅, 1 additional finding ⚠️.

Summary

Coverage spans sequence naming and validation, generated identifiers, value updates, persistence across transactions, restarts and branches, boundary and cycling behavior, and concurrent writes. The results show broad happy-path and edge-case coverage, including malformed inputs and adversarial concurrent allocation scenarios.

Not safe to merge yet — PR-attributable high-severity failures show that concurrent sequence use can both lose writes and issue duplicate values, creating a direct integrity risk for core database operations. Sequence renaming is also unsupported, but that finding is outside this PR and is a flag for later rather than a merge driver.

Tests run by Ito

View full run

Result Severity Type Description
High severity Nextval The expected result was 160 successful inserts with unique generated IDs and a sequence that remains available to subsequent sessions. In the recorded retry, 110 rows committed and at least one concurrent client failed with relation-not-found for myseq; later inserts continued at 111 through 113, but sequence metadata access intermittently returned the same relation error.
High severity Tracker Both concurrent transactions returned 1 and committed, while subsequent allocations returned 3, 5, and 7. The expected behavior was distinct committed values, with the second transaction receiving the next value and the merged state preserving both advances.
Nextval Unqualified, schema-qualified, database-qualified, and surrounding double-quoted sequence names all resolved to the intended local sequence and returned successive values from 10 through 13.
Nextval Malformed four-part input returned a parse error, a missing sequence returned a relation-not-found error, and the subsequent valid myseq call returned its first value, 1.
Persistence Two independent clients received sequence values 1 and 3; after client A committed and client B rolled back, a fresh session returned 3, confirming committed visibility without rolled-back leakage.
Persistence Committed main and feature roots retained independent sequence progress across checkout: main resumed at 3 and feature resumed at 5 without duplicates or resets.
Sequence Created myseq, generated 1, restarted Doltgres with the same data directory, and generated 2, confirming persisted sequence state.
Sequence smallserial, serial, and bigserial each generated identifier 1, with int16, int32, and int64-compatible sequence bounds respectively.
Sequence The ALTER-owned integer sequence retained its nextval default and synchronized metadata, tracker state, and persisted allocation across restart; values advanced from 2147483646 to 2147483647.
Setval In isolated local Doltgres, setval with auto-advance disabled returned 10 and the next nextval returned 10; enabling auto-advance at 20 made the next nextval return 25. Persisted sequence state read back with last_value 25 and increment 5.
Setval A sequence allocated 1, 2, and 3, then setval at the persisted maximum with auto-advance enabled. Both independent sessions consistently rejected the next allocation at the maximum, and persisted sequence state remained aligned.
Tracker Source verification confirms that opposing sequence directions are rejected by SequenceState.Merge. The recorded runtime attempt did not reach that merge path because changing an existing sequence increment is unsupported, and the replacement strategy created a new sequence rather than merging opposing states.
Tracker A non-cycling sequence returned 1, 2, and 3 before reporting a maximum-value error, while the cycling sequence wrapped from 3 back to 1 as expected.
Tracker A descending bigint sequence returned values through -9223372036854775807, then reported the configured minimum-value error without underflow. The cycling variant restarted at its maximum bound, -9223372036854775800.
⚠️ Medium severity Persistence The ALTER SEQUENCE rename returned 'RENAME SEQUENCE is not yet supported'. The new name did not exist, while the old name remained usable and advanced, contrary to the expected new-name-only behavior.
Additional Findings Details

These findings are unrelated to the current changes but were observed during testing.

🟡 Sequence rename is not supported
  • Severity: Medium Medium severity
  • Description: The ALTER SEQUENCE rename returned 'RENAME SEQUENCE is not yet supported'. The new name did not exist, while the old name remained usable and advanced, contrary to the expected new-name-only behavior.
  • Impact: Database operators cannot rename sequences through the supported SQL workflow, so migrations or maintenance tasks that require a rename fail and must continue using the old name or be performed manually.
  • Steps to Reproduce:
    1. Create an integer sequence named myseq starting at 1 with an increment of 2.
    2. Call nextval('myseq') to establish the initial sequence value.
    3. Run ALTER SEQUENCE myseq RENAME TO renamedseq.
    4. Verify that renamedseq resolves and myseq returns a missing-relation error, including after reopening the database.
  • Stub / mock content: The test used an isolated local database and psql sessions; no application behavior was mocked or bypassed.
  • Code Analysis: The production AST handler unconditionally returns an unsupported error for sequence renames at server/ast/rename_table.go:30-32, which directly explains the observed SQL failure. The lower-level core/sequences/root_object.go:323-340 path does drop and recreate the persisted collection entry, but its added TODO at line 328 leaves the global tracker identity update unimplemented; core/sequences/collection.go:70-73 confirms tracker identity is keyed globally. The PR diff does not include server/ast/rename_table.go, so the observed rejection is treated as pre-existing/outside this PR; the smallest practical fix for the tested behavior is to route sequence rename statements through the existing root-object rename operation and update the tracker key atomically with the persisted rename.
Evidence Package

Tip

Reply with @itoqa to send us feedback on this test run.

return 0, errors.Errorf(`relation "%s" does not exist`, sequenceString)
}

next, err := ait.Next(ctx, sequenceString, nil)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View All Evidence

High severity Concurrent sequence allocations lose inserts

What failed: The expected result was 160 successful inserts with unique generated IDs and a sequence that remains available to subsequent sessions. In the recorded retry, 110 rows committed and at least one concurrent client failed with relation-not-found for myseq; later inserts continued at 111 through 113, but sequence metadata access intermittently returned the same relation error.

Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
  • Severity: High High severity
  • Impact: Concurrent database clients can lose inserts when generated-ID allocation intermittently fails, leaving core write workflows incomplete. Subsequent sessions may also encounter relation errors while reading sequence metadata.
  • Steps to Reproduce:
    1. Create a sequence and a table whose generated ID uses nextval('myseq').
    2. Run concurrent clients that insert multiple rows using the generated default.
    3. Read the row count and distinct generated IDs from a fresh session, then issue additional nextval-backed inserts.
  • Stub / mock content: The test used a locally configured Doltgres instance, a temporary database, sequence, and audit table; no application mocks, route interceptions, or test bypasses were applied.
  • Code Analysis: server/functions/nextval.go:35-67, which was added by the PR, obtains the shared sequence collection, allocates through the SequenceTracker, and then calls collection.SetVal to mirror the allocation. In core/sequences/collection.go:438-457, SetVal mutates the cached Sequence fields, while getSequence at lines 534-555 reads and writes the shared accessedMap and writeCache at lines 557-584 iterates and clears that map without synchronization. The Sequence.mu declared at line 192 is not used by these paths. The smallest practical fix is to serialize access to the collection cache and the cached sequence state across getSequence, SetVal, and writeCache, or otherwise ensure the tracker-to-collection mirror is performed through a synchronized update path.
  • Why this is likely a bug: The failure occurs in a direct local SQL workload rather than a browser-only check, and the source has a concrete unsynchronized shared-state path matching the intermittent relation errors and incomplete concurrent writes. The PR introduced the tracker-to-collection update path and the associated shared cache changes, so a targeted synchronization of those paths is the practical remediation.
Relevant code

server/functions/nextval.go:58-67

next, err := ait.Next(ctx, sequenceString, nil)
if err != nil {
	return 0, err
}
err = collection.SetVal(ctx, sequenceId, next, true, true)

core/sequences/collection.go:438-455

seq, err := pgs.getSequence(ctx, name)
...
seq.Current = newValue
seq.IsAtEnd = false
seq.HasBeenCalled = hasBeenCalled
if autoAdvance {
	_, err := seq.nextValForSequence()

core/sequences/collection.go:534-555

if seq, ok := pgs.accessedMap[name]; ok {
	return seq, nil
}
...
pgs.accessedMap[seq.Id] = seq
return seq, nil

core/sequences/collection.go:557-584

for _, seq := range pgs.accessedMap {
	...
}
pgs.underlyingMap = flushed
clear(pgs.accessedMap)
Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.

**High severity — Concurrent sequence allocations lose inserts**

**What failed:** The expected result was 160 successful inserts with unique generated IDs and a sequence that remains available to subsequent sessions. In the recorded retry, 110 rows committed and at least one concurrent client failed with relation-not-found for myseq; later inserts continued at 111 through 113, but sequence metadata access intermittently returned the same relation error.

- **Impact:** Concurrent database clients can lose inserts when generated-ID allocation intermittently fails, leaving core write workflows incomplete. Subsequent sessions may also encounter relation errors while reading sequence metadata.
- **Steps to reproduce:**
  1. Create a sequence and a table whose generated ID uses nextval('myseq').
  2. Run concurrent clients that insert multiple rows using the generated default.
  3. Read the row count and distinct generated IDs from a fresh session, then issue additional nextval-backed inserts.
- **Stub / mock content:** The test used a locally configured Doltgres instance, a temporary database, sequence, and audit table; no application mocks, route interceptions, or test bypasses were applied.
- **Code analysis:** server/functions/nextval.go:35-67, which was added by the PR, obtains the shared sequence collection, allocates through the SequenceTracker, and then calls collection.SetVal to mirror the allocation. In core/sequences/collection.go:438-457, SetVal mutates the cached Sequence fields, while getSequence at lines 534-555 reads and writes the shared accessedMap and writeCache at lines 557-584 iterates and clears that map without synchronization. The Sequence.mu declared at line 192 is not used by these paths. The smallest practical fix is to serialize access to the collection cache and the cached sequence state across getSequence, SetVal, and writeCache, or otherwise ensure the tracker-to-collection mirror is performed through a synchronized update path.
- **Why this is likely a bug:** The failure occurs in a direct local SQL workload rather than a browser-only check, and the source has a concrete unsynchronized shared-state path matching the intermittent relation errors and incomplete concurrent writes. The PR introduced the tracker-to-collection update path and the associated shared cache changes, so a targeted synchronization of those paths is the practical remediation.

**Relevant code:**

`server/functions/nextval.go:58-67`

~~~go
next, err := ait.Next(ctx, sequenceString, nil)
if err != nil {
	return 0, err
}
err = collection.SetVal(ctx, sequenceId, next, true, true)
~~~

`core/sequences/collection.go:438-455`

~~~go
seq, err := pgs.getSequence(ctx, name)
...
seq.Current = newValue
seq.IsAtEnd = false
seq.HasBeenCalled = hasBeenCalled
if autoAdvance {
	_, err := seq.nextValForSequence()
~~~

`core/sequences/collection.go:534-555`

~~~go
if seq, ok := pgs.accessedMap[name]; ok {
	return seq, nil
}
...
pgs.accessedMap[seq.Id] = seq
return seq, nil
~~~

`core/sequences/collection.go:557-584`

~~~go
for _, seq := range pgs.accessedMap {
	...
}
pgs.underlyingMap = flushed
clear(pgs.accessedMap)
~~~

// SequenceTrackerKey is the key to identify the SequenceTracker in the globalstate.GlobalState
var SequenceTrackerKey dsess.TrackerKey[*SequenceTracker] = struct{}{}

func (sequence SequenceState) Merge(otherSequenceState SequenceState) (merged SequenceState, ok bool) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View All Evidence

High severity Concurrent sequence allocations reuse values

What failed: Both concurrent transactions returned 1 and committed, while subsequent allocations returned 3, 5, and 7. The expected behavior was distinct committed values, with the second transaction receiving the next value and the merged state preserving both advances.

Impact · Steps · Stub / mock · Analysis · Why this is likely a bug
  • Severity: High High severity
  • Impact: Concurrent committed allocations can return the same sequence value, allowing users or downstream records to receive duplicate identifiers or ordering values. The lost advance also causes later allocations to diverge from the number of committed transactions, and cannot be corrected for values already issued.
  • Steps to Reproduce:
    1. Create an integer sequence starting at 1 with increment 2 and cache 1.
    2. Start two independent transactions against the same sequence and call nextval from both before either transaction commits.
    3. Commit the transactions in reverse order, then call nextval repeatedly after the commits.
    4. Compare the values returned by both transactions with subsequent allocations.
  • Stub / mock content: No stubs, mocks, or bypasses were applied for this test in the recorded run.
  • Code Analysis: The PR introduces SequenceState.Merge at core/sequences/collection.go:75-96. For compatible states it chooses one state's Current at lines 90-94; when concurrent transaction states represent the same base progress, the equal-state comparison in GreaterThan at lines 116-139 does not encode that two allocations occurred, so merging can retain only one advance. The new nextval path in server/functions/nextval.go:58-64 obtains a tracker value and then mirrors it into the collection, making this merge/allocation contract part of the production sequence path. The smallest practical fix is to make tracker transaction merges account for each committed allocation, or serialize allocation at the tracker boundary so concurrent committed calls cannot merge to the same returned value; preserve the furthest resulting state rather than collapsing equal concurrent states.
  • Why this is likely a bug: The duplicate value was observed in a local transaction run, and the production code now merges transaction-local sequence states by selecting a single Current without representing both committed allocations. The later 3, 5, 7 progression confirms the sequence advanced only once for the two calls, which is a silent uniqueness failure rather than an environment-only error.
Relevant code

core/sequences/collection.go:75-96

func (sequence SequenceState) Merge(otherSequenceState SequenceState) (merged SequenceState, ok bool) {
	newSequenceState := sequence
	if sequence.Increment >= 0 && otherSequenceState.Increment >= 0 {
		newSequenceState.Increment = utils.Min(sequence.Increment, otherSequenceState.Increment)
		newSequenceState.Start = utils.Min(sequence.Start, otherSequenceState.Start)
	}
	if sequence.GreaterThan(otherSequenceState) {
		newSequenceState.Current = sequence.Current
	} else {
		newSequenceState.Current = otherSequenceState.Current
	}
	return newSequenceState, true
}

core/sequences/collection.go:116-139

func (sequence SequenceState) GreaterThan(other SequenceState) bool {
	if sequence.Increment > 0 {
		hasWrapped := sequence.Current < sequence.Start
		otherHasWrapped := other.Current < sequence.Start
		if hasWrapped == otherHasWrapped {
			return sequence.Current > other.Current
		}
		return hasWrapped
	}
	...
}

server/functions/nextval.go:35-67

func nextval(ctx *sql.Context, ait *sequences.SequenceTracker, relationName string) (int64, error) {
	...
	next, err := ait.Next(ctx, sequenceString, nil)
	if err != nil {
		return 0, err
	}
	err = collection.SetVal(ctx, sequenceId, next, true, true)
	...
	return next, err
}
Evidence Package
Copy prompt for an agent
Ito QA identified the following failure during automated PR testing. Please investigate and propose a fix.

**High severity — Concurrent sequence allocations reuse values**

**What failed:** Both concurrent transactions returned 1 and committed, while subsequent allocations returned 3, 5, and 7. The expected behavior was distinct committed values, with the second transaction receiving the next value and the merged state preserving both advances.

- **Impact:** Concurrent committed allocations can return the same sequence value, allowing users or downstream records to receive duplicate identifiers or ordering values. The lost advance also causes later allocations to diverge from the number of committed transactions, and cannot be corrected for values already issued.
- **Steps to reproduce:**
  1. Create an integer sequence starting at 1 with increment 2 and cache 1.
  2. Start two independent transactions against the same sequence and call nextval from both before either transaction commits.
  3. Commit the transactions in reverse order, then call nextval repeatedly after the commits.
  4. Compare the values returned by both transactions with subsequent allocations.
- **Stub / mock content:** No stubs, mocks, or bypasses were applied for this test in the recorded run.
- **Code analysis:** The PR introduces SequenceState.Merge at core/sequences/collection.go:75-96. For compatible states it chooses one state's Current at lines 90-94; when concurrent transaction states represent the same base progress, the equal-state comparison in GreaterThan at lines 116-139 does not encode that two allocations occurred, so merging can retain only one advance. The new nextval path in server/functions/nextval.go:58-64 obtains a tracker value and then mirrors it into the collection, making this merge/allocation contract part of the production sequence path. The smallest practical fix is to make tracker transaction merges account for each committed allocation, or serialize allocation at the tracker boundary so concurrent committed calls cannot merge to the same returned value; preserve the furthest resulting state rather than collapsing equal concurrent states.
- **Why this is likely a bug:** The duplicate value was observed in a local transaction run, and the production code now merges transaction-local sequence states by selecting a single Current without representing both committed allocations. The later 3, 5, 7 progression confirms the sequence advanced only once for the two calls, which is a silent uniqueness failure rather than an environment-only error.

**Relevant code:**

`core/sequences/collection.go:75-96`

~~~go
func (sequence SequenceState) Merge(otherSequenceState SequenceState) (merged SequenceState, ok bool) {
	newSequenceState := sequence
	if sequence.Increment >= 0 && otherSequenceState.Increment >= 0 {
		newSequenceState.Increment = utils.Min(sequence.Increment, otherSequenceState.Increment)
		newSequenceState.Start = utils.Min(sequence.Start, otherSequenceState.Start)
	}
	if sequence.GreaterThan(otherSequenceState) {
		newSequenceState.Current = sequence.Current
	} else {
		newSequenceState.Current = otherSequenceState.Current
	}
	return newSequenceState, true
}
~~~

`core/sequences/collection.go:116-139`

~~~go
func (sequence SequenceState) GreaterThan(other SequenceState) bool {
	if sequence.Increment > 0 {
		hasWrapped := sequence.Current < sequence.Start
		otherHasWrapped := other.Current < sequence.Start
		if hasWrapped == otherHasWrapped {
			return sequence.Current > other.Current
		}
		return hasWrapped
	}
	...
}
~~~

`server/functions/nextval.go:35-67`

~~~go
func nextval(ctx *sql.Context, ait *sequences.SequenceTracker, relationName string) (int64, error) {
	...
	next, err := ait.Next(ctx, sequenceString, nil)
	if err != nil {
		return 0, err
	}
	err = collection.SetVal(ctx, sequenceId, next, true, true)
	...
	return next, err
}
~~~

@zachmu zachmu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how complete this is but nothing too objectionable

Comment thread server/functions/nextval.go
Comment thread server/functions/setval.go
Comment thread server/functions/setval.go
Comment thread server/functions/setval.go
"github.com/stretchr/testify/require"
)

func TestConcurrentSequences(t *testing.T) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe there is a skipped test that does this too, might want to consolidate

Expected: []sql.Row{{1}},
},
{
Query: "/* client b */ select nextval('myseq')",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should have each client commit as well, then assert the current values

Comment thread testing/go/sequences_test.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants