Skip to content

Commit 65712df

Browse files
committed
Add category field to dataset metadata for AI-generated dataset isolation
Adds a `category` column to the `_datasets_meta` schema via a new migration, threaded through the full Rust core → FFI → SDK → Desktop stack. The ChartDatasetGenerator now tags AI-derived datasets with category "ai-generated" when calling ImportFromContentAsync. The SDK's IDatasetService and DatasetInfo types expose the new optional category field with backward-compatible defaults. Rust changes: schema migration, DatasetMeta.category field, store CRUD/mutations updated to read/write/copy category, new set_category() helper. FFI request structs (CreateEmptyRequest, ImportContentRequest) accept optional category. SDK/Desktop: DatasetInfo gains Category property, IDatasetService methods CreateEmptyDatasetAsync and ImportFromContentAsync accept optional category parameter, DatasetService.Mutations threads it to FFI JSON requests. Version bumps: SDK 1.55.0 → 1.56.0, Desktop 1.55.5 → 1.56.0.
1 parent 4e29d5d commit 65712df

12 files changed

Lines changed: 67 additions & 24 deletions

File tree

core/privstack-datasets/src/schema.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ CREATE TABLE IF NOT EXISTS _dataset_saved_queries (
7474
const SAVED_QUERIES_ADD_IS_VIEW: &str =
7575
"ALTER TABLE _dataset_saved_queries ADD COLUMN IF NOT EXISTS is_view BOOLEAN DEFAULT FALSE;";
7676

77+
/// Migration: add category column to datasets metadata for AI-generated dataset isolation.
78+
const DATASETS_META_ADD_CATEGORY: &str =
79+
"ALTER TABLE _datasets_meta ADD COLUMN IF NOT EXISTS category VARCHAR;";
80+
7781
/// Initialize all dataset schema tables.
7882
pub fn initialize_datasets_schema(conn: &Connection) -> DatasetResult<()> {
7983
conn.execute_batch(DATASETS_META_DDL)?;
@@ -83,6 +87,7 @@ pub fn initialize_datasets_schema(conn: &Connection) -> DatasetResult<()> {
8387
conn.execute_batch(DATASET_SAVED_QUERIES_DDL)?;
8488
// Migrations
8589
conn.execute_batch(SAVED_QUERIES_ADD_IS_VIEW)?;
90+
conn.execute_batch(DATASETS_META_ADD_CATEGORY)?;
8691
Ok(())
8792
}
8893

core/privstack-datasets/src/store/crud.rs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ impl DatasetStore {
6767
source_file_name,
6868
row_count,
6969
columns,
70+
category: None,
7071
created_at: now,
7172
modified_at: now,
7273
})
@@ -76,7 +77,7 @@ impl DatasetStore {
7677
pub fn list(&self) -> DatasetResult<Vec<DatasetMeta>> {
7778
let conn = self.lock_conn();
7879
let mut stmt = conn.prepare(
79-
"SELECT id, name, source_file_name, row_count, columns_json, created_at, modified_at FROM _datasets_meta ORDER BY modified_at DESC"
80+
"SELECT id, name, source_file_name, row_count, columns_json, created_at, modified_at, category FROM _datasets_meta ORDER BY modified_at DESC"
8081
)?;
8182

8283
let rows = stmt
@@ -89,6 +90,7 @@ impl DatasetStore {
8990
row.get::<_, String>(4)?,
9091
row.get::<_, i64>(5)?,
9192
row.get::<_, i64>(6)?,
93+
row.get::<_, Option<String>>(7)?,
9294
))
9395
})?
9496
.filter_map(|r| r.ok())
@@ -98,7 +100,7 @@ impl DatasetStore {
98100
drop(conn);
99101

100102
rows.into_iter()
101-
.map(|(id, name, source, row_count, cols_json, created, modified)| {
103+
.map(|(id, name, source, row_count, cols_json, created, modified, category)| {
102104
let columns: Vec<DatasetColumn> =
103105
serde_json::from_str(&cols_json).unwrap_or_default();
104106
Ok(DatasetMeta {
@@ -109,6 +111,7 @@ impl DatasetStore {
109111
source_file_name: source,
110112
row_count,
111113
columns,
114+
category,
112115
created_at: created,
113116
modified_at: modified,
114117
})
@@ -120,7 +123,7 @@ impl DatasetStore {
120123
pub fn get(&self, id: &DatasetId) -> DatasetResult<DatasetMeta> {
121124
let conn = self.lock_conn();
122125
let result = conn.query_row(
123-
"SELECT name, source_file_name, row_count, columns_json, created_at, modified_at FROM _datasets_meta WHERE id = ?",
126+
"SELECT name, source_file_name, row_count, columns_json, created_at, modified_at, category FROM _datasets_meta WHERE id = ?",
124127
params![id.to_string()],
125128
|row| {
126129
Ok((
@@ -130,12 +133,13 @@ impl DatasetStore {
130133
row.get::<_, String>(3)?,
131134
row.get::<_, i64>(4)?,
132135
row.get::<_, i64>(5)?,
136+
row.get::<_, Option<String>>(6)?,
133137
))
134138
},
135139
);
136140

137141
match result {
138-
Ok((name, source, row_count, cols_json, created, modified)) => {
142+
Ok((name, source, row_count, cols_json, created, modified, category)) => {
139143
let columns: Vec<DatasetColumn> =
140144
serde_json::from_str(&cols_json).unwrap_or_default();
141145
Ok(DatasetMeta {
@@ -144,6 +148,7 @@ impl DatasetStore {
144148
source_file_name: source,
145149
row_count,
146150
columns,
151+
category,
147152
created_at: created,
148153
modified_at: modified,
149154
})
@@ -188,6 +193,21 @@ impl DatasetStore {
188193
Ok(())
189194
}
190195

196+
/// Set or clear the category for a dataset.
197+
pub fn set_category(&self, id: &DatasetId, category: Option<&str>) -> DatasetResult<()> {
198+
let now = now_millis();
199+
let conn = self.lock_conn();
200+
let updated = conn.execute(
201+
"UPDATE _datasets_meta SET category = ?, modified_at = ? WHERE id = ?",
202+
params![category, now, id.to_string()],
203+
)?;
204+
205+
if updated == 0 {
206+
return Err(DatasetError::NotFound(id.to_string()));
207+
}
208+
Ok(())
209+
}
210+
191211
/// Rename a dataset.
192212
pub fn rename(&self, id: &DatasetId, new_name: &str) -> DatasetResult<()> {
193213
let now = now_millis();

core/privstack-datasets/src/store/mutations.rs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ impl DatasetStore {
1414
&self,
1515
name: &str,
1616
columns: &[ColumnDef],
17+
category: Option<&str>,
1718
) -> DatasetResult<DatasetMeta> {
1819
if columns.is_empty() {
1920
return Err(DatasetError::InvalidQuery(
@@ -44,9 +45,9 @@ impl DatasetStore {
4445
let columns_json = serde_json::to_string(&ds_columns)?;
4546

4647
conn.execute(
47-
r#"INSERT INTO _datasets_meta (id, name, source_file_name, row_count, columns_json, created_at, modified_at)
48-
VALUES (?, ?, NULL, 0, ?, ?, ?)"#,
49-
params![id.to_string(), name, columns_json, now, now],
48+
r#"INSERT INTO _datasets_meta (id, name, source_file_name, row_count, columns_json, category, created_at, modified_at)
49+
VALUES (?, ?, NULL, 0, ?, ?, ?, ?)"#,
50+
params![id.to_string(), name, columns_json, category, now, now],
5051
)?;
5152

5253
info!(dataset_id = %id, name, "Empty dataset created");
@@ -57,6 +58,7 @@ impl DatasetStore {
5758
source_file_name: None,
5859
row_count: 0,
5960
columns: ds_columns,
61+
category: category.map(|s| s.to_string()),
6062
created_at: now,
6163
modified_at: now,
6264
})
@@ -75,6 +77,15 @@ impl DatasetStore {
7577

7678
let conn = self.lock_conn();
7779

80+
// Read source category before duplicating
81+
let source_category: Option<String> = conn
82+
.query_row(
83+
"SELECT category FROM _datasets_meta WHERE id = ?",
84+
params![source_id.to_string()],
85+
|row| row.get(0),
86+
)
87+
.unwrap_or(None);
88+
7889
let create_sql = format!("CREATE TABLE {new_table} AS SELECT * FROM {source_table}");
7990
conn.execute_batch(&create_sql).map_err(|e| {
8091
DatasetError::ImportFailed(format!("Failed to duplicate dataset: {e}"))
@@ -88,9 +99,9 @@ impl DatasetStore {
8899
let columns_json = serde_json::to_string(&columns)?;
89100

90101
conn.execute(
91-
r#"INSERT INTO _datasets_meta (id, name, source_file_name, row_count, columns_json, created_at, modified_at)
92-
VALUES (?, ?, NULL, ?, ?, ?, ?)"#,
93-
params![new_id.to_string(), new_name, row_count, columns_json, now, now],
102+
r#"INSERT INTO _datasets_meta (id, name, source_file_name, row_count, columns_json, category, created_at, modified_at)
103+
VALUES (?, ?, NULL, ?, ?, ?, ?, ?)"#,
104+
params![new_id.to_string(), new_name, row_count, columns_json, source_category, now, now],
94105
)?;
95106

96107
info!(dataset_id = %new_id, new_name, "Dataset duplicated from {}", source_id);
@@ -101,6 +112,7 @@ impl DatasetStore {
101112
source_file_name: None,
102113
row_count,
103114
columns,
115+
category: source_category,
104116
created_at: now,
105117
modified_at: now,
106118
})
@@ -111,6 +123,7 @@ impl DatasetStore {
111123
&self,
112124
csv_content: &str,
113125
name: &str,
126+
category: Option<&str>,
114127
) -> DatasetResult<DatasetMeta> {
115128
let id = DatasetId::new();
116129
let table = dataset_table_name(&id);
@@ -143,9 +156,9 @@ impl DatasetStore {
143156
let columns_json = serde_json::to_string(&columns)?;
144157

145158
conn.execute(
146-
r#"INSERT INTO _datasets_meta (id, name, source_file_name, row_count, columns_json, created_at, modified_at)
147-
VALUES (?, ?, NULL, ?, ?, ?, ?)"#,
148-
params![id.to_string(), name, row_count, columns_json, now, now],
159+
r#"INSERT INTO _datasets_meta (id, name, source_file_name, row_count, columns_json, category, created_at, modified_at)
160+
VALUES (?, ?, NULL, ?, ?, ?, ?, ?)"#,
161+
params![id.to_string(), name, row_count, columns_json, category, now, now],
149162
)?;
150163

151164
info!(dataset_id = %id, name, row_count, "Dataset imported from content");
@@ -156,6 +169,7 @@ impl DatasetStore {
156169
source_file_name: None,
157170
row_count,
158171
columns,
172+
category: category.map(|s| s.to_string()),
159173
created_at: now,
160174
modified_at: now,
161175
})

core/privstack-datasets/src/types.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ pub struct DatasetMeta {
8080
pub source_file_name: Option<String>,
8181
pub row_count: i64,
8282
pub columns: Vec<DatasetColumn>,
83+
pub category: Option<String>,
8384
pub created_at: i64,
8485
pub modified_at: i64,
8586
}

core/privstack-ffi/src/datasets/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ pub(crate) struct SavedQueryRequest {
175175
pub(crate) struct CreateEmptyRequest {
176176
pub name: String,
177177
pub columns: Vec<privstack_datasets::ColumnDef>,
178+
pub category: Option<String>,
178179
}
179180

180181
#[derive(Deserialize)]
@@ -187,6 +188,7 @@ pub(crate) struct DuplicateRequest {
187188
pub(crate) struct ImportContentRequest {
188189
pub content: String,
189190
pub name: String,
191+
pub category: Option<String>,
190192
}
191193

192194
#[derive(Deserialize)]

core/privstack-ffi/src/datasets/mutations.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub unsafe extern "C" fn privstack_dataset_create_empty(
1818
let req = parse_json_request!(request_json, CreateEmptyRequest);
1919

2020
with_store_json!(r#"{"error":"not initialized"}"#, |store| {
21-
match store.create_empty(&req.name, &req.columns) {
21+
match store.create_empty(&req.name, &req.columns, req.category.as_deref()) {
2222
Ok(meta) => {
2323
let json =
2424
serde_json::to_string(&meta).unwrap_or_else(|_| "{}".to_string());
@@ -73,7 +73,7 @@ pub unsafe extern "C" fn privstack_dataset_import_content(
7373
let req = parse_json_request!(request_json, ImportContentRequest);
7474

7575
with_store_json!(r#"{"error":"not initialized"}"#, |store| {
76-
match store.import_csv_content(&req.content, &req.name) {
76+
match store.import_csv_content(&req.content, &req.name, req.category.as_deref()) {
7777
Ok(meta) => {
7878
let json =
7979
serde_json::to_string(&meta).unwrap_or_else(|_| "{}".to_string());

desktop/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<Project>
22
<PropertyGroup>
3-
<PrivStackSdkVersion>1.55.0</PrivStackSdkVersion>
3+
<PrivStackSdkVersion>1.56.0</PrivStackSdkVersion>
44
</PropertyGroup>
55
</Project>

desktop/PrivStack.Desktop/PrivStack.Desktop.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<CFBundleDisplayName>PrivStack</CFBundleDisplayName>
1616
<AssemblyTitle>PrivStack</AssemblyTitle>
1717
<Product>PrivStack</Product>
18-
<Version>1.55.5</Version>
18+
<Version>1.56.0</Version>
1919

2020
<!-- LlamaSharp.Backend.Cpu ships multiple AVX variants (avx/avx2/avx512/base) that collide on publish -->
2121
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>

desktop/PrivStack.Desktop/Services/AI/ChartDatasetGenerator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public ChartDatasetGenerator(IDatasetService datasetService)
5454
return null;
5555

5656
var datasetName = $"[AI] {SanitizeName(chart.Title)}{sourceDatasetName}";
57-
var dataset = await _datasetService.ImportFromContentAsync(csv, datasetName, ct);
57+
var dataset = await _datasetService.ImportFromContentAsync(csv, datasetName, ct, category: "ai-generated");
5858
Log.Information("Created chart dataset {Id} ({Name}) with aggregated data", dataset.Id, datasetName);
5959
return dataset.Id.Value;
6060
}

desktop/PrivStack.Desktop/Services/DatasetService.Mutations.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ public sealed partial class DatasetService
1111
// ── Dataset Creation ────────────────────────────────────────────────
1212

1313
public Task<DatasetInfo> CreateEmptyDatasetAsync(
14-
string name, IReadOnlyList<DatasetColumnDef> columns, CancellationToken ct)
14+
string name, IReadOnlyList<DatasetColumnDef> columns, CancellationToken ct, string? category = null)
1515
{
16-
var request = new { name, columns };
16+
var request = new { name, columns, category };
1717
var ptr = DatasetNative.CreateEmpty(JsonSerializer.Serialize(request, JsonOptions));
1818
try
1919
{
@@ -58,9 +58,9 @@ public Task<DatasetInfo> DuplicateDatasetAsync(
5858
}
5959
}
6060

61-
public Task<DatasetInfo> ImportFromContentAsync(string content, string name, CancellationToken ct)
61+
public Task<DatasetInfo> ImportFromContentAsync(string content, string name, CancellationToken ct, string? category = null)
6262
{
63-
var request = new { content, name };
63+
var request = new { content, name, category };
6464
var ptr = DatasetNative.ImportContent(JsonSerializer.Serialize(request, JsonOptions));
6565
try
6666
{

0 commit comments

Comments
 (0)