cf-workspace-store 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cf_workspace_store-0.1.0/CMakeLists.txt +6 -0
- cf_workspace_store-0.1.0/PKG-INFO +25 -0
- cf_workspace_store-0.1.0/README.md +13 -0
- cf_workspace_store-0.1.0/cf-package.yaml +40 -0
- cf_workspace_store-0.1.0/pyproject.toml +39 -0
- cf_workspace_store-0.1.0/src/cf_workspace_store/__init__.py +95 -0
- cf_workspace_store-0.1.0/src/cf_workspace_store/native/CMakeLists.txt +194 -0
- cf_workspace_store-0.1.0/src/cf_workspace_store/native/workspace_store_contract_provider.cpp +309 -0
- cf_workspace_store-0.1.0/src/cf_workspace_store/paths.py +61 -0
- cf_workspace_store-0.1.0/src/cf_workspace_store/provider_descriptor.v1.json +18 -0
- cf_workspace_store-0.1.0/src/cf_workspace_store/service_contract_provider.py +43 -0
- cf_workspace_store-0.1.0/src/cf_workspace_store/store.py +142 -0
- cf_workspace_store-0.1.0/tests/test_install_surface.py +15 -0
- cf_workspace_store-0.1.0/tests/test_store.py +46 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: cf-workspace-store
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Embedded workspace store provider for Cogniflow
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: duckdb<2.0,>=0.10
|
|
7
|
+
Requires-Dist: cf-package-contracts>=0.1.0
|
|
8
|
+
Requires-Dist: cf-service-contracts>=0.1.0
|
|
9
|
+
Provides-Extra: test
|
|
10
|
+
Requires-Dist: pytest<9.0,>=8.0; extra == "test"
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# cf_workspace_store
|
|
14
|
+
|
|
15
|
+
`cf_workspace_store` owns the embedded workspace-store infrastructure surface for Cogniflow.
|
|
16
|
+
|
|
17
|
+
It provides:
|
|
18
|
+
|
|
19
|
+
- a Python API for opening the runtime-state named store plus ephemeral sessions
|
|
20
|
+
- a native `CfWorkspaceStoreContractV1` provider for other native packages
|
|
21
|
+
|
|
22
|
+
DuckDB is the first backend implementation, but DuckDB-specific handles and types stay internal to this package.
|
|
23
|
+
|
|
24
|
+
`pipeline_states` is the only public named store surface.
|
|
25
|
+
Productive RDF semantics should use `ProviderKey.SEMANTICS_STORE`; old RDF DuckDB files are cleanup-only internals.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# cf_workspace_store
|
|
2
|
+
|
|
3
|
+
`cf_workspace_store` owns the embedded workspace-store infrastructure surface for Cogniflow.
|
|
4
|
+
|
|
5
|
+
It provides:
|
|
6
|
+
|
|
7
|
+
- a Python API for opening the runtime-state named store plus ephemeral sessions
|
|
8
|
+
- a native `CfWorkspaceStoreContractV1` provider for other native packages
|
|
9
|
+
|
|
10
|
+
DuckDB is the first backend implementation, but DuckDB-specific handles and types stay internal to this package.
|
|
11
|
+
|
|
12
|
+
`pipeline_states` is the only public named store surface.
|
|
13
|
+
Productive RDF semantics should use `ProviderKey.SEMANTICS_STORE`; old RDF DuckDB files are cleanup-only internals.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
schema_version: 1
|
|
2
|
+
package_id: cf-workspace-store
|
|
3
|
+
defaults:
|
|
4
|
+
owner: cf-team
|
|
5
|
+
last_reviewed: '2026-03-30'
|
|
6
|
+
files:
|
|
7
|
+
entries:
|
|
8
|
+
- path: CMakeLists.txt
|
|
9
|
+
status: current
|
|
10
|
+
purpose: build
|
|
11
|
+
- path: README.md
|
|
12
|
+
status: current
|
|
13
|
+
purpose: docs
|
|
14
|
+
- path: pyproject.toml
|
|
15
|
+
status: current
|
|
16
|
+
purpose: build
|
|
17
|
+
- path: src/cf_workspace_store/__init__.py
|
|
18
|
+
status: current
|
|
19
|
+
purpose: source
|
|
20
|
+
- path: src/cf_workspace_store/paths.py
|
|
21
|
+
status: current
|
|
22
|
+
purpose: source
|
|
23
|
+
- path: src/cf_workspace_store/store.py
|
|
24
|
+
status: current
|
|
25
|
+
purpose: source
|
|
26
|
+
- path: src/cf_workspace_store/provider_descriptor.v1.json
|
|
27
|
+
status: current
|
|
28
|
+
purpose: runtime
|
|
29
|
+
- path: src/cf_workspace_store/native/CMakeLists.txt
|
|
30
|
+
status: current
|
|
31
|
+
purpose: build
|
|
32
|
+
- path: src/cf_workspace_store/native/workspace_store_contract_provider.cpp
|
|
33
|
+
status: current
|
|
34
|
+
purpose: source
|
|
35
|
+
- path: tests/test_install_surface.py
|
|
36
|
+
status: current
|
|
37
|
+
purpose: test
|
|
38
|
+
- path: tests/test_store.py
|
|
39
|
+
status: current
|
|
40
|
+
purpose: test
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [
|
|
3
|
+
"scikit-build-core>=0.9",
|
|
4
|
+
"cf-package-contracts>=0.1.0",
|
|
5
|
+
]
|
|
6
|
+
build-backend = "scikit_build_core.build"
|
|
7
|
+
|
|
8
|
+
[project]
|
|
9
|
+
name = "cf-workspace-store"
|
|
10
|
+
version = "0.1.0"
|
|
11
|
+
description = "Embedded workspace store provider for Cogniflow"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.11"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"duckdb>=0.10,<2.0",
|
|
16
|
+
"cf-package-contracts>=0.1.0",
|
|
17
|
+
"cf-service-contracts>=0.1.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.entry-points."cogniflow.service_providers"]
|
|
21
|
+
workspace_store = "cf_workspace_store.service_contract_provider:provide_workspace_store_contract"
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
test = [
|
|
25
|
+
"pytest>=8.0,<9.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.scikit-build]
|
|
29
|
+
wheel.packages = ["src/cf_workspace_store"]
|
|
30
|
+
build-dir = "../../.native_deps/scikit-build/cf-workspace-store"
|
|
31
|
+
|
|
32
|
+
[tool.scikit-build.sdist]
|
|
33
|
+
include = [
|
|
34
|
+
"CMakeLists.txt",
|
|
35
|
+
"README.md",
|
|
36
|
+
"cf-package.yaml",
|
|
37
|
+
"src/cf_workspace_store/**",
|
|
38
|
+
"tests/**"
|
|
39
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from importlib import metadata
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .paths import (
|
|
7
|
+
DEFAULT_SEMANTICS_DB_FILES,
|
|
8
|
+
WorkspaceStoreKind,
|
|
9
|
+
WorkspaceStorePaths,
|
|
10
|
+
find_repo_root,
|
|
11
|
+
resolve_named_store_path,
|
|
12
|
+
resolve_semantics_dir,
|
|
13
|
+
resolve_workspace_dir,
|
|
14
|
+
resolve_workspace_store_paths,
|
|
15
|
+
)
|
|
16
|
+
from .store import (
|
|
17
|
+
ConstraintException,
|
|
18
|
+
WorkspaceQueryResult,
|
|
19
|
+
WorkspaceStore,
|
|
20
|
+
WorkspaceStoreSession,
|
|
21
|
+
open_ephemeral,
|
|
22
|
+
open_named,
|
|
23
|
+
open_path,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _locate_distribution_file(*relative_candidates: str) -> Path:
|
|
30
|
+
distribution = metadata.distribution("cf-workspace-store")
|
|
31
|
+
files = distribution.files or []
|
|
32
|
+
for candidate in relative_candidates:
|
|
33
|
+
for entry in files:
|
|
34
|
+
if entry.as_posix() == candidate:
|
|
35
|
+
return Path(distribution.locate_file(entry)).resolve()
|
|
36
|
+
raise FileNotFoundError(
|
|
37
|
+
f"Could not locate any of {relative_candidates!r} in cf-workspace-store distribution files."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _native_root() -> Path:
|
|
42
|
+
try:
|
|
43
|
+
return _locate_distribution_file(
|
|
44
|
+
"cf_workspace_store/native/bin/cf_workspace_store_provider.dll",
|
|
45
|
+
"cf_workspace_store/native/bin/libcf_workspace_store_provider.dll",
|
|
46
|
+
"cf_workspace_store/native/lib/libcf_workspace_store_provider.so",
|
|
47
|
+
"cf_workspace_store/native/lib/libcf_workspace_store_provider.dylib",
|
|
48
|
+
).parents[1]
|
|
49
|
+
except Exception:
|
|
50
|
+
return Path(__file__).resolve().parent / "native"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def cf_workspace_store_provider_path() -> str:
|
|
54
|
+
native_root = _native_root()
|
|
55
|
+
candidates = [
|
|
56
|
+
native_root / "bin" / "cf_workspace_store_provider.dll",
|
|
57
|
+
native_root / "bin" / "libcf_workspace_store_provider.dll",
|
|
58
|
+
native_root / "lib" / "libcf_workspace_store_provider.so",
|
|
59
|
+
native_root / "lib" / "libcf_workspace_store_provider.dylib",
|
|
60
|
+
native_root / "lib" / "cf_workspace_store_provider.dll",
|
|
61
|
+
native_root / "lib" / "libcf_workspace_store_provider.dll",
|
|
62
|
+
]
|
|
63
|
+
for candidate in candidates:
|
|
64
|
+
if candidate.is_file():
|
|
65
|
+
return str(candidate)
|
|
66
|
+
raise FileNotFoundError("Packaged cf_workspace_store provider library not found.")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def cf_workspace_store_provider_descriptor_path() -> str:
|
|
70
|
+
try:
|
|
71
|
+
return str(_locate_distribution_file("cf_workspace_store/provider_descriptor.v1.json"))
|
|
72
|
+
except Exception:
|
|
73
|
+
return str(Path(__file__).resolve().parent / "provider_descriptor.v1.json")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
__all__ = [
|
|
77
|
+
"__version__",
|
|
78
|
+
"DEFAULT_SEMANTICS_DB_FILES",
|
|
79
|
+
"WorkspaceQueryResult",
|
|
80
|
+
"WorkspaceStore",
|
|
81
|
+
"WorkspaceStoreKind",
|
|
82
|
+
"WorkspaceStorePaths",
|
|
83
|
+
"WorkspaceStoreSession",
|
|
84
|
+
"ConstraintException",
|
|
85
|
+
"cf_workspace_store_provider_descriptor_path",
|
|
86
|
+
"cf_workspace_store_provider_path",
|
|
87
|
+
"find_repo_root",
|
|
88
|
+
"open_ephemeral",
|
|
89
|
+
"open_named",
|
|
90
|
+
"open_path",
|
|
91
|
+
"resolve_named_store_path",
|
|
92
|
+
"resolve_semantics_dir",
|
|
93
|
+
"resolve_workspace_dir",
|
|
94
|
+
"resolve_workspace_store_paths",
|
|
95
|
+
]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.20)
|
|
2
|
+
project(cf_workspace_store_provider LANGUAGES CXX)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 17)
|
|
5
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
6
|
+
|
|
7
|
+
include(GNUInstallDirs)
|
|
8
|
+
|
|
9
|
+
function(_cf_workspace_store_first_existing_path out_var)
|
|
10
|
+
foreach(_candidate IN LISTS ARGN)
|
|
11
|
+
if (NOT _candidate STREQUAL "" AND EXISTS "${_candidate}")
|
|
12
|
+
set(${out_var} "${_candidate}" PARENT_SCOPE)
|
|
13
|
+
return()
|
|
14
|
+
endif()
|
|
15
|
+
endforeach()
|
|
16
|
+
set(${out_var} "" PARENT_SCOPE)
|
|
17
|
+
endfunction()
|
|
18
|
+
|
|
19
|
+
function(_cf_workspace_store_find_repo_native_duckdb_root out_var)
|
|
20
|
+
set(_search_roots
|
|
21
|
+
"${CMAKE_SOURCE_DIR}"
|
|
22
|
+
"${PROJECT_SOURCE_DIR}"
|
|
23
|
+
"${CMAKE_CURRENT_SOURCE_DIR}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
foreach(_search_root IN LISTS _search_roots)
|
|
27
|
+
if (_search_root STREQUAL "" OR NOT IS_DIRECTORY "${_search_root}")
|
|
28
|
+
continue()
|
|
29
|
+
endif()
|
|
30
|
+
|
|
31
|
+
set(_cursor "${_search_root}")
|
|
32
|
+
while (TRUE)
|
|
33
|
+
set(_candidate "${_cursor}/.native_deps/duckdb")
|
|
34
|
+
if (EXISTS "${_candidate}")
|
|
35
|
+
set(${out_var} "${_candidate}" PARENT_SCOPE)
|
|
36
|
+
return()
|
|
37
|
+
endif()
|
|
38
|
+
|
|
39
|
+
get_filename_component(_parent "${_cursor}" DIRECTORY)
|
|
40
|
+
if (_parent STREQUAL "${_cursor}")
|
|
41
|
+
break()
|
|
42
|
+
endif()
|
|
43
|
+
set(_cursor "${_parent}")
|
|
44
|
+
endwhile()
|
|
45
|
+
endforeach()
|
|
46
|
+
|
|
47
|
+
set(${out_var} "" PARENT_SCOPE)
|
|
48
|
+
endfunction()
|
|
49
|
+
|
|
50
|
+
function(_cf_workspace_store_find_cache_native_duckdb_root out_var)
|
|
51
|
+
set(_version_candidates "")
|
|
52
|
+
if (NOT "$ENV{DUCKDB_VERSION}" STREQUAL "")
|
|
53
|
+
list(APPEND _version_candidates "$ENV{DUCKDB_VERSION}")
|
|
54
|
+
endif()
|
|
55
|
+
list(APPEND _version_candidates "1.4.2")
|
|
56
|
+
list(REMOVE_DUPLICATES _version_candidates)
|
|
57
|
+
|
|
58
|
+
set(_triplet_candidates
|
|
59
|
+
"x64-windows-static"
|
|
60
|
+
"x64-windows"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
set(_cache_roots
|
|
64
|
+
"$ENV{CF_NATIVE_CACHE_DIR}/duckdb"
|
|
65
|
+
"$ENV{USERPROFILE}/.cogniflow/cache/native/duckdb"
|
|
66
|
+
"$ENV{HOME}/.cogniflow/cache/native/duckdb"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
foreach(_cache_root IN LISTS _cache_roots)
|
|
70
|
+
if (_cache_root STREQUAL "" OR NOT IS_DIRECTORY "${_cache_root}")
|
|
71
|
+
continue()
|
|
72
|
+
endif()
|
|
73
|
+
|
|
74
|
+
foreach(_version IN LISTS _version_candidates)
|
|
75
|
+
foreach(_triplet IN LISTS _triplet_candidates)
|
|
76
|
+
set(_candidate "${_cache_root}/${_version}/${_triplet}")
|
|
77
|
+
if (EXISTS "${_candidate}")
|
|
78
|
+
set(${out_var} "${_candidate}" PARENT_SCOPE)
|
|
79
|
+
return()
|
|
80
|
+
endif()
|
|
81
|
+
endforeach()
|
|
82
|
+
endforeach()
|
|
83
|
+
|
|
84
|
+
file(GLOB _cache_version_dirs LIST_DIRECTORIES true "${_cache_root}/*")
|
|
85
|
+
list(SORT _cache_version_dirs COMPARE NATURAL ORDER DESCENDING)
|
|
86
|
+
foreach(_version_dir IN LISTS _cache_version_dirs)
|
|
87
|
+
if (NOT IS_DIRECTORY "${_version_dir}")
|
|
88
|
+
continue()
|
|
89
|
+
endif()
|
|
90
|
+
foreach(_triplet IN LISTS _triplet_candidates)
|
|
91
|
+
set(_candidate "${_version_dir}/${_triplet}")
|
|
92
|
+
if (EXISTS "${_candidate}")
|
|
93
|
+
set(${out_var} "${_candidate}" PARENT_SCOPE)
|
|
94
|
+
return()
|
|
95
|
+
endif()
|
|
96
|
+
endforeach()
|
|
97
|
+
endforeach()
|
|
98
|
+
endforeach()
|
|
99
|
+
|
|
100
|
+
set(${out_var} "" PARENT_SCOPE)
|
|
101
|
+
endfunction()
|
|
102
|
+
|
|
103
|
+
get_filename_component(_CF_REPO_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../../.." ABSOLUTE)
|
|
104
|
+
include("${_CF_REPO_ROOT}/tools/cmake/cf_contracts_include.cmake")
|
|
105
|
+
set(CF_SDK_INCLUDE "${CF_CONTRACTS_INCLUDE}")
|
|
106
|
+
|
|
107
|
+
set(_native_duckdb_root "$ENV{CF_WORKSPACE_STORE_DUCKDB_ROOT}")
|
|
108
|
+
if (_native_duckdb_root STREQUAL "")
|
|
109
|
+
_cf_workspace_store_find_cache_native_duckdb_root(_native_duckdb_root)
|
|
110
|
+
endif()
|
|
111
|
+
if (_native_duckdb_root STREQUAL "")
|
|
112
|
+
_cf_workspace_store_find_repo_native_duckdb_root(_native_duckdb_root)
|
|
113
|
+
endif()
|
|
114
|
+
if (_native_duckdb_root STREQUAL "")
|
|
115
|
+
set(_native_duckdb_root "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../.native_deps/duckdb")
|
|
116
|
+
endif()
|
|
117
|
+
file(TO_CMAKE_PATH "${_native_duckdb_root}" _native_duckdb_root)
|
|
118
|
+
|
|
119
|
+
set(_native_duckdb_include "${_native_duckdb_root}/include")
|
|
120
|
+
set(_native_duckdb_include_flat "${_native_duckdb_root}")
|
|
121
|
+
set(_native_duckdb_src "${_native_duckdb_root}/src")
|
|
122
|
+
set(_native_duckdb_lib_win "${_native_duckdb_root}/lib/duckdb.lib")
|
|
123
|
+
set(_native_duckdb_lib_mingw "${_native_duckdb_root}/lib/libduckdb.a")
|
|
124
|
+
set(_native_duckdb_lib_flat "${_native_duckdb_root}/duckdb.lib")
|
|
125
|
+
set(_native_duckdb_src_cpp "${_native_duckdb_src}/duckdb.cpp")
|
|
126
|
+
set(_native_duckdb_runtime_dll "${_native_duckdb_root}/bin/duckdb.dll")
|
|
127
|
+
|
|
128
|
+
set(_duckdb_include_candidates
|
|
129
|
+
"$ENV{CF_WORKSPACE_STORE_DUCKDB_INCLUDE}"
|
|
130
|
+
"$ENV{CF_DATAHIVE_CPP_DUCKDB_INCLUDE}"
|
|
131
|
+
"${_native_duckdb_src}"
|
|
132
|
+
"${_native_duckdb_include}"
|
|
133
|
+
"${_native_duckdb_include_flat}"
|
|
134
|
+
)
|
|
135
|
+
_cf_workspace_store_first_existing_path(
|
|
136
|
+
CF_WORKSPACE_STORE_DUCKDB_INCLUDE_DIR
|
|
137
|
+
${_duckdb_include_candidates}
|
|
138
|
+
)
|
|
139
|
+
if (NOT CF_WORKSPACE_STORE_DUCKDB_INCLUDE_DIR STREQUAL "")
|
|
140
|
+
file(TO_CMAKE_PATH "${CF_WORKSPACE_STORE_DUCKDB_INCLUDE_DIR}" CF_WORKSPACE_STORE_DUCKDB_INCLUDE_DIR)
|
|
141
|
+
endif()
|
|
142
|
+
|
|
143
|
+
set(_duckdb_library_candidates
|
|
144
|
+
"$ENV{CF_WORKSPACE_STORE_DUCKDB_LIBRARY}"
|
|
145
|
+
"$ENV{CF_WORKSPACE_STORE_DUCKDB_LIB}"
|
|
146
|
+
"$ENV{CF_DATAHIVE_CPP_DUCKDB_LIB}"
|
|
147
|
+
"${_native_duckdb_lib_win}"
|
|
148
|
+
"${_native_duckdb_lib_mingw}"
|
|
149
|
+
"${_native_duckdb_lib_flat}"
|
|
150
|
+
)
|
|
151
|
+
_cf_workspace_store_first_existing_path(
|
|
152
|
+
CF_WORKSPACE_STORE_DUCKDB_LIBRARY
|
|
153
|
+
${_duckdb_library_candidates}
|
|
154
|
+
)
|
|
155
|
+
if (NOT CF_WORKSPACE_STORE_DUCKDB_LIBRARY STREQUAL "")
|
|
156
|
+
file(TO_CMAKE_PATH "${CF_WORKSPACE_STORE_DUCKDB_LIBRARY}" CF_WORKSPACE_STORE_DUCKDB_LIBRARY)
|
|
157
|
+
endif()
|
|
158
|
+
|
|
159
|
+
if (NOT _native_duckdb_runtime_dll STREQUAL "")
|
|
160
|
+
file(TO_CMAKE_PATH "${_native_duckdb_runtime_dll}" _native_duckdb_runtime_dll)
|
|
161
|
+
endif()
|
|
162
|
+
|
|
163
|
+
if (CF_WORKSPACE_STORE_DUCKDB_INCLUDE_DIR STREQUAL "" OR NOT EXISTS "${CF_WORKSPACE_STORE_DUCKDB_INCLUDE_DIR}/duckdb.h")
|
|
164
|
+
message(FATAL_ERROR "DuckDB headers not found. Resolved root='${_native_duckdb_root}', include='${CF_WORKSPACE_STORE_DUCKDB_INCLUDE_DIR}'.")
|
|
165
|
+
endif()
|
|
166
|
+
|
|
167
|
+
if (CF_WORKSPACE_STORE_DUCKDB_LIBRARY STREQUAL "" OR NOT EXISTS "${CF_WORKSPACE_STORE_DUCKDB_LIBRARY}")
|
|
168
|
+
message(FATAL_ERROR "DuckDB library not found. Resolved root='${_native_duckdb_root}', library='${CF_WORKSPACE_STORE_DUCKDB_LIBRARY}'.")
|
|
169
|
+
endif()
|
|
170
|
+
|
|
171
|
+
add_library(cf_workspace_store_provider SHARED
|
|
172
|
+
workspace_store_contract_provider.cpp
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
target_compile_definitions(cf_workspace_store_provider PRIVATE CF_STEP_ABI_EXPORTS)
|
|
176
|
+
target_include_directories(cf_workspace_store_provider PRIVATE
|
|
177
|
+
${CF_SDK_INCLUDE}
|
|
178
|
+
${CF_WORKSPACE_STORE_DUCKDB_INCLUDE_DIR}
|
|
179
|
+
)
|
|
180
|
+
target_link_libraries(cf_workspace_store_provider PRIVATE
|
|
181
|
+
${CF_WORKSPACE_STORE_DUCKDB_LIBRARY}
|
|
182
|
+
)
|
|
183
|
+
set_target_properties(cf_workspace_store_provider PROPERTIES
|
|
184
|
+
PREFIX ""
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
install(TARGETS cf_workspace_store_provider
|
|
188
|
+
RUNTIME DESTINATION cf_workspace_store/native/bin
|
|
189
|
+
LIBRARY DESTINATION cf_workspace_store/native/lib
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if (WIN32 AND EXISTS "${_native_duckdb_runtime_dll}")
|
|
193
|
+
install(FILES ${_native_duckdb_runtime_dll} DESTINATION cf_workspace_store/native/bin)
|
|
194
|
+
endif()
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#include "cf_step_abi.h"
|
|
2
|
+
|
|
3
|
+
#include <cstdint>
|
|
4
|
+
#include <cstdlib>
|
|
5
|
+
#include <cstring>
|
|
6
|
+
#include <filesystem>
|
|
7
|
+
#include <memory>
|
|
8
|
+
#include <optional>
|
|
9
|
+
#include <random>
|
|
10
|
+
#include <string>
|
|
11
|
+
|
|
12
|
+
#include "duckdb.h"
|
|
13
|
+
|
|
14
|
+
namespace {
|
|
15
|
+
|
|
16
|
+
struct StoreHandle {
|
|
17
|
+
duckdb_database db = nullptr;
|
|
18
|
+
duckdb_connection conn = nullptr;
|
|
19
|
+
|
|
20
|
+
~StoreHandle() {
|
|
21
|
+
if (conn != nullptr) {
|
|
22
|
+
duckdb_disconnect(&conn);
|
|
23
|
+
}
|
|
24
|
+
if (db != nullptr) {
|
|
25
|
+
duckdb_close(&db);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
std::string copy_text(const char* value, size_t size) {
|
|
31
|
+
if (value == nullptr || size == 0) {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
return std::string(value, size);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
CfStatusCode alloc_text(CfExecutionContext* ctx, const std::string& text, const char** out_text, size_t* out_size) {
|
|
38
|
+
if (out_text == nullptr || out_size == nullptr || ctx == nullptr || ctx->allocator == nullptr || ctx->allocator->alloc == nullptr) {
|
|
39
|
+
return CF_STATUS_ERROR;
|
|
40
|
+
}
|
|
41
|
+
*out_size = text.size();
|
|
42
|
+
if (text.empty()) {
|
|
43
|
+
*out_text = nullptr;
|
|
44
|
+
return CF_STATUS_OK;
|
|
45
|
+
}
|
|
46
|
+
char* buffer = static_cast<char*>(ctx->allocator->alloc(text.size() + 1u, ctx->allocator->user_data));
|
|
47
|
+
if (buffer == nullptr) {
|
|
48
|
+
return CF_STATUS_ERROR;
|
|
49
|
+
}
|
|
50
|
+
std::memcpy(buffer, text.data(), text.size());
|
|
51
|
+
buffer[text.size()] = '\0';
|
|
52
|
+
*out_text = buffer;
|
|
53
|
+
return CF_STATUS_OK;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
std::filesystem::path resolve_named_path(const CfWorkspaceStoreOpenRequestV1& request) {
|
|
57
|
+
std::filesystem::path semantics_dir;
|
|
58
|
+
const std::string explicit_semantics_dir = copy_text(request.semantics_dir, request.semantics_dir_size);
|
|
59
|
+
if (!explicit_semantics_dir.empty()) {
|
|
60
|
+
semantics_dir = explicit_semantics_dir;
|
|
61
|
+
} else {
|
|
62
|
+
std::filesystem::path workspace_root;
|
|
63
|
+
const std::string explicit_workspace_root = copy_text(request.workspace_root, request.workspace_root_size);
|
|
64
|
+
if (!explicit_workspace_root.empty()) {
|
|
65
|
+
workspace_root = explicit_workspace_root;
|
|
66
|
+
} else {
|
|
67
|
+
workspace_root = std::filesystem::current_path() / "workspace";
|
|
68
|
+
}
|
|
69
|
+
semantics_dir = workspace_root / "semantics";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const std::string kind = copy_text(request.store_kind, request.store_kind_size);
|
|
73
|
+
if (kind != "pipeline_states") {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
return semantics_dir / "cf-pipeline-states.store.db";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
std::string escape_sql_literal(const std::string& value) {
|
|
80
|
+
std::string escaped;
|
|
81
|
+
escaped.reserve(value.size() + 8u);
|
|
82
|
+
for (char ch : value) {
|
|
83
|
+
escaped.push_back(ch);
|
|
84
|
+
if (ch == '\'') {
|
|
85
|
+
escaped.push_back('\'');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return escaped;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
CfStatusCode write_error(CfExecutionContext* ctx, const std::string& text, const char** out_error, size_t* out_error_size) {
|
|
92
|
+
if (out_error == nullptr || out_error_size == nullptr) {
|
|
93
|
+
return CF_STATUS_ERROR;
|
|
94
|
+
}
|
|
95
|
+
return alloc_text(ctx, text, out_error, out_error_size);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
StoreHandle* unwrap(void* raw_handle) {
|
|
99
|
+
return static_cast<StoreHandle*>(raw_handle);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
CfStatusCode open_store_at_path(
|
|
103
|
+
CfExecutionContext* ctx,
|
|
104
|
+
const std::filesystem::path& path,
|
|
105
|
+
int read_only,
|
|
106
|
+
void** out_store,
|
|
107
|
+
const char** out_error,
|
|
108
|
+
size_t* out_error_size) {
|
|
109
|
+
if (out_store == nullptr) {
|
|
110
|
+
return CF_STATUS_INVALID;
|
|
111
|
+
}
|
|
112
|
+
*out_store = nullptr;
|
|
113
|
+
if (!read_only) {
|
|
114
|
+
std::error_code ec;
|
|
115
|
+
std::filesystem::create_directories(path.parent_path(), ec);
|
|
116
|
+
if (ec) {
|
|
117
|
+
write_error(ctx, "failed to create workspace store directory", out_error, out_error_size);
|
|
118
|
+
return CF_STATUS_ERROR;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
std::unique_ptr<StoreHandle> handle(new StoreHandle());
|
|
123
|
+
duckdb_config config = nullptr;
|
|
124
|
+
if (duckdb_create_config(&config) != DuckDBSuccess) {
|
|
125
|
+
write_error(ctx, "duckdb_create_config failed for workspace store", out_error, out_error_size);
|
|
126
|
+
return CF_STATUS_ERROR;
|
|
127
|
+
}
|
|
128
|
+
if (read_only) {
|
|
129
|
+
duckdb_set_config(config, "access_mode", "READ_ONLY");
|
|
130
|
+
}
|
|
131
|
+
if (duckdb_open_ext(path.string().c_str(), &handle->db, config, nullptr) != DuckDBSuccess) {
|
|
132
|
+
duckdb_destroy_config(&config);
|
|
133
|
+
write_error(ctx, "duckdb_open failed for workspace store", out_error, out_error_size);
|
|
134
|
+
return CF_STATUS_ERROR;
|
|
135
|
+
}
|
|
136
|
+
duckdb_destroy_config(&config);
|
|
137
|
+
if (duckdb_connect(handle->db, &handle->conn) != DuckDBSuccess) {
|
|
138
|
+
write_error(ctx, "duckdb_connect failed for workspace store", out_error, out_error_size);
|
|
139
|
+
return CF_STATUS_ERROR;
|
|
140
|
+
}
|
|
141
|
+
if (read_only) {
|
|
142
|
+
duckdb_result result {};
|
|
143
|
+
duckdb_query(handle->conn, "PRAGMA disable_checkpoint_on_shutdown", &result);
|
|
144
|
+
duckdb_destroy_result(&result);
|
|
145
|
+
}
|
|
146
|
+
*out_store = handle.release();
|
|
147
|
+
return CF_STATUS_OK;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
CfStatusCode open_named_store(
|
|
151
|
+
CfExecutionContext* ctx,
|
|
152
|
+
const CfWorkspaceStoreOpenRequestV1* request,
|
|
153
|
+
void** out_store,
|
|
154
|
+
const char** out_error,
|
|
155
|
+
size_t* out_error_size) {
|
|
156
|
+
if (request == nullptr || request->version != 1u) {
|
|
157
|
+
return CF_STATUS_INVALID;
|
|
158
|
+
}
|
|
159
|
+
const std::string kind = copy_text(request->store_kind, request->store_kind_size);
|
|
160
|
+
if (kind != "pipeline_states") {
|
|
161
|
+
write_error(ctx, "unsupported named workspace store kind", out_error, out_error_size);
|
|
162
|
+
return CF_STATUS_INVALID;
|
|
163
|
+
}
|
|
164
|
+
return open_store_at_path(ctx, resolve_named_path(*request), request->read_only, out_store, out_error, out_error_size);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
CfStatusCode open_ephemeral_store(
|
|
168
|
+
CfExecutionContext* ctx,
|
|
169
|
+
const CfWorkspaceStoreOpenRequestV1* request,
|
|
170
|
+
void** out_store,
|
|
171
|
+
const char** out_error,
|
|
172
|
+
size_t* out_error_size) {
|
|
173
|
+
std::mt19937_64 rng(std::random_device{}());
|
|
174
|
+
std::uniform_int_distribution<uint64_t> dist;
|
|
175
|
+
const auto temp_dir =
|
|
176
|
+
std::filesystem::temp_directory_path() /
|
|
177
|
+
std::filesystem::path("cf_workspace_store_ephemeral_" + std::to_string(dist(rng)) + ".db");
|
|
178
|
+
const int read_only = request != nullptr ? request->read_only : 0;
|
|
179
|
+
return open_store_at_path(ctx, temp_dir, read_only, out_store, out_error, out_error_size);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
CfStatusCode close_store(CfExecutionContext*, void* store_handle) {
|
|
183
|
+
delete unwrap(store_handle);
|
|
184
|
+
return CF_STATUS_OK;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
CfStatusCode exec_sql(
|
|
188
|
+
CfExecutionContext* ctx,
|
|
189
|
+
const CfWorkspaceStoreExecRequestV1* request,
|
|
190
|
+
const char** out_error,
|
|
191
|
+
size_t* out_error_size) {
|
|
192
|
+
if (request == nullptr || request->version != 1u || request->store_handle == nullptr) {
|
|
193
|
+
return CF_STATUS_INVALID;
|
|
194
|
+
}
|
|
195
|
+
duckdb_result result {};
|
|
196
|
+
const std::string sql = copy_text(request->sql, request->sql_size);
|
|
197
|
+
if (duckdb_query(unwrap(request->store_handle)->conn, sql.c_str(), &result) != DuckDBSuccess) {
|
|
198
|
+
const char* detail = duckdb_result_error(&result);
|
|
199
|
+
std::string error = "workspace store query failed";
|
|
200
|
+
if (detail != nullptr && detail[0] != '\0') {
|
|
201
|
+
error += ": ";
|
|
202
|
+
error += detail;
|
|
203
|
+
}
|
|
204
|
+
duckdb_destroy_result(&result);
|
|
205
|
+
write_error(ctx, error, out_error, out_error_size);
|
|
206
|
+
return CF_STATUS_ERROR;
|
|
207
|
+
}
|
|
208
|
+
duckdb_destroy_result(&result);
|
|
209
|
+
return CF_STATUS_OK;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
CfStatusCode query_scalar_text(
|
|
213
|
+
CfExecutionContext* ctx,
|
|
214
|
+
const CfWorkspaceStoreScalarTextRequestV1* request,
|
|
215
|
+
const char** out_text,
|
|
216
|
+
size_t* out_size,
|
|
217
|
+
const char** out_error,
|
|
218
|
+
size_t* out_error_size) {
|
|
219
|
+
if (request == nullptr || request->version != 1u || request->store_handle == nullptr) {
|
|
220
|
+
return CF_STATUS_INVALID;
|
|
221
|
+
}
|
|
222
|
+
duckdb_result result {};
|
|
223
|
+
const std::string sql = copy_text(request->sql, request->sql_size);
|
|
224
|
+
if (duckdb_query(unwrap(request->store_handle)->conn, sql.c_str(), &result) != DuckDBSuccess) {
|
|
225
|
+
const char* detail = duckdb_result_error(&result);
|
|
226
|
+
std::string error = "workspace store scalar text query failed";
|
|
227
|
+
if (detail != nullptr && detail[0] != '\0') {
|
|
228
|
+
error += ": ";
|
|
229
|
+
error += detail;
|
|
230
|
+
}
|
|
231
|
+
duckdb_destroy_result(&result);
|
|
232
|
+
write_error(ctx, error, out_error, out_error_size);
|
|
233
|
+
return CF_STATUS_ERROR;
|
|
234
|
+
}
|
|
235
|
+
if (duckdb_row_count(&result) == 0 || duckdb_column_count(&result) == 0) {
|
|
236
|
+
duckdb_destroy_result(&result);
|
|
237
|
+
return alloc_text(ctx, "", out_text, out_size);
|
|
238
|
+
}
|
|
239
|
+
const char* value = duckdb_value_varchar(&result, 0, 0);
|
|
240
|
+
const std::string text = value != nullptr ? value : "";
|
|
241
|
+
duckdb_free(const_cast<char*>(value));
|
|
242
|
+
duckdb_destroy_result(&result);
|
|
243
|
+
return alloc_text(ctx, text, out_text, out_size);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
CfStatusCode query_scalar_i64(
|
|
247
|
+
CfExecutionContext*,
|
|
248
|
+
const CfWorkspaceStoreScalarI64RequestV1* request,
|
|
249
|
+
int64_t* out_value,
|
|
250
|
+
const char**,
|
|
251
|
+
size_t*) {
|
|
252
|
+
if (request == nullptr || request->version != 1u || request->store_handle == nullptr || out_value == nullptr) {
|
|
253
|
+
return CF_STATUS_INVALID;
|
|
254
|
+
}
|
|
255
|
+
duckdb_result result {};
|
|
256
|
+
const std::string sql = copy_text(request->sql, request->sql_size);
|
|
257
|
+
if (duckdb_query(unwrap(request->store_handle)->conn, sql.c_str(), &result) != DuckDBSuccess) {
|
|
258
|
+
duckdb_destroy_result(&result);
|
|
259
|
+
return CF_STATUS_ERROR;
|
|
260
|
+
}
|
|
261
|
+
*out_value = (duckdb_row_count(&result) == 0 || duckdb_column_count(&result) == 0) ? 0 : duckdb_value_int64(&result, 0, 0);
|
|
262
|
+
duckdb_destroy_result(&result);
|
|
263
|
+
return CF_STATUS_OK;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
CfStatusCode export_query_to_parquet(
|
|
267
|
+
CfExecutionContext* ctx,
|
|
268
|
+
const CfWorkspaceStoreExportParquetRequestV1* request,
|
|
269
|
+
const char** out_error,
|
|
270
|
+
size_t* out_error_size) {
|
|
271
|
+
if (request == nullptr || request->version != 1u || request->store_handle == nullptr) {
|
|
272
|
+
return CF_STATUS_INVALID;
|
|
273
|
+
}
|
|
274
|
+
const std::string sql = copy_text(request->sql, request->sql_size);
|
|
275
|
+
const std::string output_path = copy_text(request->output_path, request->output_path_size);
|
|
276
|
+
const std::string command = "COPY (" + sql + ") TO '" + escape_sql_literal(output_path) + "' (FORMAT PARQUET)";
|
|
277
|
+
duckdb_result result {};
|
|
278
|
+
if (duckdb_query(unwrap(request->store_handle)->conn, command.c_str(), &result) != DuckDBSuccess) {
|
|
279
|
+
const char* detail = duckdb_result_error(&result);
|
|
280
|
+
std::string error = "workspace store parquet export failed";
|
|
281
|
+
if (detail != nullptr && detail[0] != '\0') {
|
|
282
|
+
error += ": ";
|
|
283
|
+
error += detail;
|
|
284
|
+
}
|
|
285
|
+
duckdb_destroy_result(&result);
|
|
286
|
+
write_error(ctx, error, out_error, out_error_size);
|
|
287
|
+
return CF_STATUS_ERROR;
|
|
288
|
+
}
|
|
289
|
+
duckdb_destroy_result(&result);
|
|
290
|
+
return CF_STATUS_OK;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const CfWorkspaceStoreContractV1 kWorkspaceStoreContractV1 = {
|
|
294
|
+
1u,
|
|
295
|
+
0u,
|
|
296
|
+
open_named_store,
|
|
297
|
+
open_ephemeral_store,
|
|
298
|
+
close_store,
|
|
299
|
+
exec_sql,
|
|
300
|
+
query_scalar_text,
|
|
301
|
+
query_scalar_i64,
|
|
302
|
+
export_query_to_parquet,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
} // namespace
|
|
306
|
+
|
|
307
|
+
extern "C" CF_EXPORT const CfWorkspaceStoreContractV1* cf_get_workspace_store_contract_v1() {
|
|
308
|
+
return &kWorkspaceStoreContractV1;
|
|
309
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Workspace-local embedded store path helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal, Optional
|
|
8
|
+
|
|
9
|
+
from cf_service_contracts.cogniflow_paths import resolve_workspace_dir
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
WorkspaceStoreKind = Literal["pipeline_states"]
|
|
13
|
+
|
|
14
|
+
DEFAULT_SEMANTICS_DB_FILES: dict[WorkspaceStoreKind, str] = {
|
|
15
|
+
"pipeline_states": "cf-pipeline-states.store.db",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class WorkspaceStorePaths:
|
|
21
|
+
pipeline_states: Path
|
|
22
|
+
|
|
23
|
+
def as_dict(self) -> dict[str, Path]:
|
|
24
|
+
return {
|
|
25
|
+
"pipeline_states": self.pipeline_states,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def find_repo_root(start: Optional[Path] = None) -> Optional[Path]:
|
|
30
|
+
probe = (start or Path.cwd()).resolve()
|
|
31
|
+
for candidate in (probe, *probe.parents):
|
|
32
|
+
if (candidate / ".git").exists():
|
|
33
|
+
return candidate
|
|
34
|
+
if (candidate / "sandcastle").is_dir() and (candidate / ".github").is_dir():
|
|
35
|
+
return candidate
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def resolve_semantics_dir() -> Path:
|
|
40
|
+
return (resolve_workspace_dir() / "semantics").resolve()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def resolve_named_store_path(
|
|
44
|
+
kind: WorkspaceStoreKind,
|
|
45
|
+
semantics_dir: Optional[Path] = None,
|
|
46
|
+
) -> Path:
|
|
47
|
+
if str(kind) != "pipeline_states":
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"Named workspace store kind '{kind}' is not supported. Only 'pipeline_states' remains public."
|
|
50
|
+
)
|
|
51
|
+
base = semantics_dir or resolve_semantics_dir()
|
|
52
|
+
return base / DEFAULT_SEMANTICS_DB_FILES[kind]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def resolve_workspace_store_paths(
|
|
56
|
+
semantics_dir: Optional[Path] = None,
|
|
57
|
+
) -> WorkspaceStorePaths:
|
|
58
|
+
base = semantics_dir or resolve_semantics_dir()
|
|
59
|
+
return WorkspaceStorePaths(
|
|
60
|
+
pipeline_states=base / DEFAULT_SEMANTICS_DB_FILES["pipeline_states"],
|
|
61
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": "provider_descriptor.v1",
|
|
3
|
+
"provider_key": "workspace_store",
|
|
4
|
+
"contract_symbol": "cf_get_workspace_store_contract_v1",
|
|
5
|
+
"contract_version": 1,
|
|
6
|
+
"library_candidates": [
|
|
7
|
+
"cf_workspace_store/native/bin/cf_workspace_store_provider.dll",
|
|
8
|
+
"cf_workspace_store/native/bin/libcf_workspace_store_provider.dll",
|
|
9
|
+
"cf_workspace_store/native/lib/cf_workspace_store_provider.dll",
|
|
10
|
+
"cf_workspace_store/native/lib/libcf_workspace_store_provider.dll",
|
|
11
|
+
"cf_workspace_store/native/lib/libcf_workspace_store_provider.so",
|
|
12
|
+
"cf_workspace_store/native/lib/libcf_workspace_store_provider.dylib"
|
|
13
|
+
],
|
|
14
|
+
"runtime_dir_candidates": [
|
|
15
|
+
"cf_workspace_store/native/bin",
|
|
16
|
+
"cf_workspace_store/native/lib"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .paths import resolve_named_store_path, resolve_semantics_dir, resolve_workspace_dir, resolve_workspace_store_paths
|
|
7
|
+
from .store import ConstraintException, open_named, open_path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WorkspaceStoreProvider:
|
|
11
|
+
def resolve_semantics_dir(self) -> Path:
|
|
12
|
+
return resolve_semantics_dir()
|
|
13
|
+
|
|
14
|
+
def resolve_workspace_dir(self) -> Path:
|
|
15
|
+
return resolve_workspace_dir()
|
|
16
|
+
|
|
17
|
+
def resolve_named_store_path(self, kind: str, *, semantics_dir: Any = None) -> Path:
|
|
18
|
+
"""Resolve the public named embedded store path.
|
|
19
|
+
|
|
20
|
+
Only `pipeline_states` remains a supported named store surface.
|
|
21
|
+
"""
|
|
22
|
+
return resolve_named_store_path(kind, semantics_dir=semantics_dir)
|
|
23
|
+
|
|
24
|
+
def resolve_workspace_store_paths(self, *, semantics_dir: Any = None):
|
|
25
|
+
return resolve_workspace_store_paths(semantics_dir=semantics_dir)
|
|
26
|
+
|
|
27
|
+
def open_named(self, kind: str, *, semantics_dir: Any = None, read_only: bool):
|
|
28
|
+
"""Open the supported named embedded store session.
|
|
29
|
+
|
|
30
|
+
Only `pipeline_states` remains a supported named store surface.
|
|
31
|
+
"""
|
|
32
|
+
return open_named(kind, semantics_dir=semantics_dir, read_only=read_only)
|
|
33
|
+
|
|
34
|
+
def open_path(self, path: Any, *, read_only: bool):
|
|
35
|
+
return open_path(path, read_only=read_only)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def constraint_exception(self) -> type[Exception]:
|
|
39
|
+
return ConstraintException
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def provide_workspace_store_contract() -> WorkspaceStoreProvider:
|
|
43
|
+
return WorkspaceStoreProvider()
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""DuckDB-backed embedded workspace store facade."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Iterable, Optional, Sequence
|
|
8
|
+
import tempfile
|
|
9
|
+
|
|
10
|
+
import duckdb
|
|
11
|
+
|
|
12
|
+
from .paths import WorkspaceStoreKind, resolve_named_store_path, resolve_semantics_dir
|
|
13
|
+
|
|
14
|
+
ConstraintException = duckdb.ConstraintException
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _normalize_params(params: Optional[Sequence[object]]) -> list[object]:
|
|
18
|
+
if params is None:
|
|
19
|
+
return []
|
|
20
|
+
return list(params)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class WorkspaceQueryResult:
|
|
25
|
+
_cursor: Any
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def description(self) -> Any:
|
|
29
|
+
return getattr(self._cursor, "description", None)
|
|
30
|
+
|
|
31
|
+
def fetchone(self) -> Any:
|
|
32
|
+
return self._cursor.fetchone()
|
|
33
|
+
|
|
34
|
+
def fetchall(self) -> list[Any]:
|
|
35
|
+
return list(self._cursor.fetchall())
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class WorkspaceStoreSession:
|
|
39
|
+
def __init__(self, connection: Any, *, db_path: Optional[Path]) -> None:
|
|
40
|
+
self._connection = connection
|
|
41
|
+
self.db_path = db_path
|
|
42
|
+
|
|
43
|
+
def execute(
|
|
44
|
+
self,
|
|
45
|
+
sql: str,
|
|
46
|
+
params: Optional[Sequence[object]] = None,
|
|
47
|
+
) -> WorkspaceQueryResult:
|
|
48
|
+
return WorkspaceQueryResult(self._connection.execute(sql, _normalize_params(params)))
|
|
49
|
+
|
|
50
|
+
def fetch_one(
|
|
51
|
+
self,
|
|
52
|
+
sql: str,
|
|
53
|
+
params: Optional[Sequence[object]] = None,
|
|
54
|
+
) -> Any:
|
|
55
|
+
return self._connection.execute(sql, _normalize_params(params)).fetchone()
|
|
56
|
+
|
|
57
|
+
def fetch_all(
|
|
58
|
+
self,
|
|
59
|
+
sql: str,
|
|
60
|
+
params: Optional[Sequence[object]] = None,
|
|
61
|
+
) -> list[Any]:
|
|
62
|
+
return list(self._connection.execute(sql, _normalize_params(params)).fetchall())
|
|
63
|
+
|
|
64
|
+
def executemany(
|
|
65
|
+
self,
|
|
66
|
+
sql: str,
|
|
67
|
+
params_seq: Iterable[Sequence[object]],
|
|
68
|
+
) -> None:
|
|
69
|
+
self._connection.executemany(sql, list(params_seq))
|
|
70
|
+
|
|
71
|
+
def export_query_to_parquet(
|
|
72
|
+
self,
|
|
73
|
+
sql: str,
|
|
74
|
+
output_path: str | Path,
|
|
75
|
+
params: Optional[Sequence[object]] = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
target = Path(output_path).expanduser().resolve()
|
|
78
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
escaped = str(target).replace("\\", "\\\\").replace("'", "''")
|
|
80
|
+
if params:
|
|
81
|
+
raise ValueError("export_query_to_parquet does not support bound parameters")
|
|
82
|
+
self._connection.execute(f"COPY ({sql}) TO '{escaped}' (FORMAT PARQUET)")
|
|
83
|
+
|
|
84
|
+
def close(self) -> None:
|
|
85
|
+
self._connection.close()
|
|
86
|
+
|
|
87
|
+
def __enter__(self) -> "WorkspaceStoreSession":
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
91
|
+
self.close()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class WorkspaceStore:
|
|
95
|
+
def __init__(self, *, semantics_dir: Optional[Path] = None) -> None:
|
|
96
|
+
self._semantics_dir = (semantics_dir or resolve_semantics_dir()).resolve()
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def semantics_dir(self) -> Path:
|
|
100
|
+
return self._semantics_dir
|
|
101
|
+
|
|
102
|
+
def open_named(
|
|
103
|
+
self,
|
|
104
|
+
kind: WorkspaceStoreKind,
|
|
105
|
+
*,
|
|
106
|
+
read_only: bool = False,
|
|
107
|
+
) -> WorkspaceStoreSession:
|
|
108
|
+
if str(kind) != "pipeline_states":
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"Named workspace store kind '{kind}' is not supported. Only 'pipeline_states' remains public."
|
|
111
|
+
)
|
|
112
|
+
db_path = resolve_named_store_path(kind, self._semantics_dir)
|
|
113
|
+
if not read_only:
|
|
114
|
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
connection = duckdb.connect(str(db_path), read_only=bool(read_only))
|
|
116
|
+
return WorkspaceStoreSession(connection, db_path=db_path)
|
|
117
|
+
|
|
118
|
+
def open_ephemeral(self) -> WorkspaceStoreSession:
|
|
119
|
+
temp_dir = Path(tempfile.mkdtemp(prefix="cf_workspace_store_"))
|
|
120
|
+
db_path = temp_dir / "ephemeral.store.db"
|
|
121
|
+
return open_path(db_path, read_only=False)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def open_named(
|
|
125
|
+
kind: WorkspaceStoreKind,
|
|
126
|
+
*,
|
|
127
|
+
read_only: bool = False,
|
|
128
|
+
semantics_dir: Optional[Path] = None,
|
|
129
|
+
) -> WorkspaceStoreSession:
|
|
130
|
+
return WorkspaceStore(semantics_dir=semantics_dir).open_named(kind, read_only=read_only)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def open_ephemeral(*, semantics_dir: Optional[Path] = None) -> WorkspaceStoreSession:
|
|
134
|
+
return WorkspaceStore(semantics_dir=semantics_dir).open_ephemeral()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def open_path(db_path: str | Path, *, read_only: bool = False) -> WorkspaceStoreSession:
|
|
138
|
+
path = Path(db_path).expanduser().resolve()
|
|
139
|
+
if not read_only:
|
|
140
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
connection = duckdb.connect(str(path), read_only=bool(read_only))
|
|
142
|
+
return WorkspaceStoreSession(connection, db_path=path)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import cf_workspace_store
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_provider_surface_is_exported() -> None:
|
|
9
|
+
assert cf_workspace_store.cf_workspace_store_provider_descriptor_path()
|
|
10
|
+
assert "cf_workspace_store_provider_path" in cf_workspace_store.__all__
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_provider_descriptor_is_packaged() -> None:
|
|
14
|
+
descriptor_path = Path(cf_workspace_store.cf_workspace_store_provider_descriptor_path())
|
|
15
|
+
assert descriptor_path.is_file()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from cf_workspace_store import WorkspaceStore, resolve_workspace_store_paths
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_named_store_uses_pipeline_state_store_name(tmp_path: Path) -> None:
|
|
12
|
+
store = WorkspaceStore(semantics_dir=tmp_path)
|
|
13
|
+
paths = resolve_workspace_store_paths(tmp_path)
|
|
14
|
+
|
|
15
|
+
with store.open_named("pipeline_states") as session:
|
|
16
|
+
session.execute("CREATE TABLE IF NOT EXISTS demo(value INTEGER)")
|
|
17
|
+
|
|
18
|
+
assert paths.pipeline_states.name == "cf-pipeline-states.store.db"
|
|
19
|
+
assert paths.pipeline_states.is_file()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_pipeline_state_store_does_not_warn(tmp_path: Path) -> None:
|
|
23
|
+
store = WorkspaceStore(semantics_dir=tmp_path)
|
|
24
|
+
|
|
25
|
+
with warnings.catch_warnings(record=True) as caught:
|
|
26
|
+
warnings.simplefilter("always")
|
|
27
|
+
with store.open_named("pipeline_states") as session:
|
|
28
|
+
session.execute("CREATE TABLE IF NOT EXISTS demo(value INTEGER)")
|
|
29
|
+
|
|
30
|
+
assert caught == []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_open_named_rejects_removed_legacy_store_kinds(tmp_path: Path) -> None:
|
|
34
|
+
store = WorkspaceStore(semantics_dir=tmp_path)
|
|
35
|
+
|
|
36
|
+
with pytest.raises(ValueError, match="Only 'pipeline_states' remains public"):
|
|
37
|
+
store.open_named("pipelines")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_ephemeral_store_supports_basic_sql(tmp_path: Path) -> None:
|
|
41
|
+
store = WorkspaceStore(semantics_dir=tmp_path)
|
|
42
|
+
with store.open_ephemeral() as session:
|
|
43
|
+
session.execute("CREATE TABLE demo(value INTEGER)")
|
|
44
|
+
session.execute("INSERT INTO demo VALUES (7)")
|
|
45
|
+
row = session.fetch_one("SELECT value FROM demo")
|
|
46
|
+
assert row == (7,)
|