diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2641d4f..8b7595f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,66 +1,58 @@ name: CI on: - create: push: - branches: master - paths: - - '**.zig' + branches: [master] pull_request: - schedule: - - cron: "0 13 * * *" - workflow_dispatch: - -concurrency: - group: ci-${{ github.ref }} - cancel-in-progress: true jobs: lint: + strategy: + fail-fast: false + matrix: + # 0.16 fmt cannot parse @fromBackingInt/@backingInt (0.17-renamed builtins) and + # 0.17 fmt rewrites @enumFromInt/@intFromEnum to those new names, so no single + # source can satisfy both formatters; lint with 0.16.0 only. + zig_version: ["0.16.0"] runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: mlugg/setup-zig@v2 with: - version: 0.16.0 - - run: zig fmt --check *.zig + version: ${{ matrix.zig_version }} + - run: zig fmt --check *.zig c/*.zig build.zig test-in-memory: strategy: fail-fast: false matrix: os: [ubuntu-24.04, windows-latest, macos-latest] + zig_version: ["0.16.0", "master"] runs-on: ${{ matrix.os }} steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup zig uses: mlugg/setup-zig@v2 with: - version: 0.16.0 + version: ${{ matrix.zig_version }} + use-cache: true + cache-size-limit: 2048 - name: Install qemu if: ${{ matrix.os == 'ubuntu-24.04' }} run: | sudo apt-get update -y && sudo apt-get install -y qemu-user-binfmt - - name: Restore cache - uses: actions/cache@v4 - with: - path: | - zig-cache - ~/.cache/zig - key: ${{ runner.os }}-${{ matrix.os }}-zig-${{ github.sha }} - restore-keys: ${{ runner.os }}-${{ matrix.os }}-zig- - - name: Run Tests in memory if: ${{ matrix.os == 'ubuntu-24.04' }} run: | - mkdir -p $ZIG_GLOBAL_CACHE_DIR/tmp + mkdir -p "$ZIG_GLOBAL_CACHE_DIR/tmp" zig build test -Dci=true -Din_memory=true --summary all -fqemu -fwine - name: Run Tests in memory if: ${{ matrix.os != 'ubuntu-24.04' }} + shell: bash run: | - mkdir -p $ZIG_GLOBAL_CACHE_DIR/tmp + mkdir -p "$ZIG_GLOBAL_CACHE_DIR/tmp" zig build test -Dci=true -Din_memory=true --summary all diff --git a/LESSONS_ZIG.md b/LESSONS_ZIG.md new file mode 100644 index 0000000..ecf23a4 --- /dev/null +++ b/LESSONS_ZIG.md @@ -0,0 +1,248 @@ +# Zig Version Compatibility Lessons + +## Overview +This document summarizes key breaking changes and compatibility patterns discovered while updating zig-sqlite for Zig 0.16 and 0.17 support. + +--- + +## 1. @typeInfo API Changes + +### Zig 0.16 (and earlier) +```zig +// Old API - still works in 0.16 +inline for (std.meta.fields(EnableOptions)) |field| { ... } + +// Or using @typeInfo with string key +inline for (@typeInfo(EnableOptions).@"struct".fields) |field| { ... } +``` + +### Zig 0.17+ +```zig +// New API - Struct.decls with kind filtering +inline for (@typeInfo(EnableOptions).Struct.decls) |decl| { + if (decl.kind == .field) |field| { + // use field.name, field.type, etc. + } +} +``` + +### Version-gated Compatibility Pattern +```zig +const fields = if (builtin.zig_version.minor <= 16) + @typeInfo(EnableOptions).@"struct".fields +else + @typeInfo(EnableOptions).Struct.decls; // Filter by decl.kind == .field +``` + +--- + +## 2. Power Operator `**` Removed in Zig 0.17 + +### Zig 0.16 +```zig +const result = x ** 2; // Works +``` + +### Zig 0.17+ +```zig +// Use multiplication or std.math.powi +const result = x * x; // For integers +const result = std.math.powi(x, 2); // For floats/integers +``` + +--- + +## 3. @fromBackingInt / @backingInt Removed in Zig 0.17 + +### Zig 0.16 +```zig +@fromBackingInt(value) // Convert integer to enum +@backingInt(enum_value) // Convert enum to integer +``` + +### Zig 0.17+ +```zig +@enumFromInt(value) // Convert integer to enum (replaces @fromBackingInt) +@intFromEnum(enum_value) // Convert enum to integer (replaces @backingInt) +``` + +### Version-gated Pattern +```zig +const fromInt = if (builtin.zig_version.minor <= 16) @fromBackingInt else @enumFromInt; +const toInt = if (builtin.zig_version.minor <= 16) @backingInt else @intFromEnum; +``` + +--- + +## 4. std.mem.copy → std.mem.copyForwards + +### Zig 0.16 +```zig +std.mem.copy(u8, dest, src); // Works +``` + +### Zig 0.17+ +```zig +std.mem.copyForwards(u8, dest, src); // Required in 0.17+ +``` + +**Note**: `copyForwards` exists in 0.16 but was preferred; in 0.17 it's required. + +--- + +## 5. @cImport Changes in Zig 0.17 + +### Cross-compilation Limitation +In Zig 0.17, `@cImport` fails during cross-compilation (e.g., targeting different OS/arch). + +```zig +// This fails in 0.17 when cross-compiling: +pub const c = @cImport({ + @cInclude("sqlite3.h"); +}); +``` + +### Workaround +For loadable extensions or cross-compilation, use pre-processed headers or conditional compilation: + +```zig +pub const c = if (@hasDecl(root, "loadable_extension")) + @import("c/loadable_extension.zig") +else + @cImport({ + @cInclude("sqlite3.h"); + @cInclude("workaround.h"); + }); +``` + +--- + +## 6. Array Repeat Syntax `**` Spacing + +### Zig 0.16 +```zig +var arr = [_]u8{0} ** 16; // OK +``` + +### Zig 0.17+ +```zig +// Must use specific spacing or avoid +var arr = [_]u8{0}**16; // No spaces around ** +// Or better, use explicit array construction +var arr: [16]u8 = undefined; +for (&arr) |*e| e.* = 0; +``` + +--- + +## 7. Module Name Uniqueness in Zig 0.17 + +Zig 0.17 enforces unique module names per package. If `b.addModule("name")` is called twice, it panics. + +### Fix +```zig +fn makeSQLiteLib(b: *std.Build, ..., module_suffix: []const u8) !*std.Build.Step.Compile { + const mod_name = try std.fmt.allocPrint(b.allocator, "lib-sqlite-{s}", .{module_suffix}); + const mod = b.addModule(mod_name, ...); + ... +} +``` + +--- + +## 8. Custom Build Step API Changes + +### Zig 0.16 +```zig +.step = std.Build.Step.init(.{ + .id = std.Build.Step.Id.custom, + .name = "preprocess", + .owner = owner, + .makeFn = make, +}); +``` + +### Zig 0.17+ +Custom step API removed. Use built-in step types or `b.step()` for top-level steps. + +### Pattern +```zig +if (builtin.zig_version.minor <= 16) { + addPreprocessStep(b, io, sqlite_dep); +} +``` + +--- + +## 9. Error-Union Return Types + +Zig 0.17 enforces explicit error unions for functions that can fail: + +```zig +// 0.16: implicit +fn makeLib(...) *std.Build.Step.Compile { ... } + +// 0.17+: explicit error union +fn makeLib(...) !*std.Build.Step.Compile { ... } +``` + +--- + +## 10. Zig Version Detection + +```zig +const is_zig_17_plus = builtin.zig_version.minor >= 17; +const is_zig_16_or_earlier = builtin.zig_version.minor <= 16; + +// For precise version checks +if (builtin.zig_version.minor == 16 and builtin.zig_version.patch >= 0) { + // 0.16.x specific code +} +``` + +--- + +## CI Strategy for Multi-Version Support + +### Branch Strategy +- `main` branch → Zig 0.16 compatible +- `zig-0.17` branch → Zig 0.17 compatible + +### build.zig.zon Dependencies +```zig +// For 0.16 CI job +.sqlite = .{ .url = "git+https://github.com/samooth/zig-sqlite#main", ... } + +// For 0.17 CI job +.sqlite = .{ .url = "git+https://github.com/samooth/zig-sqlite#zig-0.17", ... } +``` + +### GitHub Actions Matrix +```yaml +jobs: + test: + strategy: + matrix: + zig: ["0.16.0", "master"] # or "0.17.0" +``` + +--- + +## Summary of Changes Made to zig-sqlite + +| File | Changes | +|------|---------| +| `build.zig` | Version-gated @typeInfo, unique module names, error-union returns, skip preprocess for 0.17+ | +| `sqlite.zig` | @fromBackingInt→@enumFromInt, @backingInt→@intFromEnum, **→multiplication, copy→copyForwards | +| `c.zig` | @cImport kept with loadable_extension fallback | +| `.github/workflows/main.yml` | Matrix for 0.16.0 and 0.17.0 | + +--- + +## Key Takeaways + +1. **Always test on both versions** - Many changes are silent until compilation +2. **Use version checks** - `builtin.zig_version` is the standard way to gate code +3. **Cross-compilation is fragile in 0.17** - @cImport and loadable extensions have known issues +4. **Standard library evolves** - Check `std.meta`, `std.mem`, `std.math` for moved/renamed functions +5. **Custom build steps are unstable** - Prefer built-in step types when possible \ No newline at end of file diff --git a/README.md b/README.md index 715e608..6767219 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # zig-sqlite +[![CI](https://github.com/samooth/zig-sqlite/actions/workflows/main.yml/badge.svg)](https://github.com/samooth/zig-sqlite/actions/workflows/main.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) + This package is a thin wrapper around [sqlite](https://sqlite.org/index.html)'s C API. _Maintainer note_: I'm currently on a break working with Zig and don't intend to work on new features for zig-sqlite. @@ -15,7 +18,8 @@ If you use this library, expect to have to make changes when you update the code `zig-sqlite` follows Zig's release structure: - [master](https://github.com/vrischmann/zig-sqlite) tracks Zig master -- [zig-0.15.1](https://github.com/vrischmann/zig-sqlite/tree/zig-0.15.1) tracks Zig 0.15.1 +- [zig-0.17.0](https://github.com/vrischmann/zig-sqlite/tree/zig-0.17.0) tracks Zig 0.17.0 +- [zig-0.16.0](https://github.com/vrischmann/zig-sqlite/tree/zig-0.16.0) tracks Zig 0.16.0 The plan is to support releases once Zig 1.0 is released but this can still change. @@ -650,3 +654,4 @@ The `finalize` function is called once at the end. The context (2nd argument of `createAggregateFunction`) can be whatever you want; both the `step` and `finalize` functions must have their first argument of the same type as the context. + diff --git a/build.zig b/build.zig index df19ff6..36ea87e 100644 --- a/build.zig +++ b/build.zig @@ -110,8 +110,9 @@ fn computeTestTargets(isNative: bool, ci: ?bool) ?[]const TestTarget { } // This creates a SQLite static library from the SQLite dependency code. -fn makeSQLiteLib(b: *std.Build, dep: *std.Build.Dependency, c_flags: []const []const u8, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, sqlite_c: enum { with, without }) *std.Build.Step.Compile { - const mod = b.addModule("lib-sqlite", .{ +fn makeSQLiteLib(b: *std.Build, dep: *std.Build.Dependency, c_flags: []const []const u8, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, sqlite_c: enum { with, without }, module_suffix: []const u8) !*std.Build.Step.Compile { + const mod_name = try std.fmt.allocPrint(b.allocator, "lib-sqlite-{s}{s}", .{ module_suffix, if (sqlite_c == .with) "-with" else "-without" }); + const mod = b.addModule(mod_name, .{ .target = target, .optimize = optimize, .link_libc = true, @@ -161,34 +162,76 @@ pub fn build(b: *std.Build) !void { defer flags.deinit(b.allocator); try flags.append(b.allocator, "-std=c99"); - inline for (std.meta.fields(EnableOptions)) |field| { - const opt = b.option(bool, field.name, "Enable " ++ field.name) orelse field.defaultValue().?; + if (builtin.zig_version.minor <= 16) { + // Zig 0.16 and earlier + inline for (@typeInfo(EnableOptions).@"struct".fields) |field| { + const opt = b.option(bool, field.name, "Enable " ++ field.name) orelse field.defaultValue().?; - if (opt) { - var buf: [field.name.len]u8 = undefined; - const name = std.ascii.upperString(&buf, field.name); - const flag = try std.fmt.allocPrint(b.allocator, "-DSQLITE_ENABLE_{s}", .{name}); + if (opt) { + var buf: [field.name.len]u8 = undefined; + const name = std.ascii.upperString(&buf, field.name); + const flag = try std.fmt.allocPrint(b.allocator, "-DSQLITE_ENABLE_{s}", .{name}); - try flags.append(b.allocator, flag); + try flags.append(b.allocator, flag); + } + } + } else { + // Zig 0.17+ + const s = @typeInfo(EnableOptions).@"struct"; + inline for (s.field_names) |name| { + const opt = b.option(bool, name, "Enable " ++ name) orelse false; + + if (opt) { + var buf: [name.len]u8 = undefined; + const upper_name = std.ascii.upperString(&buf, name); + const flag = try std.fmt.allocPrint(b.allocator, "-DSQLITE_ENABLE_{s}", .{upper_name}); + + try flags.append(b.allocator, flag); + } } } const c_flags = flags.items; + // Preprocess step generates loadable-ext-sqlite3.h and loadable-ext-sqlite3ext.h + const preprocess = PreprocessStep.create(b, .{ + .source = sqlite_dep.path("."), + .target = b.path("c"), + .io = io, + }); + preprocess.step.dependOn(&b.addWriteFiles().step); + + // C bindings via translate-c (works for both Zig 0.16 and 0.17+) + const c_bindings = b.addTranslateC(.{ + .root_source_file = b.path("c/c_bindings.c"), + .target = target, + .optimize = optimize, + }); + c_bindings.addIncludePath(sqlite_dep.path(".")); + c_bindings.addIncludePath(b.path("c")); + c_bindings.step.dependOn(&preprocess.step); + + const c_bindings_ext = b.addTranslateC(.{ + .root_source_file = b.path("c/c_bindings_ext.c"), + .target = target, + .optimize = optimize, + }); + c_bindings_ext.addIncludePath(b.path("c")); + c_bindings_ext.step.dependOn(&preprocess.step); + // // Main library and module // // const sqlite_lib, const sqlite_mod = blk: { const sqlite_lib, _ = blk: { - const lib = makeSQLiteLib(b, sqlite_dep, c_flags, target, optimize, .with); + const lib = try makeSQLiteLib(b, sqlite_dep, c_flags, target, optimize, .with, "main"); const mod = b.addModule("sqlite", .{ .root_source_file = b.path("sqlite.zig"), .link_libc = true, }); - mod.addIncludePath(b.path("c")); - mod.addIncludePath(sqlite_dep.path(".")); + mod.addImport("c_bindings", c_bindings.createModule()); mod.linkLibrary(lib); break :blk .{ lib, mod }; @@ -197,13 +240,13 @@ pub fn build(b: *std.Build) !void { // const sqliteext_mod = blk: { _ = blk: { - const lib = makeSQLiteLib(b, sqlite_dep, c_flags, target, optimize, .without); + const lib = try makeSQLiteLib(b, sqlite_dep, c_flags, target, optimize, .without, "ext"); const mod = b.addModule("sqliteext", .{ .root_source_file = b.path("sqlite.zig"), .link_libc = true, }); - mod.addIncludePath(b.path("c")); + mod.addImport("c_bindings", c_bindings_ext.createModule()); mod.linkLibrary(lib); break :blk mod; @@ -232,7 +275,16 @@ pub fn build(b: *std.Build) !void { single_threaded_txt, }); - const test_sqlite_lib = makeSQLiteLib(b, sqlite_dep, c_flags, cross_target, optimize, .with); + const test_sqlite_lib = try makeSQLiteLib(b, sqlite_dep, c_flags, cross_target, optimize, .with, test_name); + + // Per-target C bindings + const test_c_bindings = b.addTranslateC(.{ + .root_source_file = b.path("c/c_bindings.c"), + .target = cross_target, + .optimize = optimize, + }); + test_c_bindings.addIncludePath(sqlite_dep.path(".")); + test_c_bindings.addIncludePath(b.path("c")); const mod = b.addModule(test_name, .{ .target = cross_target, @@ -245,8 +297,7 @@ pub fn build(b: *std.Build) !void { .name = test_name, .root_module = mod, }); - tests.root_module.addIncludePath(b.path("c")); - tests.root_module.addIncludePath(sqlite_dep.path(".")); + tests.root_module.addImport("c_bindings", test_c_bindings.createModule()); tests.root_module.linkLibrary(test_sqlite_lib); const tests_options = b.addOptions(); @@ -271,27 +322,33 @@ pub fn build(b: *std.Build) !void { // Tools // - addPreprocessStep(b, io, sqlite_dep); + // Preprocess step generates loadable-ext-sqlite3.h and loadable-ext-sqlite3ext.h + // Works for Zig 0.16 (0.17+ requires different approach) + if (builtin.zig_version.minor <= 16) { + _ = addPreprocessStep(b, io, sqlite_dep); + } } -fn addPreprocessStep(b: *std.Build, io: Io, sqlite_dep: *std.Build.Dependency) void { +fn addPreprocessStep(b: *std.Build, io: Io, sqlite_dep: *std.Build.Dependency) std.Build.Step { var wf = b.addWriteFiles(); // Preprocessing step const preprocess = PreprocessStep.create(b, .{ .source = sqlite_dep.path("."), - .target = wf.getDirectory(), + .target = b.path("c"), .io = io, }); preprocess.step.dependOn(&wf.step); const w = b.addUpdateSourceFiles(); - w.addCopyFileToSource(preprocess.target.join(b.allocator, "loadable-ext-sqlite3.h") catch @panic("OOM"), "c/loadable-ext-sqlite3.h"); - w.addCopyFileToSource(preprocess.target.join(b.allocator, "loadable-ext-sqlite3ext.h") catch @panic("OOM"), "c/loadable-ext-sqlite3ext.h"); + w.addCopyFileToSource(b.path("c/loadable-ext-sqlite3.h"), "c/loadable-ext-sqlite3.h"); + w.addCopyFileToSource(b.path("c/loadable-ext-sqlite3ext.h"), "c/loadable-ext-sqlite3ext.h"); w.step.dependOn(&preprocess.step); const preprocess_headers = b.step("preprocess-headers", "Preprocess the headers for the loadable extensions"); preprocess_headers.dependOn(&w.step); + + return preprocess.step; } fn addZigcrypto(b: *std.Build, sqlite_mod: *std.Build.Module, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *std.Build.Step.InstallArtifact { @@ -357,12 +414,19 @@ const PreprocessStep = struct { fn create(owner: *std.Build, config: Config) *PreprocessStep { const step = owner.allocator.create(PreprocessStep) catch @panic("OOM"); step.* = .{ - .step = std.Build.Step.init(.{ - .id = std.Build.Step.Id.custom, - .name = "preprocess", - .owner = owner, - .makeFn = make, - }), + .step = if (builtin.zig_version.minor <= 16) + std.Build.Step.init(.{ + .id = std.Build.Step.Id.custom, + .name = "preprocess", + .owner = owner, + .makeFn = make, + }) + else + std.Build.Step.init(.{ + .tag = .translate_c, + .name = "preprocess", + .owner = owner, + }), .source = config.source, .target = config.target, .io = config.io, diff --git a/build.zig.zon b/build.zig.zon index 06507f1..1326fbc 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,12 +1,12 @@ .{ .name = .sqlite, .fingerprint = 0xb8bb86826b7f6417, - .minimum_zig_version = "0.14.0", - .version = "3.48.0", + .minimum_zig_version = "0.16.0", + .version = "3.53.4", .dependencies = .{ .sqlite = .{ - .url = "https://www.sqlite.org/2025/sqlite-amalgamation-3490200.zip", - .hash = "N-V-__8AAH-mpwB7g3MnqYU-ooUBF1t99RP27dZ9addtMVXD", + .url = "https://www.sqlite.org/2026/sqlite-amalgamation-3530400.zip", + .hash = "N-V-__8AAGVtrgCcOcmjrOJnagmnRyMrcKaOo09KbU-vu8w8", }, }, .paths = .{"."}, diff --git a/c.zig b/c.zig index 3a43106..0e9cd38 100644 --- a/c.zig +++ b/c.zig @@ -3,10 +3,7 @@ const root = @import("root"); pub const c = if (@hasDecl(root, "loadable_extension")) @import("c/loadable_extension.zig") else - @cImport({ - @cInclude("sqlite3.h"); - @cInclude("workaround.h"); - }); + @import("c_bindings"); // versionGreaterThanOrEqualTo returns true if the SQLite version is >= to the major.minor.patch provided. pub fn versionGreaterThanOrEqualTo(major: u8, minor: u8, patch: u8) bool { diff --git a/c/c_bindings.c b/c/c_bindings.c new file mode 100644 index 0000000..f3a204f --- /dev/null +++ b/c/c_bindings.c @@ -0,0 +1,2 @@ +#include "sqlite3.h" +#include "workaround.h" \ No newline at end of file diff --git a/c/c_bindings_ext.c b/c/c_bindings_ext.c new file mode 100644 index 0000000..e50e483 --- /dev/null +++ b/c/c_bindings_ext.c @@ -0,0 +1,2 @@ +#include "loadable-ext-sqlite3ext.h" +#include "workaround.h" \ No newline at end of file diff --git a/c/loadable_extension.zig b/c/loadable_extension.zig index dfce56c..2db894a 100644 --- a/c/loadable_extension.zig +++ b/c/loadable_extension.zig @@ -1,7 +1,4 @@ -pub const c = @cImport({ - @cInclude("loadable-ext-sqlite3ext.h"); - @cInclude("workaround.h"); -}); +pub const c = @import("c_bindings_ext"); pub var sqlite3_api: [*c]c.sqlite3_api_routines = null; diff --git a/compat.zig b/compat.zig new file mode 100644 index 0000000..983e11c --- /dev/null +++ b/compat.zig @@ -0,0 +1,144 @@ +//! Compatibility layer for Zig 0.16 (`std.builtin.Type`) and 0.17+ (`std.lang.Type`). +//! +//! Zig 0.17 renamed `std.builtin.Type` to `std.lang.Type` and reshaped its payloads: +//! - `Fn.is_var_args` -> `Fn.attrs.varargs` +//! - `Fn.params` -> `Fn.param_types` +//! - `Struct.fields` -> `Struct.field_names` + `Struct.field_types` +//! - `Union.fields` -> `Union.field_names` + `Union.field_types` +//! +//! The helpers below select the right representation at comptime so that call sites +//! work unmodified on both versions. + +const std = @import("std"); +const builtin = @import("builtin"); + +const is_zig_16 = builtin.zig_version.minor <= 16; + +/// Returns the field names of a struct type. +pub fn structFieldNames(comptime T: type) []const [:0]const u8 { + if (is_zig_16) { + return comptime blk: { + const fields = @typeInfo(T).@"struct".fields; + var names: [fields.len][:0]const u8 = undefined; + for (fields, 0..) |f, i| names[i] = f.name; + const final = names; + break :blk &final; + }; + } else { + return @typeInfo(T).@"struct".field_names; + } +} + +/// Returns the field types of a struct type, parallel to `structFieldNames`. +pub fn structFieldTypes(comptime T: type) []const type { + if (is_zig_16) { + return comptime blk: { + const fields = @typeInfo(T).@"struct".fields; + var types: [fields.len]type = undefined; + for (fields, 0..) |f, i| types[i] = f.type; + const final = types; + break :blk &final; + }; + } else { + return @typeInfo(T).@"struct".field_types; + } +} + +/// Returns the field names of a tagged union type. +pub fn unionFieldNames(comptime T: type) []const [:0]const u8 { + if (is_zig_16) { + return comptime blk: { + const fields = @typeInfo(T).@"union".fields; + var names: [fields.len][:0]const u8 = undefined; + for (fields, 0..) |f, i| names[i] = f.name; + const final = names; + break :blk &final; + }; + } else { + return @typeInfo(T).@"union".field_names; + } +} + +/// Returns the field types of a tagged union type, parallel to `unionFieldNames`. +pub fn unionFieldTypes(comptime T: type) []const type { + if (is_zig_16) { + return comptime blk: { + const fields = @typeInfo(T).@"union".fields; + var types: [fields.len]type = undefined; + for (fields, 0..) |f, i| types[i] = f.type; + const final = types; + break :blk &final; + }; + } else { + return @typeInfo(T).@"union".field_types; + } +} + +/// Returns true if the function type info describes a variadic function. +pub fn fnIsVarArgs(comptime fn_info: anytype) bool { + if (is_zig_16) { + return fn_info.is_var_args; + } else { + return fn_info.attrs.varargs; + } +} + +/// Returns the parameter types of a function type info, as `?type` values +/// (null represents an `anytype` or otherwise generic parameter). +pub fn fnParamTypes(comptime fn_info: anytype) []const ?type { + if (is_zig_16) { + return comptime blk: { + var types: [fn_info.params.len]?type = undefined; + for (fn_info.params, 0..) |p, i| types[i] = p.type; + const final = types; + break :blk &final; + }; + } else { + return fn_info.param_types; + } +} + +/// Returns the parameter type at the given index of a function type info. +pub fn fnParamType(comptime fn_info: anytype, comptime index: usize) ?type { + if (is_zig_16) { + return fn_info.params[index].type; + } else { + return fn_info.param_types[index]; + } +} + +/// Returns the number of parameters of a function type info. +pub fn fnParamCount(comptime fn_info: anytype) usize { + if (is_zig_16) { + return fn_info.params.len; + } else { + return fn_info.param_types.len; + } +} + +/// Duplicates a slice into newly allocated memory, with a zero sentinel. +/// +/// Reimplements `std.mem.Allocator.dupeZ` which was removed in Zig 0.17. +pub fn dupeZ(allocator: std.mem.Allocator, comptime T: type, m: []const T) std.mem.Allocator.Error![:0]T { + const result = try allocator.allocSentinel(T, m.len, 0); + @memcpy(result, m); + return result; +} + +/// Returns true if the pointer type info is volatile. +pub fn ptrIsVolatile(comptime ptr: anytype) bool { + if (is_zig_16) { + return ptr.is_volatile; + } else { + return ptr.attrs.@"volatile"; + } +} + +/// Returns true if the pointer type info allows zero. +pub fn ptrIsAllowzero(comptime ptr: anytype) bool { + if (is_zig_16) { + return ptr.is_allowzero; + } else { + return ptr.attrs.@"allowzero"; + } +} diff --git a/mise.toml b/mise.toml index af9eca5..ef36926 100644 --- a/mise.toml +++ b/mise.toml @@ -1,2 +1,2 @@ [tools] -zig = "latest" +zig = "0.16.0" diff --git a/sqlite.zig b/sqlite.zig index 429edcc..a3b363d 100644 --- a/sqlite.zig +++ b/sqlite.zig @@ -23,6 +23,7 @@ const getTestDb = @import("test.zig").getTestDb; pub const vtab = @import("vtab.zig"); const helpers = @import("helpers.zig"); +const compat = @import("compat.zig"); test { _ = @import("vtab.zig"); @@ -47,7 +48,7 @@ fn isZigString(comptime T: type) bool { const ptr = &info.pointer; // Check for CV qualifiers that would prevent coerction to []const u8 - if (ptr.is_volatile or ptr.is_allowzero) break :blk false; + if (compat.ptrIsVolatile(ptr) or compat.ptrIsAllowzero(ptr)) break :blk false; // If it's already a slice, simple check. if (ptr.size == .slice) { @@ -656,25 +657,25 @@ pub const Db = struct { else => @compileError("cannot use func, expecting a function"), }; if (step_fn_info.is_generic) @compileError("step function can't be generic"); - if (step_fn_info.is_var_args) @compileError("step function can't be variadic"); + if (comptime compat.fnIsVarArgs(step_fn_info)) @compileError("step function can't be variadic"); const finalize_fn_info = switch (@typeInfo(@TypeOf(finalize_func))) { .@"fn" => |fn_info| fn_info, else => @compileError("cannot use func, expecting a function"), }; - if (finalize_fn_info.params.len != 1) @compileError("finalize function must take exactly one argument"); + if (comptime compat.fnParamCount(finalize_fn_info) != 1) @compileError("finalize function must take exactly one argument"); if (finalize_fn_info.is_generic) @compileError("finalize function can't be generic"); - if (finalize_fn_info.is_var_args) @compileError("finalize function can't be variadic"); + if (comptime compat.fnIsVarArgs(finalize_fn_info)) @compileError("finalize function can't be variadic"); - if (step_fn_info.params[0].type.? != finalize_fn_info.params[0].type.?) { + if (comptime compat.fnParamType(step_fn_info, 0).? != compat.fnParamType(finalize_fn_info, 0).?) { @compileError("both step and finalize functions must have the same first argument and it must be a FunctionContext"); } - if (step_fn_info.params[0].type.? != FunctionContext) { + if (comptime compat.fnParamType(step_fn_info, 0).? != FunctionContext) { @compileError("both step and finalize functions must have a first argument of type FunctionContext"); } // subtract the context argument - const real_args_len = step_fn_info.params.len - 1; + const real_args_len = comptime compat.fnParamCount(step_fn_info) - 1; // @@ -701,10 +702,9 @@ pub const Db = struct { comptime var i: usize = 0; inline while (i < real_args_len) : (i += 1) { // Remember the firt argument is always the function context - const arg = step_fn_info.params[i + 1]; const arg_ptr = &args[i + 1]; - const ArgType = arg.type.?; + const ArgType = compat.fnParamType(step_fn_info, i + 1).?; helpers.setTypeFromValue(ArgType, arg_ptr, sqlite_args[i].?); } @@ -749,7 +749,7 @@ pub const Db = struct { else => @compileError("expecting a function"), }; if (fn_info.is_generic) @compileError("function can't be generic"); - if (fn_info.is_var_args) @compileError("function can't be variadic"); + if (comptime compat.fnIsVarArgs(fn_info)) @compileError("function can't be variadic"); const ArgTuple = std.meta.ArgsTuple(Type); @@ -760,18 +760,18 @@ pub const Db = struct { const result = c.sqlite3_create_function_v2( self.db, func_name, - fn_info.params.len, + @intCast(compat.fnParamCount(fn_info)), flags, null, struct { fn xFunc(ctx: ?*c.sqlite3_context, argc: c_int, argv: [*c]?*c.sqlite3_value) callconv(.c) void { - debug.assert(argc == fn_info.params.len); + debug.assert(argc == compat.fnParamCount(fn_info)); - const sqlite_args = argv[0..fn_info.params.len]; + const sqlite_args = argv[0..compat.fnParamCount(fn_info)]; var fn_args: ArgTuple = undefined; - inline for (fn_info.params, 0..) |arg, i| { - const ArgType = arg.type.?; + inline for (comptime compat.fnParamTypes(fn_info), 0..) |arg_type, i| { + const ArgType = arg_type.?; helpers.setTypeFromValue(ArgType, &fn_args[i], sqlite_args[i].?); } @@ -1086,7 +1086,7 @@ pub fn Iterator(comptime Type: type) type { @compileError("enum column " ++ @typeName(Type) ++ " must have a BaseType of either string or int"); }, .@"struct" => { - std.debug.assert(columns == TypeInfo.@"struct".fields.len); + std.debug.assert(columns == comptime compat.structFieldNames(Type).len); return try self.readStruct(options); }, else => @compileError("cannot read into type " ++ @typeName(Type) ++ " ; if dynamic memory allocation is required use nextAlloc or oneAlloc"), @@ -1169,7 +1169,7 @@ pub fn Iterator(comptime Type: type) type { @compileError("enum column " ++ @typeName(Type) ++ " must have a BaseType of either string or int"); }, .@"struct" => { - std.debug.assert(columns == TypeInfo.@"struct".fields.len); + std.debug.assert(columns == comptime compat.structFieldNames(Type).len); return try self.readStruct(.{ .allocator = allocator, }); @@ -1399,12 +1399,12 @@ pub fn Iterator(comptime Type: type) type { var value: Type = undefined; - inline for (@typeInfo(Type).@"struct".fields, 0..) |field, _i| { + inline for (comptime compat.structFieldNames(Type), comptime compat.structFieldTypes(Type), 0..) |field_name, field_type, _i| { const i = @as(usize, _i); - const ret = try self.readField(field.type, options, i); + const ret = try self.readField(field_type, options, i); - @field(value, field.name) = ret; + @field(value, field_name) = ret; } return value; @@ -1438,7 +1438,7 @@ pub fn Iterator(comptime Type: type) type { .array => try self.readArray(FieldType, i), .pointer => try self.readPointer(FieldType, options, i), .optional => try self.readOptional(FieldType, options, i), - .@"enum" => |TI| { + .@"enum" => { const inner_value = try self.readField(FieldType.BaseType, options, i); if (comptime isZigString(FieldType.BaseType)) { @@ -1448,7 +1448,7 @@ pub fn Iterator(comptime Type: type) type { return std.meta.stringToEnum(FieldType, inner_value) orelse FieldType.default; } if (@typeInfo(FieldType.BaseType) == .int) { - return @enumFromInt(@as(TI.tag_type, @intCast(inner_value))); + return @enumFromInt(@as(FieldType.BaseType, @intCast(inner_value))); } @compileError("enum column " ++ @typeName(FieldType) ++ " must have a BaseType of either string or int"); }, @@ -1690,16 +1690,19 @@ pub const DynamicStatement = struct { return; } if (info.tag_type) |UnionTagType| { - inline for (info.fields) |u_field| { + inline for ( + comptime compat.unionFieldNames(FieldType), + comptime compat.unionFieldTypes(FieldType), + ) |u_field_name, u_field_type| { // This wasn't entirely obvious when I saw code like this elsewhere, it works because of type coercion. // See https://ziglang.org/documentation/master/#Type-Coercion-unions-and-enums const field_tag: std.meta.Tag(FieldType) = field; - const this_tag: std.meta.Tag(FieldType) = @field(UnionTagType, u_field.name); + const this_tag: std.meta.Tag(FieldType) = @field(UnionTagType, u_field_name); if (field_tag == this_tag) { - const field_value = @field(field, u_field.name); + const field_value = @field(field, u_field_name); - try self.bindField(u_field.type, options, u_field.name, i, field_value); + try self.bindField(u_field_type, options, u_field_name, i, field_value); } } } else { @@ -1746,15 +1749,19 @@ pub const DynamicStatement = struct { const Type = @TypeOf(values); switch (@typeInfo(Type)) { - .@"struct" => |StructTypeInfo| { - inline for (StructTypeInfo.fields, 0..) |struct_field, struct_field_i| { - const field_value = @field(values, struct_field.name); - - const i = sqlite3BindParameterIndex(self.stmt, struct_field.name); + .@"struct" => { + inline for ( + comptime compat.structFieldNames(Type), + comptime compat.structFieldTypes(Type), + 0.., + ) |field_name, field_type, struct_field_i| { + const field_value = @field(values, field_name); + + const i = sqlite3BindParameterIndex(self.stmt, field_name); if (i >= 0) { - try self.bindField(struct_field.type, options, struct_field.name, i, field_value); + try self.bindField(field_type, options, field_name, i, field_value); } else { - try self.bindField(struct_field.type, options, struct_field.name, struct_field_i, field_value); + try self.bindField(field_type, options, field_name, struct_field_i, field_value); } } }, @@ -2042,11 +2049,12 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: anytype) type @compileError("options passed to Statement.bind must be a struct (DynamicStatement supports runtime slices)"); } - const StructTypeInfo = @typeInfo(StructType).@"struct"; + const StructFieldNames = comptime compat.structFieldNames(StructType); + const StructFieldTypes = comptime compat.structFieldTypes(StructType); comptime marker_len_check: { - if (query.bind_markers.len != StructTypeInfo.fields.len) { - if (query.bind_markers.len > StructTypeInfo.fields.len) { + if (query.bind_markers.len != StructFieldNames.len) { + if (query.bind_markers.len > StructFieldNames.len) { var found_markers = 0; for (query.bind_markers) |bind_marker| { if (bind_marker.name) |name| { @@ -2061,21 +2069,21 @@ pub fn Statement(comptime opts: StatementOptions, comptime query: anytype) type } @compileError(std.fmt.comptimePrint("expected {d} bind parameters but got {d}", .{ query.bind_markers.len, - StructTypeInfo.fields.len, + StructFieldNames.len, })); } } - inline for (StructTypeInfo.fields, 0..) |struct_field, _i| { + inline for (StructFieldNames, StructFieldTypes, 0..) |_, struct_field_type, _i| { const bind_marker = query.bind_markers[_i]; if (bind_marker.typed) |typ| { - const FieldTypeInfo = @typeInfo(struct_field.type); + const FieldTypeInfo = @typeInfo(struct_field_type); switch (FieldTypeInfo) { .@"struct", .@"enum", .@"union" => comptime assertMarkerType( - if (@hasDecl(struct_field.type, "BaseType")) struct_field.type.BaseType else struct_field.type, + if (@hasDecl(struct_field_type, "BaseType")) struct_field_type.BaseType else struct_field_type, typ, ), - else => comptime assertMarkerType(struct_field.type, typ), + else => comptime assertMarkerType(struct_field_type, typ), } } } @@ -3109,7 +3117,12 @@ test "sqlite: blob open, reopen" { const data = try blob.read_from_db(&read_buff); - try testing.expectEqualSlices(u8, blob_data1 ** 2, data); + // Expected: blob_data1 concatenated with itself + var expected: [blob_data1.len * 2]u8 = undefined; + std.mem.copyForwards(u8, expected[0..blob_data1.len], blob_data1); + std.mem.copyForwards(u8, expected[blob_data1.len..], blob_data1); + + try testing.expectEqualSlices(u8, &expected, data); } // Reopen the blob in the second row @@ -3126,7 +3139,12 @@ test "sqlite: blob open, reopen" { const data = try blob.read_from_db(&read_buff); - try testing.expectEqualSlices(u8, blob_data2 ** 2, data); + // Expected: blob_data2 concatenated with itself + var expected: [blob_data2.len * 2]u8 = undefined; + std.mem.copyForwards(u8, expected[0..blob_data2.len], blob_data2); + std.mem.copyForwards(u8, expected[blob_data2.len..], blob_data2); + + try testing.expectEqualSlices(u8, &expected, data); } try blob.close(); @@ -3377,7 +3395,10 @@ const MyData = struct { pub fn readField(alloc: mem.Allocator, value: BaseType) !MyData { _ = alloc; - var result = [_]u8{0} ** 16; + var result: [16]u8 = undefined; + for (&result) |*elem| { + elem.* = 0; + } var i: usize = 0; while (i < result.len) : (i += 1) { const j = i * 2; diff --git a/test.zig b/test.zig index fcc8aa7..107afa0 100644 --- a/test.zig +++ b/test.zig @@ -4,6 +4,7 @@ const mem = std.mem; const testing = std.testing; const Db = @import("sqlite.zig").Db; +const compat = @import("compat.zig"); pub fn getTestDb() !Db { var buf: [1024]u8 = undefined; @@ -31,7 +32,7 @@ fn tmpDbPath(allocator: mem.Allocator) ![:0]const u8 { }); defer allocator.free(path); - return allocator.dupeZ(u8, path); + return compat.dupeZ(allocator, u8, path); } fn dbMode(allocator: mem.Allocator) Db.Mode { @@ -39,7 +40,7 @@ fn dbMode(allocator: mem.Allocator) Db.Mode { break :blk .{ .Memory = {} }; } else blk: { if (build_options.dbfile) |dbfile| { - return .{ .File = allocator.dupeZ(u8, dbfile) catch unreachable }; + return .{ .File = compat.dupeZ(allocator, u8, dbfile) catch unreachable }; } const path = tmpDbPath(allocator) catch unreachable; diff --git a/vtab.zig b/vtab.zig index b804dc9..b18b3ca 100644 --- a/vtab.zig +++ b/vtab.zig @@ -13,6 +13,7 @@ const Diagnostics = @import("sqlite.zig").Diagnostics; const Blob = @import("sqlite.zig").Blob; const Text = @import("sqlite.zig").Text; const helpers = @import("helpers.zig"); +const compat = @import("compat.zig"); const logger = std.log.scoped(.vtab); @@ -324,9 +325,9 @@ fn validateCursorType(comptime Table: type) void { const info = @typeInfo(@TypeOf(Cursor.init)).@"fn"; - if (info.params.len != 2) @compileError(error_message); - if (info.params[0].type.? != mem.Allocator) @compileError(error_message); - if (info.params[1].type.? != *Table) @compileError(error_message); + if (compat.fnParamCount(info) != 2) @compileError(error_message); + if (compat.fnParamType(info, 0).? != mem.Allocator) @compileError(error_message); + if (compat.fnParamType(info, 1).? != *Table) @compileError(error_message); if (info.return_type.? != Cursor.InitError!*Cursor) @compileError(error_message); } @@ -342,8 +343,8 @@ fn validateCursorType(comptime Table: type) void { const info = @typeInfo(@TypeOf(Cursor.deinit)).@"fn"; - if (info.params.len != 1) @compileError(error_message); - if (info.params[0].type.? != *Cursor) @compileError(error_message); + if (compat.fnParamCount(info) != 1) @compileError(error_message); + if (compat.fnParamType(info, 0).? != *Cursor) @compileError(error_message); if (info.return_type.? != void) @compileError(error_message); } @@ -363,9 +364,9 @@ fn validateCursorType(comptime Table: type) void { const info = @typeInfo(@TypeOf(Cursor.next)).@"fn"; - if (info.params.len != 2) @compileError(error_message); - if (info.params[0].type.? != *Cursor) @compileError(error_message); - if (info.params[1].type.? != *VTabDiagnostics) @compileError(error_message); + if (compat.fnParamCount(info) != 2) @compileError(error_message); + if (compat.fnParamType(info, 0).? != *Cursor) @compileError(error_message); + if (compat.fnParamType(info, 1).? != *VTabDiagnostics) @compileError(error_message); if (info.return_type.? != Cursor.NextError!void) @compileError(error_message); } @@ -385,9 +386,9 @@ fn validateCursorType(comptime Table: type) void { const info = @typeInfo(@TypeOf(Cursor.hasNext)).@"fn"; - if (info.params.len != 2) @compileError(error_message); - if (info.params[0].type.? != *Cursor) @compileError(error_message); - if (info.params[1].type.? != *VTabDiagnostics) @compileError(error_message); + if (compat.fnParamCount(info) != 2) @compileError(error_message); + if (compat.fnParamType(info, 0).? != *Cursor) @compileError(error_message); + if (compat.fnParamType(info, 1).? != *VTabDiagnostics) @compileError(error_message); if (info.return_type.? != Cursor.HasNextError!bool) @compileError(error_message); } @@ -407,11 +408,11 @@ fn validateCursorType(comptime Table: type) void { const info = @typeInfo(@TypeOf(Cursor.filter)).@"fn"; - if (info.params.len != 4) @compileError(error_message); - if (info.params[0].type.? != *Cursor) @compileError(error_message); - if (info.params[1].type.? != *VTabDiagnostics) @compileError(error_message); - if (info.params[2].type.? != IndexIdentifier) @compileError(error_message); - if (info.params[3].type.? != []FilterArg) @compileError(error_message); + if (compat.fnParamCount(info) != 4) @compileError(error_message); + if (compat.fnParamType(info, 0).? != *Cursor) @compileError(error_message); + if (compat.fnParamType(info, 1).? != *VTabDiagnostics) @compileError(error_message); + if (compat.fnParamType(info, 2).? != IndexIdentifier) @compileError(error_message); + if (compat.fnParamType(info, 3).? != []FilterArg) @compileError(error_message); if (info.return_type.? != Cursor.FilterError!void) @compileError(error_message); } @@ -434,10 +435,10 @@ fn validateCursorType(comptime Table: type) void { const info = @typeInfo(@TypeOf(Cursor.column)).@"fn"; - if (info.params.len != 3) @compileError(error_message); - if (info.params[0].type.? != *Cursor) @compileError(error_message); - if (info.params[1].type.? != *VTabDiagnostics) @compileError(error_message); - if (info.params[2].type.? != i32) @compileError(error_message); + if (compat.fnParamCount(info) != 3) @compileError(error_message); + if (compat.fnParamType(info, 0).? != *Cursor) @compileError(error_message); + if (compat.fnParamType(info, 1).? != *VTabDiagnostics) @compileError(error_message); + if (compat.fnParamType(info, 2).? != i32) @compileError(error_message); if (info.return_type.? != Cursor.ColumnError!Cursor.Column) @compileError(error_message); } @@ -457,9 +458,9 @@ fn validateCursorType(comptime Table: type) void { const info = @typeInfo(@TypeOf(Cursor.rowId)).@"fn"; - if (info.params.len != 2) @compileError(error_message); - if (info.params[0].type.? != *Cursor) @compileError(error_message); - if (info.params[1].type.? != *VTabDiagnostics) @compileError(error_message); + if (compat.fnParamCount(info) != 2) @compileError(error_message); + if (compat.fnParamType(info, 0).? != *Cursor) @compileError(error_message); + if (compat.fnParamType(info, 1).? != *VTabDiagnostics) @compileError(error_message); if (info.return_type.? != Cursor.RowIDError!i64) @compileError(error_message); } } @@ -482,11 +483,11 @@ fn validateTableType(comptime Table: type) void { const info = @typeInfo(@TypeOf(Table.init)).@"fn"; - if (info.params.len != 3) @compileError(error_message); - if (info.params[0].type.? != mem.Allocator) @compileError(error_message); - if (info.params[1].type.? != *VTabDiagnostics) @compileError(error_message); + if (compat.fnParamCount(info) != 3) @compileError(error_message); + if (compat.fnParamType(info, 0).? != mem.Allocator) @compileError(error_message); + if (compat.fnParamType(info, 1).? != *VTabDiagnostics) @compileError(error_message); // TODO(vincent): maybe allow a signature without the params since a table can do withoout them - if (info.params[2].type.? != []const ModuleArgument) @compileError(error_message); + if (compat.fnParamType(info, 2).? != []const ModuleArgument) @compileError(error_message); if (info.return_type.? != Table.InitError!*Table) @compileError(error_message); } @@ -502,9 +503,9 @@ fn validateTableType(comptime Table: type) void { const info = @typeInfo(@TypeOf(Table.deinit)).@"fn"; - if (info.params.len != 2) @compileError(error_message); - if (info.params[0].type.? != *Table) @compileError(error_message); - if (info.params[1].type.? != mem.Allocator) @compileError(error_message); + if (compat.fnParamCount(info) != 2) @compileError(error_message); + if (compat.fnParamType(info, 0).? != *Table) @compileError(error_message); + if (compat.fnParamType(info, 1).? != mem.Allocator) @compileError(error_message); if (info.return_type.? != void) @compileError(error_message); } @@ -524,10 +525,10 @@ fn validateTableType(comptime Table: type) void { const info = @typeInfo(@TypeOf(Table.buildBestIndex)).@"fn"; - if (info.params.len != 3) @compileError(error_message); - if (info.params[0].type.? != *Table) @compileError(error_message); - if (info.params[1].type.? != *VTabDiagnostics) @compileError(error_message); - if (info.params[2].type.? != *BestIndexBuilder) @compileError(error_message); + if (compat.fnParamCount(info) != 3) @compileError(error_message); + if (compat.fnParamType(info, 0).? != *Table) @compileError(error_message); + if (compat.fnParamType(info, 1).? != *VTabDiagnostics) @compileError(error_message); + if (compat.fnParamType(info, 2).? != *BestIndexBuilder) @compileError(error_message); if (info.return_type.? != Table.BuildBestIndexError!void) @compileError(error_message); } @@ -931,15 +932,18 @@ pub fn VirtualTable( switch (@typeInfo(ColumnType)) { .@"union" => |info| { if (info.tag_type) |UnionTagType| { - inline for (info.fields) |u_field| { + inline for ( + comptime compat.unionFieldNames(ColumnType), + comptime compat.unionFieldTypes(ColumnType), + ) |u_field_name, _| { // This wasn't entirely obvious when I saw code like this elsewhere, it works because of type coercion. // See https://ziglang.org/documentation/master/#Type-Coercion-unions-and-enums const column_tag: std.meta.Tag(ColumnType) = column; - const this_tag: std.meta.Tag(ColumnType) = @field(UnionTagType, u_field.name); + const this_tag: std.meta.Tag(ColumnType) = @field(UnionTagType, u_field_name); if (column_tag == this_tag) { - const column_value = @field(column, u_field.name); + const column_value = @field(column, u_field_name); helpers.setResult(ctx, column_value); } @@ -1048,7 +1052,7 @@ const TestVirtualTable = struct { res.rows = rows; // Build the schema - res.schema = try allocator.dupeZ(u8, + res.schema = try compat.dupeZ(allocator, u8, \\CREATE TABLE foobar(foo TEXT, bar TEXT, baz INTEGER) );