Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/js-udf.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Timeplus supports JavaScript-based UDF running in the SQL engine. You can develo
## Register a JS UDF via SQL {#ddl}
Please check [CREATE FUNCTION](/sql-create-function#javascript-udf) page for the SQL syntax.

Since Timeplus Enterprise 3.2.1, a server can refuse new JavaScript UDFs entirely by setting [`enable_javascript_udf: false`](/server_config#enable_javascript_udf) in `timeplusd.yaml`. Creation then fails with *JavaScript UDF creation is disabled by server config `enable_javascript_udf`*; UDFs already registered keep working.

## Register a JS UDF via Web Console {#register}

1. Open "UDFs" from the navigation menu on the left, and click the 'New UDF' button.
Expand Down
29 changes: 29 additions & 0 deletions docs/named-collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Named collections can be used in:
2. External table DDL settings
3. Input DDL settings
4. Disk creation arguments.
5. [Python UDF](/py-udf) initialization parameters, since Timeplus Enterprise 3.3.1.

For external stream / external table / input, named collection can specify any setting value other than `type`.

Expand Down Expand Up @@ -165,3 +166,31 @@ Create a S3 disk.
```sql
CREATE DISK s3_disk1 disk(named_collection=s3_config);
```

### Python UDF Initialization

Keep a credential used by a [Python UDF](/py-udf) out of the UDF definition. The collection supplies the `init_function_parameters` key, which is passed to the UDF's init hook when the module loads:

```sql
CREATE NAMED COLLECTION nc_udf_init AS
init_function_parameters = '{"api_key":"s3cr3t"}' NOT OVERRIDABLE;
```

Reference it from the UDF:

```sql
CREATE OR REPLACE FUNCTION call_api(x string) RETURNS string LANGUAGE PYTHON AS $$
import json
API_KEY = ''

def _tp_init(params):
global API_KEY
API_KEY = json.loads(params)['api_key']

def call_api(xs):
return [API_KEY + ':' + x.decode('utf-8') for x in xs]
$$ SETTINGS init_function_name = '_tp_init',
named_collection = 'nc_udf_init';
```

`SHOW CREATE FUNCTION call_api` reveals only the collection name, never the value. Creating such a UDF requires the `NAMED COLLECTION` privilege. See [Initialization hook](/py-udf#init_hook) for details.
265 changes: 214 additions & 51 deletions docs/py-udf.md

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions docs/server_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,20 @@ LEFT JOIN tableB ON tableA.id=tableB.id
SETTINGS join_max_buffered_bytes=8589934592;
```

#### enable_python_udf
Type: bool

Default: true

Available since Timeplus Enterprise 3.2.1. When set to false, this node rejects `CREATE FUNCTION`/`CREATE AGGREGATE FUNCTION` with `LANGUAGE PYTHON` — via SQL or the REST endpoint — with the error *Python UDF creation is disabled by server config `enable_python_udf`*. Existing Python UDFs continue to run; the flag gates creation only. It is applied by `SYSTEM RELOAD CONFIG` and is per-node, so set it on every node. See [Python UDF](/py-udf#enable_flag).

#### enable_javascript_udf
Type: bool

Default: true

Available since Timeplus Enterprise 3.2.1. The JavaScript counterpart of `enable_python_udf`, rejecting creation of `LANGUAGE JAVASCRIPT` UDFs and UDAFs with the error *JavaScript UDF creation is disabled by server config `enable_javascript_udf`*.

#### logger
```yaml
logger:
Expand Down
56 changes: 56 additions & 0 deletions docs/shared/python-external-stream-write.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,62 @@ CREATE MATERIALIZED VIEW high_value_alerts INTO py_alert_sink AS

The materialized view feeds chunks into the sink as they are produced; each chunk becomes one call to `py_alert_sink`.

### Flushing buffered writes {#flush}

Available since **Timeplus Enterprise 3.3.1**.

Sinks that batch — accumulating rows in Python and shipping them in one API call, one file, or one bulk insert — need a moment to push what is still in the buffer. `flush_function_name` names a zero-argument function that Timeplus calls for you:

* **On every checkpoint**, so a long-running materialized view sink does not hold rows indefinitely.
* **Once when the query closes**, immediately *before* the deinit function, so a graceful shutdown does not drop buffered rows.

```sql
CREATE EXTERNAL STREAM py_batched_sink (event_id string, body string)
AS $$
import builtins, json, urllib.request

def open_client(config):
builtins._tp_batch = {"url": json.loads(config)["url"], "rows": []}

def collect(event_id, body):
builtins._tp_batch["rows"].extend(
{"id": eid.decode(), "body": b.decode()} for eid, b in zip(event_id, body)
)

def flush_batch():
batch = builtins._tp_batch
if not batch["rows"]:
return
req = urllib.request.Request(
batch["url"],
data=json.dumps(batch["rows"]).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
urllib.request.urlopen(req).read()
batch["rows"] = []

def close_client():
if hasattr(builtins, "_tp_batch"):
del builtins._tp_batch
$$
SETTINGS
type = 'python',
init_function_name = 'open_client',
init_function_parameters = '{"url":"https://hooks.example.com/bulk"}',
write_function_name = 'collect',
flush_function_name = 'flush_batch',
deinit_function_name = 'close_client';
```

Notes:

* The flush function takes **no arguments** and its return value is ignored. Make it safe to call when the buffer is empty — Timeplus does not know whether you have anything pending.
* It runs at most once during close, even if close is reached through both an explicit finish and session teardown, so `flush` followed by `deinit` is the exact order to rely on.
* It is a **sink-only** hook. A stream that is only read from never calls it, no matter what `flush_function_name` is set to.
* The name must exist in the Python body. A missing function fails the INSERT with `UDF_INTERNAL_ERROR`, and unlike `init_function_parameters` this is *not* caught at `CREATE` time.
* If flush raises during close, the exception surfaces from the query and deinit still runs.

### Custom protocol example: webhook POST

Load the destination URL in init, reuse that configuration for every chunk, and clear it in deinit. Init parameters carry the URL so the Python body is reusable across environments. To pool an actual HTTP connection, swap `urllib` for a session-aware client (for example `requests.Session()`) and stash the session itself on `builtins`.
Expand Down
8 changes: 7 additions & 1 deletion docs/shared/python-external-stream.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ def init_fn(config): # optional

def deinit_fn(): # optional
...

def flush_fn(): # optional, sink only
...
$$
SETTINGS
type = 'python', -- required
Expand All @@ -28,6 +31,7 @@ SETTINGS
init_function_name = '..',
init_function_parameters = '..', -- requires init_function_name
deinit_function_name = '..',
flush_function_name = '..', -- sink only, 3.3.1+
mode = 'auto' -- 'auto' (default), 'streaming', or 'batch'
```

Expand All @@ -39,6 +43,7 @@ SETTINGS
* **init_function_name**: name of a Python function called once before read/write processing begins. Use it to open connections, warm caches, or prepare state for the entry function to consume.
* **init_function_parameters**: a single string passed as the only argument to the init function. Any format works (JSON, `key=value`, or a plain string) — parsing is up to your Python code. Requires `init_function_name`; otherwise the stream fails to create with `Setting 'init_function_parameters' requires 'init_function_name' to be configured`.
* **deinit_function_name**: name of a Python function called once after read/write processing completes, for cleanup.
* **flush_function_name**: name of a Python function called on every checkpoint and once before deinit, so buffered writes are not lost. Takes no arguments. **Write path only** — it is never called when the stream is read from. Available since **Timeplus Enterprise 3.3.1**. See [Flushing buffered writes](/python-external-stream-sink#flush).
* **mode**: Python execution mode — `'auto'` (default), `'streaming'`, or `'batch'`. See [Modes](#modes).

## Modes
Expand All @@ -59,7 +64,8 @@ Each query that reads from or writes to a Python External Stream creates its own
2. Local API credential globals are injected into the module (see [Local API credentials](#local-api-credentials)).
3. If `init_function_name` is set, the init function is called once. When `init_function_parameters` is non-empty, it is passed as the only argument; otherwise init receives no arguments.
4. The read or write entry function is called as data flows.
5. When the query ends — normally or via cancellation — `deinit_function_name`, if set, is called.
5. On the write path only, if `flush_function_name` is set it is called at every checkpoint, and once more when the query closes — before deinit.
6. When the query ends — normally or via cancellation — `deinit_function_name`, if set, is called.

Each query gets its own module, so ordinary module globals created by the DDL body are not reused across queries. If you stash state on Python's `builtins` module, use a stream-specific attribute name and remove it in deinit; `builtins` is shared by the embedded interpreter, so leftover attributes can be visible to later Python sessions in the same server process. Treat clients or caches opened in init as per-query resources and close them in deinit.

Expand Down
3 changes: 2 additions & 1 deletion docs/sql-create-external-stream.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,11 @@ SETTINGS
init_function_name = '..',
init_function_parameters = '..',
deinit_function_name = '..',
flush_function_name = '..', -- sink only, 3.3.1+
mode = 'auto' -- 'auto' (default), 'streaming', or 'batch'
```

Available in **Timeplus Enterprise 3.2.2+**.
Available in **Timeplus Enterprise 3.2.2+**. `flush_function_name` requires **3.3.1+**.

Please check the [Python External Stream Source](/python-external-stream-source) for read-side settings, generator/batch sources, and lifecycle hooks, and the [Python External Stream Sink](/python-external-stream-sink) for write-side semantics, materialized-view sinks, and custom-protocol examples.

Expand Down
31 changes: 29 additions & 2 deletions docs/sql-create-function.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ $$;
[Learn More](/js-udf)

## Python UDF
starting from v2.7, Timeplus Enterprise also supports Python-based UDF. You can develop User-defined scalar functions (UDFs) or User-defined aggregate functions (UDAFs) with the embedded Python 3.10 runtime in Timeplus core engine. No need to deploy extra server/service for the UDF.
starting from v2.7, Timeplus Enterprise also supports Python-based UDF. You can develop User-defined scalar functions (UDFs) or User-defined aggregate functions (UDAFs) with the embedded Python runtime in Timeplus core engine — Python 3.14 free-threaded since Timeplus Enterprise 3.3.1, Python 3.10 in earlier versions. No need to deploy extra server/service for the UDF.

[Learn more](/py-udf) why Python UDF, and how to map the data types in Timeplus and Python, as well as how to manage dependencies.

Expand Down Expand Up @@ -180,4 +180,31 @@ $$
SETTINGS ...
```

[Learn More](/py-udf)
### Settings {#py-udf-settings}

Python UDFs and UDAFs accept the following settings, all available since Timeplus Enterprise 3.3.1. Any other setting name is rejected at creation time, and all three are Python-only — using them with `LANGUAGE JAVASCRIPT` fails.

| Setting | Description |
| -- | -- |
| `init_function_name` | Name of a function defined in the same code block that Timeplus calls once when the module is loaded, before the first UDF call and before a UDAF class is constructed. Must exist in the source, or the `CREATE` is rejected. |
| `init_function_parameters` | A single string passed as the only argument to the init function. Encode structured configuration as JSON. Requires `init_function_name`. Stored in the UDF definition and visible in `SHOW CREATE FUNCTION` — do not put secrets here. |
| `named_collection` | Name of a [named collection](/named-collection) whose `init_function_parameters` key is passed to the init function instead. Requires `init_function_name`, requires the `NAMED COLLECTION` privilege, and is mutually exclusive with `init_function_parameters`. Only the collection name is stored, so the value stays out of `SHOW CREATE FUNCTION`. |

If neither parameter source is set, the init function is called with no arguments.

```sql
CREATE OR REPLACE FUNCTION call_api(x string) RETURNS string LANGUAGE PYTHON AS $$
import json
API_KEY = ''

def _tp_init(params):
global API_KEY
API_KEY = json.loads(params)['api_key']

def call_api(xs):
return [API_KEY + ':' + x.decode('utf-8') for x in xs]
$$ SETTINGS init_function_name = '_tp_init',
named_collection = 'nc_udf_init';
```

[Learn More](/py-udf) — see [Initialization hook](/py-udf#init_hook) for the full behavior.
69 changes: 68 additions & 1 deletion docs/sql-system-python-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,71 @@ SYSTEM INSTALL PYTHON PACKAGE 'requests==2.32.3';
-- Alternative form with separate version literal
SYSTEM INSTALL PYTHON PACKAGE 'requests' '2.32.3';

-- Install several packages at once from inline requirements text (3.1.2+)
SYSTEM INSTALL PYTHON PACKAGE REQUIREMENTS 'requests==2.32.3
pydantic>=2.0
# comments and blank lines are ignored
httpx';

-- Install from a private mirror instead of PyPI (3.1.2+)
SYSTEM INSTALL PYTHON PACKAGE 'internal-lib==1.4.0'
INDEX_URL 'https://mirror.example.com/simple';

-- Fall back to PyPI for anything the private mirror does not carry
SYSTEM INSTALL PYTHON PACKAGE REQUIREMENTS 'internal-lib==1.4.0
requests==2.32.3'
INDEX_URL 'https://mirror.example.com/simple'
EXTRA_INDEX_URL 'https://pypi.org/simple';

-- List installed packages (returns: package_name, version)
SYSTEM LIST PYTHON PACKAGES;

-- Uninstall
SYSTEM UNINSTALL PYTHON PACKAGE 'requests';
```

:::info Clean environment since 3.3.1
Timeplus Enterprise 3.3.1 upgraded the embedded interpreter to **Python 3.14 free-threaded** and stopped bundling third-party packages — only the standard library, `pip` and `truststore` ship with the product. Everything your UDFs import must be installed with the commands on this page (or declared via [`python_requirements`](#declarative)). See [Python UDF](/py-udf#upgrade_314) for the upgrade steps.
:::

## Installing from requirements text {#requirements}

Available since **Timeplus Enterprise 3.1.2**.

`SYSTEM INSTALL PYTHON PACKAGE REQUIREMENTS '<text>'` takes the contents of a `requirements.txt` as a single string literal and installs every line in one cluster-wide task. Rules:

- One package specification per line, using the same PEP 440 syntax as the single-package form. Blank lines and `#` comments are skipped.
- **pip options are rejected.** Any line starting with `-` (`-r nested.txt`, `--index-url ...`, `-e .`) fails with `Requirements line '...' is not supported. Please pass index options via API fields instead` — use the `INDEX_URL` / `EXTRA_INDEX_URL` clauses below.
- The text must contain at least one package specification, and is capped at 1024 effective lines.
- `REQUIREMENTS` cannot be combined with a package-name literal — `SYSTEM INSTALL PYTHON PACKAGE REQUIREMENTS 'requests' '2.0'` is a syntax error.
- The whole batch is tracked as a **single** row in `system.python_package_tasks`, with `package_name` set to the literal `requirements.txt` rather than to any individual package:
```sql
SELECT status, error_code, error_message
FROM system.python_package_tasks
WHERE package_name = 'requirements.txt' AND operation = 'install'
ORDER BY created_at DESC
LIMIT 1;
```

## Private package indexes {#index-url}

Available since **Timeplus Enterprise 3.1.2**. Both clauses work with either install form, and may be combined:

- **INDEX_URL '\<url\>'** — replaces the default index, mapping to pip's `--index-url`. At most one.
- **EXTRA_INDEX_URL '\<url\>'** — an additional index consulted after the primary one, mapping to pip's `--extra-index-url`. Repeat the clause to pass several.

Only `http://` and `https://` URLs are accepted; anything else is rejected up front with `Invalid --index-url value '...'. Only http(s) URLs are supported`, before any node runs pip. Credentials embedded in the URL (`https://user:token@mirror.example.com/simple`) are passed through to pip as-is, but the statement is distributed across the cluster and recorded in `system.query_log` like any other query — prefer a mirror that authenticates by network location, or configure the index once in [`python_requirements`](#declarative) rather than repeating the secret in every statement.

These clauses apply to the statement only; they are not remembered for later installs.

## Behavior
- Scope: Cluster-wide installation/uninstallation using the UDF runtime’s Python 3.10 environment.
- Scope: Cluster-wide installation/uninstallation using the UDF runtime’s Python environment (Python 3.14 since 3.3.1, Python 3.10 before that).
- Permissions: Requires `SYSTEM RELOAD CONFIG` privilege.
- Versioning: Accepts PEP 440 specifiers in the first literal (e.g., `>=`, `==`, `~=`). When using the second literal, provide the exact version string.
- Install location: Uses pip’s user install under the embedded interpreter; no system-level Python changes.
- Async operations: Install/uninstall run asynchronously. Track progress via `system.python_package_tasks`.
- Wheel compatibility: The 3.3.1+ interpreter is `cp314t`, so a package needs a free-threaded (or pure-Python) wheel; otherwise pip builds it from source and needs a toolchain on the node.
- Durability: Packages installed this way live in the node’s local user site-packages. On a node without a persistent volume they are lost on reschedule — use [`python_requirements`](#declarative) for those.

Monitor status
```sql
Expand All @@ -48,6 +100,21 @@ Granting permissions
GRANT SYSTEM RELOAD CONFIG ON *.* TO gen;
```

## Declarative alternative: `python_requirements` {#declarative}

Since Timeplus Enterprise 3.3.1 you can declare packages in a `requirements.txt` on S3 and have every node reconcile against it, instead of issuing `SYSTEM INSTALL` per node. This is the recommended approach for clusters and for ephemeral compute nodes, where locally installed packages do not survive a reschedule.

```yaml
# timeplusd.yaml
python_requirements:
url: https://my-bucket.s3.us-west-2.amazonaws.com/proton/requirements.txt
poll_interval_sec: 300 # re-check for changes; 0 = startup only
```

Each node fetches the file on startup and then every `poll_interval_sec`, installing anything missing — so edits roll out without a restart. The reconcile only *installs*: removing a line does not uninstall the package, use `SYSTEM UNINSTALL PYTHON PACKAGE` for that. Pin exact versions so all nodes converge. Full options are documented in [Python UDF](/py-udf#python_requirements).

## Compatibility
- Proton/Enterprise 3.0+: Use these SQL commands. This is the only supported method in 3.0+.
- Enterprise 3.1.2+: `REQUIREMENTS`, `INDEX_URL` and `EXTRA_INDEX_URL` clauses. A statement using any of them is dispatched to the cluster with a newer request format, so every node must be on 3.1.2 or later.
- Enterprise 3.3.1+: The embedded runtime is Python 3.14 free-threaded and ships no third-party packages; `python_requirements` is available as a declarative alternative.
- Enterprise 2.x: Use REST API or `timeplusd python -m pip` (see /py-udf#install_lib). These legacy methods are not supported on 3.0+.