rtbot-sql 0.1.14__cp314-cp314-win_amd64.whl

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.
rtbot_sql/__init__.py ADDED
@@ -0,0 +1,52 @@
1
+ """rtbot-sql Python runtime."""
2
+
3
+ from .catalog import ColumnDef, InMemoryCatalog, StreamSchema, TableSchema, ViewMeta
4
+ from .compiler import compile_sql, native, validate_sql
5
+ from .formatter import normalize_time_column
6
+ from .pipeline_runner import InputMessage, LocalPipelineRunner, PipelineOutput
7
+ from .runtime import InsertResult, RtBotSql, SqlError
8
+ from .stream_store import InMemoryStreamStore, Message
9
+
10
+
11
+ _default = RtBotSql()
12
+
13
+
14
+ def execute(sql: str):
15
+ return _default.execute(sql)
16
+
17
+
18
+ def insert_dataframe(stream_name: str, dataframe):
19
+ return _default.insert_dataframe(stream_name, dataframe)
20
+
21
+
22
+ def show_graph(view_name: str):
23
+ return _default.show_graph(view_name)
24
+
25
+
26
+ def explain(sql: str):
27
+ return _default.explain(sql)
28
+
29
+
30
+ __all__ = [
31
+ "ColumnDef",
32
+ "StreamSchema",
33
+ "ViewMeta",
34
+ "TableSchema",
35
+ "InMemoryCatalog",
36
+ "InMemoryStreamStore",
37
+ "Message",
38
+ "InputMessage",
39
+ "PipelineOutput",
40
+ "LocalPipelineRunner",
41
+ "InsertResult",
42
+ "RtBotSql",
43
+ "SqlError",
44
+ "normalize_time_column",
45
+ "compile_sql",
46
+ "validate_sql",
47
+ "native",
48
+ "execute",
49
+ "insert_dataframe",
50
+ "show_graph",
51
+ "explain",
52
+ ]
Binary file
rtbot_sql/catalog.py ADDED
@@ -0,0 +1,247 @@
1
+ """In-memory catalog for the Python runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Dict, List, Optional, Sequence
7
+
8
+ from .compiler import native
9
+
10
+
11
+ @dataclass
12
+ class ColumnDef:
13
+ name: str
14
+ index: int
15
+
16
+
17
+ @dataclass
18
+ class StreamSchema:
19
+ name: str
20
+ columns: List[ColumnDef] = field(default_factory=list)
21
+
22
+
23
+ @dataclass
24
+ class ViewMeta:
25
+ name: str
26
+ entity_type: Any
27
+ view_type: Any
28
+ field_map: Dict[str, int] = field(default_factory=dict)
29
+ source_streams: List[str] = field(default_factory=list)
30
+ program_json: str = ""
31
+ output_stream: str = ""
32
+ per_key_prefix: str = ""
33
+ known_keys: List[float] = field(default_factory=list)
34
+ key_index: int = -1
35
+
36
+
37
+ @dataclass
38
+ class TableSchema:
39
+ name: str
40
+ columns: List[ColumnDef] = field(default_factory=list)
41
+ changelog_stream: str = ""
42
+
43
+
44
+ def _to_column_def(obj: Any) -> ColumnDef:
45
+ if isinstance(obj, ColumnDef):
46
+ return ColumnDef(obj.name, obj.index)
47
+ if isinstance(obj, native.ColumnDef):
48
+ return ColumnDef(obj.name, int(obj.index))
49
+ if isinstance(obj, dict):
50
+ return ColumnDef(str(obj["name"]), int(obj["index"]))
51
+ raise TypeError(f"Unsupported column definition: {type(obj)!r}")
52
+
53
+
54
+ def _to_stream_schema(name: str, obj: Any) -> StreamSchema:
55
+ if isinstance(obj, StreamSchema):
56
+ return StreamSchema(name=name or obj.name, columns=[_to_column_def(c) for c in obj.columns])
57
+ if isinstance(obj, native.StreamSchema):
58
+ return StreamSchema(name=name or obj.name, columns=[_to_column_def(c) for c in obj.columns])
59
+ if isinstance(obj, dict):
60
+ cols = [_to_column_def(c) for c in obj.get("columns", [])]
61
+ return StreamSchema(name=name or str(obj.get("name", name)), columns=cols)
62
+ raise TypeError(f"Unsupported stream schema: {type(obj)!r}")
63
+
64
+
65
+ def _to_view_meta(name: str, obj: Any) -> ViewMeta:
66
+ if isinstance(obj, ViewMeta):
67
+ return ViewMeta(
68
+ name=name or obj.name,
69
+ entity_type=obj.entity_type,
70
+ view_type=obj.view_type,
71
+ field_map=dict(obj.field_map),
72
+ source_streams=list(obj.source_streams),
73
+ program_json=obj.program_json,
74
+ output_stream=obj.output_stream,
75
+ per_key_prefix=obj.per_key_prefix,
76
+ known_keys=list(obj.known_keys),
77
+ key_index=int(obj.key_index),
78
+ )
79
+ if isinstance(obj, native.ViewMeta):
80
+ return ViewMeta(
81
+ name=name or obj.name,
82
+ entity_type=obj.entity_type,
83
+ view_type=obj.view_type,
84
+ field_map=dict(obj.field_map),
85
+ source_streams=list(obj.source_streams),
86
+ program_json=obj.program_json,
87
+ output_stream=obj.output_stream,
88
+ per_key_prefix=obj.per_key_prefix,
89
+ known_keys=list(obj.known_keys),
90
+ key_index=int(obj.key_index),
91
+ )
92
+ if isinstance(obj, dict):
93
+ return ViewMeta(
94
+ name=name or str(obj.get("name", name)),
95
+ entity_type=obj.get("entity_type", native.EntityType.VIEW),
96
+ view_type=obj.get("view_type", native.ViewType.SCALAR),
97
+ field_map=dict(obj.get("field_map", {})),
98
+ source_streams=list(obj.get("source_streams", [])),
99
+ program_json=str(obj.get("program_json", "")),
100
+ output_stream=str(obj.get("output_stream", "")),
101
+ per_key_prefix=str(obj.get("per_key_prefix", "")),
102
+ known_keys=[float(v) for v in obj.get("known_keys", [])],
103
+ key_index=int(obj.get("key_index", -1)),
104
+ )
105
+ raise TypeError(f"Unsupported view metadata: {type(obj)!r}")
106
+
107
+
108
+ def _to_table_schema(name: str, obj: Any) -> TableSchema:
109
+ if isinstance(obj, TableSchema):
110
+ return TableSchema(
111
+ name=name or obj.name,
112
+ columns=[_to_column_def(c) for c in obj.columns],
113
+ changelog_stream=obj.changelog_stream,
114
+ )
115
+ if isinstance(obj, native.TableSchema):
116
+ return TableSchema(
117
+ name=name or obj.name,
118
+ columns=[_to_column_def(c) for c in obj.columns],
119
+ changelog_stream=obj.changelog_stream,
120
+ )
121
+ if isinstance(obj, dict):
122
+ return TableSchema(
123
+ name=name or str(obj.get("name", name)),
124
+ columns=[_to_column_def(c) for c in obj.get("columns", [])],
125
+ changelog_stream=str(obj.get("changelog_stream", "")),
126
+ )
127
+ raise TypeError(f"Unsupported table schema: {type(obj)!r}")
128
+
129
+
130
+ def _column_to_native(col: ColumnDef) -> native.ColumnDef:
131
+ return native.ColumnDef(col.name, int(col.index))
132
+
133
+
134
+ def _stream_to_native(schema: StreamSchema) -> native.StreamSchema:
135
+ out = native.StreamSchema()
136
+ out.name = schema.name
137
+ out.columns = [_column_to_native(c) for c in schema.columns]
138
+ return out
139
+
140
+
141
+ def _view_to_native(meta: ViewMeta) -> native.ViewMeta:
142
+ out = native.ViewMeta()
143
+ out.name = meta.name
144
+ out.entity_type = meta.entity_type
145
+ out.view_type = meta.view_type
146
+ out.field_map = dict(meta.field_map)
147
+ out.source_streams = list(meta.source_streams)
148
+ out.program_json = meta.program_json
149
+ out.output_stream = meta.output_stream
150
+ out.per_key_prefix = meta.per_key_prefix
151
+ out.known_keys = [float(v) for v in meta.known_keys]
152
+ out.key_index = int(meta.key_index)
153
+ return out
154
+
155
+
156
+ def _table_to_native(schema: TableSchema) -> native.TableSchema:
157
+ out = native.TableSchema()
158
+ out.name = schema.name
159
+ out.columns = [_column_to_native(c) for c in schema.columns]
160
+ out.changelog_stream = schema.changelog_stream
161
+ return out
162
+
163
+
164
+ class InMemoryCatalog:
165
+ def __init__(self) -> None:
166
+ self._streams: Dict[str, StreamSchema] = {}
167
+ self._views: Dict[str, ViewMeta] = {}
168
+ self._tables: Dict[str, TableSchema] = {}
169
+
170
+ def register_stream(self, name: str, schema: Any) -> None:
171
+ self._streams[name] = _to_stream_schema(name, schema)
172
+
173
+ def register_view(self, name: str, meta: Any) -> None:
174
+ self._views[name] = _to_view_meta(name, meta)
175
+
176
+ def register_table(self, name: str, schema: Any) -> None:
177
+ self._tables[name] = _to_table_schema(name, schema)
178
+
179
+ def lookup_stream(self, name: str) -> Optional[StreamSchema]:
180
+ return self._streams.get(name)
181
+
182
+ def lookup_view(self, name: str) -> Optional[ViewMeta]:
183
+ return self._views.get(name)
184
+
185
+ def lookup_table(self, name: str) -> Optional[TableSchema]:
186
+ return self._tables.get(name)
187
+
188
+ def resolve_entity(self, name: str) -> Optional[Any]:
189
+ if name in self._streams:
190
+ return native.EntityType.STREAM
191
+ if name in self._views:
192
+ return self._views[name].entity_type
193
+ if name in self._tables:
194
+ return native.EntityType.TABLE
195
+ return None
196
+
197
+ def snapshot(self) -> native.CatalogSnapshot:
198
+ snap = native.CatalogSnapshot()
199
+ snap.streams = {name: _stream_to_native(schema) for name, schema in self._streams.items()}
200
+ snap.views = {name: _view_to_native(meta) for name, meta in self._views.items()}
201
+ snap.tables = {name: _table_to_native(schema) for name, schema in self._tables.items()}
202
+ return snap
203
+
204
+ def add_key(self, view_name: str, key: float) -> None:
205
+ view = self._views.get(view_name)
206
+ if view is None:
207
+ return
208
+ key_value = float(key)
209
+ if key_value not in view.known_keys:
210
+ view.known_keys.append(key_value)
211
+
212
+ def get_known_keys(self, view_name: str) -> set:
213
+ view = self._views.get(view_name)
214
+ if view is None:
215
+ return set()
216
+ return set(view.known_keys)
217
+
218
+ def drop_stream(self, name: str) -> None:
219
+ self._streams.pop(name, None)
220
+
221
+ def drop_view(self, name: str) -> None:
222
+ self._views.pop(name, None)
223
+
224
+ def drop_table(self, name: str) -> None:
225
+ self._tables.pop(name, None)
226
+
227
+ def drop(self, name: str) -> bool:
228
+ removed = False
229
+ if name in self._streams:
230
+ self._streams.pop(name, None)
231
+ removed = True
232
+ if name in self._views:
233
+ self._views.pop(name, None)
234
+ removed = True
235
+ if name in self._tables:
236
+ self._tables.pop(name, None)
237
+ removed = True
238
+ return removed
239
+
240
+ def streams(self) -> Dict[str, StreamSchema]:
241
+ return dict(self._streams)
242
+
243
+ def views(self) -> Dict[str, ViewMeta]:
244
+ return dict(self._views)
245
+
246
+ def tables(self) -> Dict[str, TableSchema]:
247
+ return dict(self._tables)
rtbot_sql/compiler.py ADDED
@@ -0,0 +1,45 @@
1
+ """Thin wrappers over the native rtbot-sql extension."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import sys
7
+ from importlib.machinery import ExtensionFileLoader, ModuleSpec
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ try:
12
+ from . import _rtbot_sql_native as native # type: ignore[attr-defined]
13
+ except ImportError: # pragma: no cover - fallback for Bazel runfiles layout
14
+ try:
15
+ import _rtbot_sql_native as native # type: ignore
16
+ except ImportError:
17
+ if not sys.platform.startswith("win"):
18
+ raise
19
+ ext_path = Path(__file__).with_name("_rtbot_sql_native.so")
20
+ if not ext_path.exists():
21
+ raise
22
+ loader = ExtensionFileLoader("rtbot_sql._rtbot_sql_native", str(ext_path))
23
+ spec = ModuleSpec("rtbot_sql._rtbot_sql_native", loader, origin=str(ext_path))
24
+ native = importlib.util.module_from_spec(spec)
25
+ loader.exec_module(native)
26
+
27
+
28
+ def compile_sql(sql: str, catalog_snapshot: Any) -> Any:
29
+ return native.compile_sql(sql, catalog_snapshot)
30
+
31
+
32
+ def validate_sql(sql: str) -> dict:
33
+ return native.validate_sql(sql)
34
+
35
+
36
+ def compile_select_to_program(sql: str, catalog_snapshot: Any) -> Any:
37
+ """Compile a SELECT by wrapping it in CREATE MATERIALIZED VIEW.
38
+
39
+ compile_sql() only emits program_json for tier-3 SELECT queries. For tier-2,
40
+ this wrapper forces graph compilation and lets the Python runtime execute the
41
+ query locally through the native pipeline runner.
42
+ """
43
+ select_sql = sql.strip().rstrip(";")
44
+ wrapped_sql = f"CREATE MATERIALIZED VIEW __rtbot_sql_tmp AS {select_sql}"
45
+ return native.compile_sql(wrapped_sql, catalog_snapshot)
rtbot_sql/formatter.py ADDED
@@ -0,0 +1,73 @@
1
+ """Helpers to format runtime outputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, Iterable, List, Optional, Sequence
6
+
7
+ try:
8
+ import pandas as pd
9
+ _HAS_PANDAS = True
10
+ except Exception: # pragma: no cover - optional dependency
11
+ pd = None
12
+ _HAS_PANDAS = False
13
+
14
+
15
+ def ordered_columns(field_map: Dict[str, int]) -> List[str]:
16
+ return [name for name, _ in sorted(field_map.items(), key=lambda item: item[1])]
17
+
18
+
19
+ def project_rows(rows: Iterable[Sequence[float]], field_map: Dict[str, int]) -> List[List[float]]:
20
+ ordered = sorted(field_map.items(), key=lambda item: item[1])
21
+ indices = [index for _, index in ordered]
22
+
23
+ projected: List[List[float]] = []
24
+ for row in rows:
25
+ projected.append([
26
+ float(row[idx]) if 0 <= idx < len(row) else 0.0
27
+ for idx in indices
28
+ ])
29
+ return projected
30
+
31
+
32
+ def format_rows(
33
+ rows: Iterable[Sequence[float]],
34
+ field_map: Dict[str, int],
35
+ timestamps: Optional[List[int]] = None,
36
+ time_values: Optional[List[Any]] = None,
37
+ time_column: str = "time",
38
+ as_dataframe: bool = True,
39
+ ):
40
+ columns = ordered_columns(field_map)
41
+ normalized = project_rows(rows, field_map)
42
+
43
+ if as_dataframe and _HAS_PANDAS:
44
+ df = pd.DataFrame(normalized, columns=columns)
45
+ if timestamps is not None:
46
+ values = time_values if time_values is not None else timestamps
47
+ df.insert(0, time_column, values)
48
+ # Auto-normalize: rename _time -> time and convert numeric timestamps.
49
+ if '_time' in df.columns and 'time' not in df.columns:
50
+ df = df.rename(columns={'_time': 'time'})
51
+ if pd.api.types.is_numeric_dtype(df['time']):
52
+ df['time'] = pd.to_datetime(df['time'], unit='ms')
53
+ return df
54
+
55
+ result = {
56
+ "columns": columns,
57
+ "rows": normalized,
58
+ }
59
+ if timestamps is not None:
60
+ result["timestamps"] = timestamps
61
+ result["time_column"] = time_column
62
+ return result
63
+
64
+
65
+ def normalize_time_column(df, default_unit='ms'):
66
+ if 'time' in df.columns:
67
+ return df
68
+ if '_time' in df.columns:
69
+ out = df.rename(columns={'_time': 'time'}).copy()
70
+ if pd.api.types.is_numeric_dtype(out['time']):
71
+ out['time'] = pd.to_datetime(out['time'], unit=default_unit)
72
+ return out
73
+ raise KeyError("Expected a time column in SQL result ('time' or '_time')")
rtbot_sql/jupyter.py ADDED
@@ -0,0 +1,42 @@
1
+ """Jupyter display helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Dict
7
+
8
+
9
+ def _safe_node_id(node_id: str) -> str:
10
+ return node_id.replace(":", "_").replace("-", "_")
11
+
12
+
13
+ def program_json_to_mermaid(program_json: str) -> str:
14
+ parsed = json.loads(program_json)
15
+ lines = ["flowchart LR"]
16
+
17
+ operators = parsed.get("operators", [])
18
+ for op in operators:
19
+ op_id = str(op.get("id", "unknown"))
20
+ op_type = str(op.get("type", "Operator"))
21
+ lines.append(f' {_safe_node_id(op_id)}["{op_id}\\n{op_type}"]')
22
+
23
+ for connection in parsed.get("connections", []):
24
+ src = _safe_node_id(str(connection.get("from", "")))
25
+ dst = _safe_node_id(str(connection.get("to", "")))
26
+ from_port = str(connection.get("fromPort", "o1"))
27
+ to_port = str(connection.get("toPort", "i1"))
28
+ lines.append(f' {src} -->|"{from_port}->{to_port}"| {dst}')
29
+
30
+ return "\n".join(lines)
31
+
32
+
33
+ def show_graph(program_json: str):
34
+ mermaid = program_json_to_mermaid(program_json)
35
+
36
+ try:
37
+ from IPython.display import Markdown, display
38
+
39
+ display(Markdown(f"```mermaid\n{mermaid}\n```"))
40
+ return None
41
+ except Exception: # pragma: no cover - used outside notebooks
42
+ return mermaid
@@ -0,0 +1,98 @@
1
+ """Local pipeline runner backed by native rtbot::Program execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Dict, Iterable, List
7
+
8
+ from .compiler import native
9
+ from .stream_store import Message
10
+
11
+
12
+ @dataclass
13
+ class InputMessage:
14
+ timestamp: int
15
+ values: List[float]
16
+ port: str = "i1"
17
+
18
+
19
+ @dataclass
20
+ class PipelineOutput:
21
+ timestamp: int
22
+ values: List[float]
23
+ operator_id: str = ""
24
+ port: str = ""
25
+
26
+
27
+ def _normalize_input(item: Any) -> InputMessage:
28
+ if isinstance(item, InputMessage):
29
+ return InputMessage(int(item.timestamp), [float(v) for v in item.values], item.port)
30
+ if isinstance(item, Message):
31
+ return InputMessage(int(item.timestamp), [float(v) for v in item.values], "i1")
32
+ if isinstance(item, dict):
33
+ return InputMessage(
34
+ int(item["timestamp"]),
35
+ [float(v) for v in item["values"]],
36
+ str(item.get("port", "i1")),
37
+ )
38
+ if isinstance(item, (tuple, list)) and len(item) >= 2:
39
+ port = "i1" if len(item) < 3 else str(item[2])
40
+ return InputMessage(int(item[0]), [float(v) for v in item[1]], port)
41
+ raise TypeError(f"Unsupported input message: {type(item)!r}")
42
+
43
+
44
+ class LocalPipelineRunner:
45
+ def __init__(self) -> None:
46
+ self._pipelines: Dict[str, native.NativePipeline] = {}
47
+ self._counter = 0
48
+
49
+ def _next_id(self) -> str:
50
+ self._counter += 1
51
+ return f"pipeline_{self._counter}"
52
+
53
+ def deploy(self, program_json: str, _input_streams: Any = None, _output_config: Any = None) -> str:
54
+ pipeline_id = self._next_id()
55
+ self._pipelines[pipeline_id] = native.NativePipeline(program_json)
56
+ return pipeline_id
57
+
58
+ def run_once(self, program_json: str, input_messages: Iterable[Any]) -> List[PipelineOutput]:
59
+ pipeline = native.NativePipeline(program_json)
60
+ return self._run_pipeline(pipeline, input_messages)
61
+
62
+ def feed(
63
+ self,
64
+ pipeline_id: str,
65
+ timestamp: int,
66
+ values: List[float],
67
+ port: str = "i1",
68
+ ) -> List[PipelineOutput]:
69
+ pipeline = self._pipelines[pipeline_id]
70
+ native_outputs = pipeline.feed(int(timestamp), [float(v) for v in values], str(port))
71
+ return [
72
+ PipelineOutput(
73
+ timestamp=int(out.timestamp),
74
+ values=[float(v) for v in out.values],
75
+ operator_id=str(out.operator_id),
76
+ port=str(out.port),
77
+ )
78
+ for out in native_outputs
79
+ ]
80
+
81
+ def destroy(self, pipeline_id: str) -> None:
82
+ self._pipelines.pop(pipeline_id, None)
83
+
84
+ def _run_pipeline(self, pipeline: native.NativePipeline, input_messages: Iterable[Any]) -> List[PipelineOutput]:
85
+ outputs: List[PipelineOutput] = []
86
+ for raw in input_messages:
87
+ msg = _normalize_input(raw)
88
+ native_outputs = pipeline.feed(msg.timestamp, msg.values, msg.port)
89
+ outputs.extend(
90
+ PipelineOutput(
91
+ timestamp=int(out.timestamp),
92
+ values=[float(v) for v in out.values],
93
+ operator_id=str(out.operator_id),
94
+ port=str(out.port),
95
+ )
96
+ for out in native_outputs
97
+ )
98
+ return outputs
rtbot_sql/runtime.py ADDED
@@ -0,0 +1,592 @@
1
+ """User-facing Python runtime API for rtbot-sql."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import time
8
+ from collections import defaultdict
9
+ from dataclasses import dataclass
10
+ from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple
11
+
12
+ from .catalog import InMemoryCatalog, StreamSchema, ViewMeta
13
+ from .compiler import compile_select_to_program, compile_sql, native
14
+ from .formatter import format_rows
15
+ from .jupyter import show_graph as display_graph
16
+ from .pipeline_runner import LocalPipelineRunner
17
+ from .stream_store import InMemoryStreamStore
18
+
19
+
20
+ class SqlError(RuntimeError):
21
+ def __init__(self, errors: Sequence[Any]):
22
+ self.errors = list(errors)
23
+ messages = [getattr(err, "message", str(err)) for err in self.errors]
24
+ super().__init__("; ".join(messages) if messages else "SQL error")
25
+
26
+
27
+ @dataclass
28
+ class InsertResult:
29
+ rows: int
30
+ elapsed_s: float
31
+ rows_per_sec: float
32
+
33
+ def __repr__(self):
34
+ return f"Inserted {self.rows:,} rows in {self.elapsed_s:.2f}s ({self.rows_per_sec:,.0f} rows/sec)"
35
+
36
+
37
+ def _enum_name(value: Any) -> str:
38
+ if hasattr(value, "name"):
39
+ return str(value.name)
40
+ text = str(value)
41
+ if "." in text:
42
+ return text.split(".")[-1]
43
+ return text
44
+
45
+
46
+ def _extract_limit(sql: str) -> Optional[int]:
47
+ match = re.search(r"\bLIMIT\s+(\d+)\b", sql, flags=re.IGNORECASE)
48
+ if not match:
49
+ return None
50
+ return int(match.group(1))
51
+
52
+
53
+ class RtBotSql:
54
+ def __init__(self) -> None:
55
+ self._catalog = InMemoryCatalog()
56
+ self._store = InMemoryStreamStore()
57
+ self._runner = LocalPipelineRunner()
58
+
59
+ self._view_pipelines: Dict[str, str] = {}
60
+ self._view_port_maps: Dict[str, Dict[str, str]] = {}
61
+ self._dependencies: Dict[str, set] = defaultdict(set)
62
+
63
+ self._last_timestamp = int(time.time() * 1000)
64
+ self._time_column_name = "time"
65
+ self._time_unit_override: Optional[str] = None
66
+ self._time_formatter: Optional[Callable[[List[int]], List[Any]]] = None
67
+
68
+ def _next_timestamp(self) -> int:
69
+ now = int(time.time() * 1000)
70
+ if now <= self._last_timestamp:
71
+ self._last_timestamp += 1
72
+ else:
73
+ self._last_timestamp = now
74
+ return self._last_timestamp
75
+
76
+ @staticmethod
77
+ def _normalize_sql(sql: str) -> str:
78
+ out = sql.strip()
79
+ out = re.sub(r"^\s*CREATE\s+STREAM\b", "CREATE TABLE", out, flags=re.IGNORECASE)
80
+ out = re.sub(r"^\s*DROP\s+STREAM\b", "DROP TABLE", out, flags=re.IGNORECASE)
81
+ return out
82
+
83
+ def execute(self, sql: str):
84
+ normalized = self._normalize_sql(sql)
85
+
86
+ result = compile_sql(normalized, self._catalog.snapshot())
87
+ if result.has_errors():
88
+ raise SqlError(result.errors)
89
+
90
+ statement = result.statement_type
91
+ if statement == native.StatementType.CREATE_STREAM:
92
+ self._handle_create_stream(result)
93
+ return None
94
+
95
+ if statement == native.StatementType.INSERT:
96
+ self._handle_insert(result)
97
+ return None
98
+
99
+ if statement in (
100
+ native.StatementType.CREATE_VIEW,
101
+ native.StatementType.CREATE_MATERIALIZED_VIEW,
102
+ ):
103
+ self._handle_create_view(result, statement)
104
+ return None
105
+
106
+ if statement == native.StatementType.DROP:
107
+ self._handle_drop(result)
108
+ return None
109
+
110
+ if statement == native.StatementType.SELECT:
111
+ return self._handle_select(normalized, result)
112
+
113
+ raise SqlError([f"Unsupported statement type: {statement}"])
114
+
115
+ def _handle_create_stream(self, result: native.CompilationResult) -> None:
116
+ self._catalog.register_stream(result.entity_name, result.stream_schema)
117
+
118
+ def _handle_insert(self, result: native.CompilationResult) -> None:
119
+ stream_name = result.entity_name
120
+ schema = self._catalog.lookup_stream(stream_name)
121
+ if schema is None:
122
+ raise SqlError([f"Unknown stream: {stream_name}"])
123
+
124
+ payload = [float(v) for v in result.insert_payload]
125
+ if schema.columns and len(payload) != len(schema.columns):
126
+ raise SqlError([
127
+ f"INSERT payload length mismatch for {stream_name}: expected {len(schema.columns)}, got {len(payload)}"
128
+ ])
129
+
130
+ self._append_and_propagate(stream_name, self._next_timestamp(), payload)
131
+
132
+ def _handle_create_view(
133
+ self,
134
+ result: native.CompilationResult,
135
+ statement: Any,
136
+ ) -> None:
137
+ name = result.entity_name
138
+ materialized = statement == native.StatementType.CREATE_MATERIALIZED_VIEW
139
+
140
+ view_meta = ViewMeta(
141
+ name=name,
142
+ entity_type=(
143
+ native.EntityType.MATERIALIZED_VIEW
144
+ if materialized
145
+ else native.EntityType.VIEW
146
+ ),
147
+ view_type=result.view_type,
148
+ field_map=dict(result.field_map),
149
+ source_streams=list(result.source_streams),
150
+ program_json=result.program_json,
151
+ output_stream=name,
152
+ per_key_prefix=f"{name}:key:",
153
+ known_keys=[],
154
+ key_index=int(result.key_index),
155
+ )
156
+
157
+ self._catalog.register_view(name, view_meta)
158
+ for source in view_meta.source_streams:
159
+ self._dependencies[source].add(name)
160
+
161
+ pipeline_id = self._runner.deploy(result.program_json, view_meta.source_streams, {"output_stream": name})
162
+ self._view_pipelines[name] = pipeline_id
163
+ source_port_map: Dict[str, str] = {}
164
+ for i, source in enumerate(view_meta.source_streams):
165
+ source_port_map[source] = f"i{i + 1}"
166
+ self._view_port_maps[name] = source_port_map
167
+
168
+ # Backfill the new view from already-known source data.
169
+ if self._uses_snapshot_sync(name):
170
+ source_rank = {source: i for i, source in enumerate(view_meta.source_streams)}
171
+ events = []
172
+ for source in view_meta.source_streams:
173
+ for msg in self._store.read(source):
174
+ events.append((msg.timestamp, source_rank[source], source, msg.values))
175
+ events.sort(key=lambda item: (item[0], item[1]))
176
+
177
+ history: Dict[str, List[Any]] = {source: [] for source in view_meta.source_streams}
178
+ for timestamp, _, trigger_source, trigger_values in events:
179
+ history[trigger_source].append((timestamp, list(trigger_values)))
180
+
181
+ snapshot: Dict[str, List[float]] = {}
182
+ ready = True
183
+ for source in view_meta.source_streams:
184
+ if source == trigger_source:
185
+ snapshot[source] = list(trigger_values)
186
+ continue
187
+
188
+ prior = None
189
+ for prior_timestamp, prior_values in reversed(history[source]):
190
+ if prior_timestamp <= timestamp:
191
+ prior = prior_values
192
+ break
193
+
194
+ if prior is None:
195
+ ready = False
196
+ break
197
+
198
+ snapshot[source] = list(prior)
199
+
200
+ if not ready:
201
+ continue
202
+
203
+ outputs = []
204
+ for source in view_meta.source_streams:
205
+ port = source_port_map.get(source, "i1")
206
+ outputs.extend(self._runner.feed(pipeline_id, timestamp, snapshot[source], port=port))
207
+
208
+ for out in outputs:
209
+ self._append_and_propagate(name, out.timestamp, out.values)
210
+ else:
211
+ for i, source in enumerate(view_meta.source_streams):
212
+ port = f"i{i + 1}"
213
+ for msg in self._store.read(source):
214
+ outputs = self._runner.feed(pipeline_id, msg.timestamp, msg.values, port=port)
215
+ for out in outputs:
216
+ self._append_and_propagate(name, out.timestamp, out.values)
217
+
218
+ def _handle_drop(self, result: native.CompilationResult) -> None:
219
+ name = result.drop_entity_name
220
+
221
+ if name in self._view_pipelines:
222
+ self._runner.destroy(self._view_pipelines[name])
223
+ self._view_pipelines.pop(name, None)
224
+ self._view_port_maps.pop(name, None)
225
+
226
+ self._catalog.drop(name)
227
+ self._store.clear(name)
228
+
229
+ for dependents in self._dependencies.values():
230
+ dependents.discard(name)
231
+
232
+ self._dependencies.pop(name, None)
233
+
234
+ def _uses_snapshot_sync(self, dependent: str) -> bool:
235
+ view = self._catalog.lookup_view(dependent)
236
+ if view is None or len(view.source_streams) <= 1:
237
+ return False
238
+
239
+ for source in view.source_streams:
240
+ if self._catalog.lookup_stream(source) is None:
241
+ return False
242
+
243
+ return True
244
+
245
+ def _feed_dependent_pipeline(
246
+ self,
247
+ dependent: str,
248
+ stream_name: str,
249
+ timestamp: int,
250
+ values: List[float],
251
+ ) -> List[Any]:
252
+ pipeline_id = self._view_pipelines.get(dependent)
253
+ if pipeline_id is None:
254
+ return []
255
+
256
+ port_map = self._view_port_maps.get(dependent, {})
257
+
258
+ if not self._uses_snapshot_sync(dependent):
259
+ port = port_map.get(stream_name, "i1")
260
+ return self._runner.feed(pipeline_id, timestamp, values, port=port)
261
+
262
+ view = self._catalog.lookup_view(dependent)
263
+ if view is None:
264
+ return []
265
+
266
+ snapshot: Dict[str, List[float]] = {}
267
+ for source in view.source_streams:
268
+ if source == stream_name:
269
+ snapshot[source] = list(values)
270
+ continue
271
+
272
+ prior = self._store.read_latest_before(source, timestamp)
273
+ if prior is None:
274
+ return []
275
+ snapshot[source] = list(prior.values)
276
+
277
+ outputs = []
278
+ for source in view.source_streams:
279
+ port = port_map.get(source, "i1")
280
+ outputs.extend(self._runner.feed(pipeline_id, timestamp, snapshot[source], port=port))
281
+ return outputs
282
+
283
+ def _append_and_propagate(self, stream_name: str, timestamp: int, values: List[float]) -> None:
284
+ self._store.append(stream_name, timestamp, values)
285
+
286
+ view = self._catalog.lookup_view(stream_name)
287
+ if (
288
+ view is not None
289
+ and view.view_type == native.ViewType.KEYED
290
+ and 0 <= view.key_index < len(values)
291
+ ):
292
+ self._catalog.add_key(stream_name, float(values[view.key_index]))
293
+
294
+ for dependent in list(self._dependencies.get(stream_name, set())):
295
+ outputs = self._feed_dependent_pipeline(dependent, stream_name, timestamp, values)
296
+ for out in outputs:
297
+ self._append_and_propagate(dependent, out.timestamp, out.values)
298
+
299
+ def _project_messages(
300
+ self,
301
+ messages: Sequence[Any],
302
+ field_map: Dict[str, int],
303
+ ) -> List[List[float]]:
304
+ ordered = sorted(field_map.items(), key=lambda item: item[1])
305
+ indices = [index for _, index in ordered]
306
+
307
+ rows: List[List[float]] = []
308
+ for msg in messages:
309
+ values = list(msg.values)
310
+ rows.append([
311
+ float(values[idx]) if 0 <= idx < len(values) else 0.0
312
+ for idx in indices
313
+ ])
314
+ return rows
315
+
316
+ def _resolve_field_map(self, source: str, field_map: Dict[str, int]) -> Dict[str, int]:
317
+ if field_map:
318
+ return dict(field_map)
319
+
320
+ source_view = self._catalog.lookup_view(source)
321
+ if source_view is not None:
322
+ return dict(source_view.field_map)
323
+
324
+ source_stream = self._catalog.lookup_stream(source)
325
+ if source_stream is not None:
326
+ return {column.name: column.index for column in source_stream.columns}
327
+
328
+ source_table = self._catalog.lookup_table(source)
329
+ if source_table is not None:
330
+ return {column.name: column.index for column in source_table.columns}
331
+
332
+ return {}
333
+
334
+ def _format_time_values(self, timestamps: List[int]) -> List[Any]:
335
+ if self._time_formatter is not None:
336
+ return list(self._time_formatter(list(timestamps)))
337
+
338
+ if not timestamps:
339
+ return []
340
+
341
+ try:
342
+ import pandas as pd # type: ignore
343
+ except Exception:
344
+ return [int(ts) for ts in timestamps]
345
+
346
+ unit = self._time_unit_override
347
+ if unit is None:
348
+ max_abs = max(abs(int(ts)) for ts in timestamps)
349
+ if max_abs >= 10**17:
350
+ unit = "ns"
351
+ elif max_abs >= 10**14:
352
+ unit = "us"
353
+ elif max_abs >= 10**11:
354
+ unit = "ms"
355
+ else:
356
+ unit = "s"
357
+
358
+ return list(pd.to_datetime(timestamps, unit=unit))
359
+
360
+ def configure_time_format(
361
+ self,
362
+ *,
363
+ unit: Optional[str] = None,
364
+ formatter: Optional[Callable[[List[int]], List[Any]]] = None,
365
+ column_name: str = "time",
366
+ ) -> None:
367
+ if unit is not None and unit not in {"s", "ms", "us", "ns"}:
368
+ raise SqlError([f"Invalid time unit '{unit}'. Expected one of: s, ms, us, ns"])
369
+ self._time_unit_override = unit
370
+ self._time_formatter = formatter
371
+ self._time_column_name = column_name
372
+
373
+ def _execute_tier1(self, sql: str, result: native.CompilationResult):
374
+ source = result.source_streams[0] if result.source_streams else ""
375
+ if not source:
376
+ return format_rows([], {}, timestamps=[])
377
+
378
+ effective_field_map = self._resolve_field_map(source, dict(result.field_map))
379
+ limit = _extract_limit(sql)
380
+ source_view = self._catalog.lookup_view(source)
381
+
382
+ if (
383
+ source_view is not None
384
+ and source_view.view_type == native.ViewType.KEYED
385
+ and " where " not in sql.lower()
386
+ ):
387
+ known_keys = sorted(self._catalog.get_known_keys(source))
388
+ latest_by_key = self._store.read_latest_per_key(
389
+ source,
390
+ known_keys,
391
+ source_view.key_index,
392
+ )
393
+ messages = [latest_by_key[k] for k in known_keys if k in latest_by_key]
394
+ if limit is not None:
395
+ messages = messages[:limit]
396
+ else:
397
+ if limit is None:
398
+ messages = self._store.read(source)
399
+ else:
400
+ messages = self._store.read_latest(source, limit)
401
+
402
+ rows = self._project_messages(messages, effective_field_map)
403
+ timestamps = [msg.timestamp for msg in messages]
404
+ return format_rows(
405
+ rows,
406
+ effective_field_map,
407
+ timestamps=timestamps,
408
+ time_values=self._format_time_values(timestamps),
409
+ time_column=self._time_column_name,
410
+ )
411
+
412
+ def _execute_with_pipeline(self, sql: str, result: native.CompilationResult):
413
+ original_field_map = dict(result.field_map)
414
+ runtime_result = result
415
+ if not runtime_result.program_json:
416
+ runtime_result = compile_select_to_program(sql, self._catalog.snapshot())
417
+ if runtime_result.has_errors():
418
+ raise SqlError(runtime_result.errors)
419
+
420
+ effective_field_map = dict(runtime_result.field_map)
421
+ if not effective_field_map:
422
+ effective_field_map = original_field_map
423
+ if not effective_field_map and runtime_result.source_streams:
424
+ effective_field_map = self._resolve_field_map(runtime_result.source_streams[0], {})
425
+
426
+ if not runtime_result.source_streams:
427
+ return format_rows(
428
+ [],
429
+ effective_field_map,
430
+ timestamps=[],
431
+ time_values=[],
432
+ time_column=self._time_column_name,
433
+ )
434
+
435
+ inputs: List[Tuple[int, List[float], str]] = []
436
+ uses_snapshot_sync = (
437
+ len(runtime_result.source_streams) > 1
438
+ and all(self._catalog.lookup_stream(source) is not None for source in runtime_result.source_streams)
439
+ )
440
+
441
+ if uses_snapshot_sync:
442
+ source_rank = {source: i for i, source in enumerate(runtime_result.source_streams)}
443
+ events: List[Tuple[int, int, str, List[float]]] = []
444
+ for source in runtime_result.source_streams:
445
+ for msg in self._store.read(source):
446
+ events.append((msg.timestamp, source_rank[source], source, list(msg.values)))
447
+ events.sort(key=lambda item: (item[0], item[1]))
448
+
449
+ latest_by_source: Dict[str, List[float]] = {}
450
+ for timestamp, _, trigger_source, trigger_values in events:
451
+ latest_by_source[trigger_source] = list(trigger_values)
452
+ if len(latest_by_source) < len(runtime_result.source_streams):
453
+ continue
454
+
455
+ for i, source in enumerate(runtime_result.source_streams):
456
+ inputs.append((timestamp, list(latest_by_source[source]), f"i{i + 1}"))
457
+ else:
458
+ for i, source in enumerate(runtime_result.source_streams):
459
+ port = f"i{i + 1}"
460
+ inputs.extend((msg.timestamp, msg.values, port) for msg in self._store.read(source))
461
+
462
+ outputs = self._runner.run_once(runtime_result.program_json, inputs)
463
+ timestamps = [out.timestamp for out in outputs]
464
+ rows = [list(out.values) for out in outputs]
465
+
466
+ limit = _extract_limit(sql)
467
+ if limit is not None:
468
+ rows = rows[:limit]
469
+ timestamps = timestamps[:limit]
470
+
471
+ return format_rows(
472
+ rows,
473
+ effective_field_map,
474
+ timestamps=timestamps,
475
+ time_values=self._format_time_values(timestamps),
476
+ time_column=self._time_column_name,
477
+ )
478
+
479
+ def _handle_select(self, sql: str, result: native.CompilationResult):
480
+ if result.select_tier == native.SelectTier.TIER1_READ:
481
+ return self._execute_tier1(sql, result)
482
+ return self._execute_with_pipeline(sql, result)
483
+
484
+ def explain(self, sql: str) -> Dict[str, Any]:
485
+ normalized = self._normalize_sql(sql)
486
+ result = compile_sql(normalized, self._catalog.snapshot())
487
+
488
+ output = {
489
+ "statement_type": _enum_name(result.statement_type),
490
+ "select_tier": _enum_name(result.select_tier),
491
+ "entity_name": result.entity_name,
492
+ "source_streams": list(result.source_streams),
493
+ "field_map": dict(result.field_map),
494
+ "view_type": _enum_name(result.view_type),
495
+ "key_index": int(result.key_index),
496
+ "program_json": None,
497
+ "errors": [
498
+ {
499
+ "message": err.message,
500
+ "line": int(err.line),
501
+ "column": int(err.column),
502
+ }
503
+ for err in result.errors
504
+ ],
505
+ }
506
+
507
+ if result.program_json:
508
+ output["program_json"] = json.loads(result.program_json)
509
+
510
+ return output
511
+
512
+ def show_graph(self, view_name: str):
513
+ view = self._catalog.lookup_view(view_name)
514
+ if view is None:
515
+ raise SqlError([f"Unknown view: {view_name}"])
516
+ return display_graph(view.program_json)
517
+
518
+ def debug(self, sql: str) -> Dict[str, Any]:
519
+ return self.explain(sql)
520
+
521
+ def export_for_redis(self, view_name: str) -> str:
522
+ view = self._catalog.lookup_view(view_name)
523
+ if view is None:
524
+ raise SqlError([f"Unknown view: {view_name}"])
525
+
526
+ kind = (
527
+ "MATERIALIZED VIEW"
528
+ if view.entity_type == native.EntityType.MATERIALIZED_VIEW
529
+ else "VIEW"
530
+ )
531
+ sources = ", ".join(view.source_streams)
532
+ return (
533
+ "-- Original SQL text is not persisted in the Python runtime catalog.\n"
534
+ f"CREATE {kind} {view_name} AS /* sources: {sources} */;"
535
+ )
536
+
537
+ def insert_dataframe(
538
+ self,
539
+ stream_name: str,
540
+ dataframe: Any,
541
+ column_map: Optional[Dict[str, str]] = None,
542
+ ) -> InsertResult:
543
+ schema = self._catalog.lookup_stream(stream_name)
544
+ if schema is None:
545
+ raise SqlError([f"Unknown stream: {stream_name}"])
546
+
547
+ cmap = column_map or {}
548
+
549
+ time_col = None
550
+ if hasattr(dataframe, "columns"):
551
+ for candidate in ("time", "timestamp", "ts"):
552
+ if candidate in dataframe.columns:
553
+ time_col = candidate
554
+ break
555
+
556
+ row_count = 0
557
+ t0 = time.perf_counter()
558
+
559
+ if hasattr(dataframe, "iterrows"):
560
+ for _, row in dataframe.iterrows():
561
+ timestamp = int(row[time_col]) if time_col else self._next_timestamp()
562
+ values = [float(row[cmap.get(col.name, col.name)]) for col in schema.columns]
563
+ self._append_and_propagate(stream_name, timestamp, values)
564
+ row_count += 1
565
+ else:
566
+ for row in dataframe:
567
+ if isinstance(row, dict):
568
+ if time_col:
569
+ timestamp = int(row.get(time_col, self._next_timestamp()))
570
+ else:
571
+ dict_time_col = None
572
+ for candidate in ("time", "timestamp", "ts"):
573
+ if candidate in row:
574
+ dict_time_col = candidate
575
+ break
576
+ timestamp = int(row[dict_time_col]) if dict_time_col else self._next_timestamp()
577
+ values = [float(row[cmap.get(col.name, col.name)]) for col in schema.columns]
578
+ else:
579
+ timestamp = self._next_timestamp()
580
+ values = [float(v) for v in row]
581
+ self._append_and_propagate(stream_name, timestamp, values)
582
+ row_count += 1
583
+
584
+ elapsed = time.perf_counter() - t0
585
+ rps = row_count / elapsed if elapsed > 0 else float('inf')
586
+ return InsertResult(rows=row_count, elapsed_s=elapsed, rows_per_sec=rps)
587
+
588
+ def get_catalog(self) -> InMemoryCatalog:
589
+ return self._catalog
590
+
591
+ def get_store(self) -> InMemoryStreamStore:
592
+ return self._store
@@ -0,0 +1,100 @@
1
+ """In-memory stream message store."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Any, Dict, Iterable, List, Optional
7
+
8
+
9
+ @dataclass
10
+ class Message:
11
+ timestamp: int
12
+ values: List[float]
13
+
14
+
15
+ def _coerce_message(item: Any) -> Message:
16
+ if isinstance(item, Message):
17
+ return Message(int(item.timestamp), [float(v) for v in item.values])
18
+ if isinstance(item, dict):
19
+ return Message(int(item["timestamp"]), [float(v) for v in item["values"]])
20
+ if isinstance(item, (tuple, list)) and len(item) == 2:
21
+ return Message(int(item[0]), [float(v) for v in item[1]])
22
+ raise TypeError(f"Unsupported message payload: {type(item)!r}")
23
+
24
+
25
+ class InMemoryStreamStore:
26
+ def __init__(self) -> None:
27
+ self._streams: Dict[str, List[Message]] = {}
28
+
29
+ def append(self, stream_name: str, timestamp: int, vector: List[float]) -> None:
30
+ messages = self._streams.setdefault(stream_name, [])
31
+ messages.append(Message(int(timestamp), [float(v) for v in vector]))
32
+
33
+ def append_batch(self, stream_name: str, messages: Iterable[Any]) -> None:
34
+ for item in messages:
35
+ msg = _coerce_message(item)
36
+ self.append(stream_name, msg.timestamp, msg.values)
37
+
38
+ def read(self, stream_name: str) -> List[Message]:
39
+ return list(self._streams.get(stream_name, []))
40
+
41
+ def read_latest(self, stream_name: str, count: int) -> List[Message]:
42
+ if count <= 0:
43
+ return []
44
+ data = self._streams.get(stream_name, [])
45
+ if count >= len(data):
46
+ return list(data)
47
+ return list(data[-count:])
48
+
49
+ def read_latest_before(self, stream_name: str, timestamp: int) -> Optional[Message]:
50
+ data = self._streams.get(stream_name, [])
51
+ target = int(timestamp)
52
+ for msg in reversed(data):
53
+ if msg.timestamp <= target:
54
+ return Message(msg.timestamp, list(msg.values))
55
+ return None
56
+
57
+ def read_range(
58
+ self,
59
+ stream_name: str,
60
+ from_time: Optional[int],
61
+ to_time: Optional[int],
62
+ limit: Optional[int] = None,
63
+ ) -> List[Message]:
64
+ result: List[Message] = []
65
+ for msg in self._streams.get(stream_name, []):
66
+ if from_time is not None and msg.timestamp < from_time:
67
+ continue
68
+ if to_time is not None and msg.timestamp > to_time:
69
+ continue
70
+ result.append(msg)
71
+ if limit is not None and limit > 0 and len(result) >= limit:
72
+ break
73
+ return result
74
+
75
+ def read_latest_per_key(
76
+ self,
77
+ view_name: str,
78
+ known_keys: Iterable[float],
79
+ key_index: int,
80
+ ) -> Dict[float, Message]:
81
+ wanted = {float(v) for v in known_keys}
82
+ out: Dict[float, Message] = {}
83
+
84
+ if not wanted:
85
+ return out
86
+
87
+ for msg in reversed(self._streams.get(view_name, [])):
88
+ if key_index < 0 or key_index >= len(msg.values):
89
+ continue
90
+ key = float(msg.values[key_index])
91
+ if key not in wanted or key in out:
92
+ continue
93
+ out[key] = msg
94
+ if len(out) == len(wanted):
95
+ break
96
+
97
+ return out
98
+
99
+ def clear(self, stream_name: str) -> None:
100
+ self._streams.pop(stream_name, None)
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.1
2
+ Name: rtbot-sql
3
+ Author: Katenaria
4
+ Author-email: services@katenaria.com
5
+ Home-page: https://rtbot.dev
6
+ License: Proprietary
7
+ Description-Content-Type: text/markdown
8
+ Summary: Real-time SQL computation engine for streaming numerical data
9
+ Project-URL: Documentation, https://rtbot.dev/docs
10
+ Project-URL: Source, https://github.com/rtbot-dev/rtbot-sql
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: Other/Proprietary License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Requires-Python: >=3.13
21
+ Version: 0.1.14
22
+
23
+ # RtBot SQL
24
+
25
+ Real-time SQL computation engine for streaming numerical data. Write SQL to define streaming pipelines — RtBot compiles them into high-performance C++ programs that process data incrementally, one message at a time.
26
+
27
+ ## Quick start
28
+
29
+ ```python
30
+ from rtbot_sql import RtBotSql
31
+
32
+ sql = RtBotSql()
33
+ sql.execute("CREATE STREAM sensors (temperature DOUBLE, pressure DOUBLE)")
34
+ sql.execute("""
35
+ CREATE MATERIALIZED VIEW alerts AS
36
+ SELECT temperature, pressure,
37
+ MOVING_AVERAGE(temperature, 50) AS avg_temp,
38
+ MOVING_STD(temperature, 50) AS std_temp
39
+ FROM sensors
40
+ WHERE ABS(temperature - MOVING_AVERAGE(temperature, 50)) > 2 * MOVING_STD(temperature, 50)
41
+ """)
42
+
43
+ import pandas as pd
44
+ df = pd.read_csv("sensor_history.csv")
45
+ sql.insert_dataframe("sensors", df)
46
+ results = sql.execute("SELECT * FROM alerts")
47
+ ```
48
+
49
+ ## Features
50
+
51
+ - **One language, all stages** — the SQL you write in a notebook is the same SQL that runs in production
52
+ - **Incremental execution** — each new data point updates pipeline state in constant time
53
+ - **Deterministic** — same input always produces the same output, regardless of timing
54
+ - **High performance** — native C++ engine with Python bindings via pybind11
55
+
56
+ ## Documentation
57
+
58
+ - [Getting Started](https://rtbot.dev/docs/user-guide/getting-started)
59
+ - [Python Quickstart](https://rtbot.dev/docs/user-guide/quickstart-python)
60
+ - [SQL Reference](https://rtbot.dev/docs/reference/rtbot-sql/overview)
61
+
62
+ ## License
63
+
64
+ Proprietary. See [rtbot.dev](https://rtbot.dev) for licensing options.
65
+
@@ -0,0 +1,12 @@
1
+ rtbot_sql/_rtbot_sql_native.so,sha256=LwP12rSJOE6uLgjUajKHYfosUawhpo8OlnCAfn0lgo8,6317568
2
+ rtbot_sql/__init__.py,sha256=7A40ISiExSGggL6uMctNPVmH6wtjjBpY2IGKUhOuiLI,1209
3
+ rtbot_sql/catalog.py,sha256=GgPTJD_LIyhrOMWxualw1LOELOIs8yBleo_sfqQjH14,8170
4
+ rtbot_sql/compiler.py,sha256=hCgOtob2uFBqbpCR7ZPnVxmJ1jBlZvfNd9Qe94HskyE,1643
5
+ rtbot_sql/formatter.py,sha256=34T4-4hGVIRUmH1ut7xD7U-6miMT4Kbn54_Qj1H2Rcc,2340
6
+ rtbot_sql/jupyter.py,sha256=KwiqlK62U-A-a9nHUWPYsRTQJkjuzDIjrUC-ldhBHOs,1242
7
+ rtbot_sql/pipeline_runner.py,sha256=TKvQkk0uCE4Qr7CkjVmQUWQaI8o0t0qexyeQU6Pj3lg,3163
8
+ rtbot_sql/runtime.py,sha256=KJCMXLRoZwJgwLHzTwFH0sspHRyN3jSK10m5pd-mipQ,20436
9
+ rtbot_sql/stream_store.py,sha256=0XNgnNSgKKe1uJ7WgY5jL_t18dgdYAl6VsxXQq1wN-4,3136
10
+ rtbot_sql-0.1.14.dist-info/WHEEL,sha256=N9E0YzaAkxKF8USZMMWbkNIXLjcNcbLNitkJXYC8GwI,101
11
+ rtbot_sql-0.1.14.dist-info/METADATA,sha256=uDCfH0eIFZ0WX7mZPMAi3k2kYMK47xcbaWlrFGNE-pU,2374
12
+ rtbot_sql-0.1.14.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bazel-wheelmaker 1.0
3
+ Root-Is-Purelib: false
4
+ Tag: cp314-cp314-win_amd64