diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40483e1..95d1da2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,7 @@ jobs: pip install sphinx sphinx_rtd_theme cd docs && python3 -m pip install -r requirements.txt && cd - make clean-docs docs + git diff --exit-code - name: Run SQL tests run: make test diff --git a/.gitignore b/.gitignore index d9058c3..9fc55d8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ vsql-cli vsql-server *.vsql .vscode +.vlang_tmp_build/ diff --git a/Makefile b/Makefile index debaf0c..fc077e8 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,11 @@ BUILD_OPTIONS = PROD = -prod +# Ready is a some quick tasks that can easily be forgotten when preparing a +# diff. + +ready: grammar fmt snippets + # Binaries bin/vsql: @@ -19,7 +24,10 @@ bin/vsql.exe: # Documentation -docs: +snippets: + python3 generate-snippets.py > docs/snippets.rst + +docs: snippets mkdir -p docs/_static cd docs && make html SPHINXOPTS="-W --keep-going -n" diff --git a/README.md b/README.md index 449541f..e2d55e9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ database written in pure [V](https://vlang.io) with zero dependencies. After [installing or updating](https://vsql.readthedocs.io/en/latest/install.html), you can use vsql -[within your V applications](https://vsql.readthedocs.io/en/latest/v-module.html), +[within your V applications](https://vsql.readthedocs.io/en/latest/v-client-library.html), interact with database files using the [CLI](https://vsql.readthedocs.io/en/latest/cli.html), or connect to a database using the @@ -21,7 +21,7 @@ or use one of the quick links: - [FAQ](https://vsql.readthedocs.io/en/latest/faq.html) - [CLI](https://vsql.readthedocs.io/en/latest/cli.html) -- [Supported PostgreSQL clients](https://vsql.readthedocs.io/en/latest/supported-clients.html) +- [Supported PostgreSQL clients](https://vsql.readthedocs.io/en/latest/postgresql-clients.html) - [Features](https://vsql.readthedocs.io/en/latest/features.html) - [SQL Reference](https://vsql.readthedocs.io/en/latest/features.html) - [Contributing](https://vsql.readthedocs.io/en/latest/contributing.html) diff --git a/cmd/vsql/cli.v b/cmd/vsql/cli.v index 65f3539..5db9a9f 100644 --- a/cmd/vsql/cli.v +++ b/cmd/vsql/cli.v @@ -28,17 +28,19 @@ fn cli_command(cmd cli.Command) ? { if query != '' { start := time.ticks() result := db.query(query)? + mut total_rows := 0 for row in result { for column in result.columns { print('$column.name: ${row.get_string(column.name)} ') } + total_rows++ } - if result.rows.len > 0 { + if total_rows > 0 { println('') } - println('$result.rows.len ${vsql.pluralize(result.rows.len, 'row')} (${time.ticks() - start} ms)') + println('$total_rows ${vsql.pluralize(total_rows, 'row')} (${time.ticks() - start} ms)') } println('') diff --git a/docs/cli.rst b/docs/cli.rst index a64f0f7..57944df 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -1,5 +1,5 @@ -CLI -=== +Command Line Interface (CLI) +============================ You can also work with database files through the CLI (ctrl+c to exit): diff --git a/docs/client-interfaces.rst b/docs/client-interfaces.rst new file mode 100644 index 0000000..76fc822 --- /dev/null +++ b/docs/client-interfaces.rst @@ -0,0 +1,9 @@ +Client Interfaces +================= + +.. toctree:: + :maxdepth: 1 + + cli.rst + postgresql-clients.rst + v-client-library.rst diff --git a/docs/getting-started.rst b/docs/getting-started.rst index 6d8906c..2dbeb00 100644 --- a/docs/getting-started.rst +++ b/docs/getting-started.rst @@ -6,6 +6,3 @@ Getting Started install.rst faq.rst - v-module.rst - cli.rst - supported-clients.rst diff --git a/docs/index.rst b/docs/index.rst index 9dd63d0..45935e5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,6 +10,7 @@ Welcome to the vsql documentation. features.rst sql-reference.rst sql-language.rst + client-interfaces.rst internals.rst development.rst appendix.rst diff --git a/docs/install.rst b/docs/install.rst index 21ccf40..5e46042 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -15,4 +15,4 @@ See Also - :doc:`cli` - :doc:`contributing` -- :doc:`v-module` +- :doc:`v-client-library` diff --git a/docs/supported-clients.rst b/docs/postgresql-clients.rst similarity index 97% rename from docs/supported-clients.rst rename to docs/postgresql-clients.rst index 5f947eb..c906921 100644 --- a/docs/supported-clients.rst +++ b/docs/postgresql-clients.rst @@ -1,5 +1,5 @@ -Supported Clients -================= +PostgreSQL Clients +================== vsql can run as a server that is compatible with PostgreSQL clients. However, some clients will execute complex or highly PostgreSQL-specific queries during diff --git a/docs/server.rst b/docs/server.rst index 9a26175..41335d7 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -4,7 +4,7 @@ Server vsql can be run as a server and any PostgreSQL-compatible driver can access it. This is ideal if you want to use a more familar or feature rich database client. -See the :doc:`list of supported clients here`. +See the :doc:`list of supported clients here`. Now run it with (if the file does not exist it will be created): diff --git a/docs/snippets.rst b/docs/snippets.rst new file mode 100644 index 0000000..68174d4 --- /dev/null +++ b/docs/snippets.rst @@ -0,0 +1,322 @@ +.. |br| raw:: html + +
+ +.. |v.Boolean| replace:: + Possible values for a BOOLEAN. + +.. |v.Boolean.str| replace:: + Returns ``TRUE``, ``FALSE`` or ``UNKNOWN``. + +.. |v.Column| replace:: + A column definition. + +.. |v.Column.name| replace:: + name is case-sensitive. The name is equivilent to using a deliminated + identifier (with double quotes). + +.. |v.Column.not_null| replace:: + not_null will be true if ``NOT NULL`` was specified on the column. + +.. |v.Column.str| replace:: + str returns the column definition like: + |br| |br| + "foo" INT + BAR DOUBLE PRECISION NOT NULL + +.. |v.Column.typ| replace:: + typ of the column contains more specifics like size and precision. + +.. |v.Connection| replace:: + A Connection allows querying and other introspection for a database file. Use + open() or open_database() to create a Connection. + +.. |v.Connection.prepare| replace:: + prepare returns a precompiled statement that can be executed multiple times + with different provided parameters. + +.. |v.Connection.query| replace:: + query executes a statement. If there is a result set it will be returned. + +.. |v.Connection.register_function| replace:: + register_function will register a function that can be used in SQL + expressions. + +.. |v.Connection.register_virtual_table| replace:: + register_virtual_table will register a function that can provide data at + runtime to a virtual table. + +.. |v.ConnectionOptions| replace:: + ConnectionOptions can modify the behavior of a connection when it is opened. + You should not create the ConnectionOptions instance manually. Instead, use + default_connection_options() as a starting point and modify the attributes. + +.. |v.ConnectionOptions.mutex| replace:: + In short, vsql (with default options) when dealing with concurrent + read/write access to single file provides the following protections: + |br| |br| + - Fine: Multiple processes open() the same file. + |br| |br| + - Fine: Multiple goroutines sharing an open() on the same file. + |br| |br| + - Bad: Multiple goroutines open() the same file. + |br| |br| + The mutex option will protect against the third Bad case if you + provide the same mutex instance to all open() calls: + |br| |br| + mutex := sync.new_rwmutex() // only create one of these + |br| |br| + mut options := default_connection_options() + options.mutex = mutex + |br| |br| + Since locking all database isn't ideal. You could provide a consistent + RwMutex that belongs to each file - such as from a map. + +.. |v.ConnectionOptions.now| replace:: + now allows you to override the wall clock that is used. The Time must be + in UTC with a separate offset for the current local timezone (in positive + or negative minutes). + +.. |v.ConnectionOptions.page_size| replace:: + Warning: This only works for :memory: databases. Configuring it for + file-based databases will either be ignored or causes crashes. + +.. |v.ConnectionOptions.query_cache| replace:: + query_cache contains the precompiled prepared statements that can be + reused. This makes execution much faster as parsing the SQL is extremely + expensive. + |br| |br| + By default each connection will be given its own query cache. However, + you can safely share a single cache over multiple connections and you are + encouraged to do so. + +.. |v.PreparedStmt| replace:: + A prepared statement is compiled and validated, but not executed. It can then + be executed with a set of host parameters to be substituted into the + statement. Each invocation requires all host parameters to be passed in. + +.. |v.PreparedStmt.query| replace:: + Execute the prepared statement. + +.. |v.QueryCache| replace:: + A QueryCache improves the performance of parsing by caching previously cached + statements. By default, a new QueryCache is created for each Connection. + However, you can share a single QueryCache safely amung multiple connections + for even better performance. See ConnectionOptions. + +.. |v.Result| replace:: + A Result contains zero or more rows returned from a query. + |br| |br| + See next() for an example on iterating rows in a Result. + +.. |v.Result.columns| replace:: + The columns provided for each row (even if there are zero rows.) + +.. |v.Result.elapsed_exec| replace:: + The time is took to execute the query. + +.. |v.Result.elapsed_parse| replace:: + The time it took to parse/compile the query before running it. + +.. |v.Result.next| replace:: + next provides the iteration for V, use it like: + |br| |br| + for row in result { } + +.. |v.Row| replace:: + Represents a single row which may contain one or more columns. + +.. |v.Row.get| replace:: + get returns the underlying Value. It will return an error if the column does + not exist. + +.. |v.Row.get_bool| replace:: + get_bool only works on a BOOLEAN value. If the column permits NULL values it + will be represented as UNKNOWN. + |br| |br| + An error is returned if the type is not a BOOLEAN or the column name does not + exist. + +.. |v.Row.get_f64| replace:: + get_f64 will only work for columns that are numerical (DOUBLE PRECISION, + FLOAT, REAL, etc). If the value is NULL, 0 will be returned. See get_null(). + +.. |v.Row.get_int| replace:: + get_int will only work for columns that are integers (SMALLINT, INTEGER or + BIGINT). If the value is NULL, 0 will be returned. See get_null(). + +.. |v.Row.get_null| replace:: + get_null will return true if the column name is NULL. An error will be + returned if the column does not exist. + +.. |v.Row.get_string| replace:: + get_string is the most flexible getter and will try to coerce the value + (including non-strings like numbers, booleans, NULL, etc) into some kind of + string. + |br| |br| + An error is only returned if the column does not exist. + +.. |v.SQLState| replace:: + SQLState is a compatible V error. It contains a human-readable message and' + the SQLSTATE code. + +.. |v.SQLState.code| replace:: + Is the integer representation of the SQLSTATE. Convert to a string with + sqlstate_from_int. + +.. |v.SQLState.msg| replace:: + Provides the human-readable message. + +.. |v.SQLType| replace:: + Represents the fundamental SQL type. + +.. |v.SQLType.str| replace:: + The SQL representation, such as ``TIME WITHOUT TIME ZONE``. + +.. |v.Time| replace:: + Time is the internal way that time is represented and provides other + conversions such as to/from storage and to/from V's native time.Time. + +.. |v.Time.str| replace:: + Returns the Time formatted based on its type. + +.. |v.Time.t| replace:: + Internal V time represenation. + +.. |v.Time.time_zone| replace:: + Number of minutes from 00:00 (positive or negative) + +.. |v.Time.typ| replace:: + typ.size is the precision (0 to 6) + +.. |v.Type| replace:: + Represents a fully-qualified SQL type. + +.. |v.Type.not_null| replace:: + Is NOT NULL? + +.. |v.Type.size| replace:: + The size specified for the type. + +.. |v.Type.str| replace:: + The SQL representation, such as ``TIME(3) WITHOUT TIME ZONE``. + +.. |v.Type.typ| replace:: + Base SQL type. + +.. |v.Value| replace:: + A single value. It contains it's type information in ``typ``. + +.. |v.Value.cmp| replace:: + cmp returns for the first argument: + |br| |br| + -1 if v < v2 + 0 if v == v2 + 1 if v > v2 + |br| |br| + The SQL standard doesn't define if NULLs should be always ordered first or + last. In vsql, NULLs are always considered to be less than any other non-null + value. The second return value will be true if either value is NULL. + |br| |br| + Or an error if the values are different types (cannot be compared). + +.. |v.Value.is_null| replace:: + Used by all types (including those that have NULL built in like BOOLEAN). + +.. |v.Value.str| replace:: + The string representation of this value. Different types will have different + formatting. + +.. |v.Value.typ| replace:: + TODO(elliotchance): Make these non-mutable. + The type of this Value. + +.. |v.VirtualTableProviderFn| replace:: + A function than will provide rows to a virtual table. + +.. |v.default_connection_options| replace:: + default_connection_options returns the sensible defaults used by open() and + the correct base to provide your own option overrides. See ConnectionOptions. + +.. |v.new_bigint_value| replace:: + new_bigint_value creates a ``BIGINT`` value. + +.. |v.new_boolean_value| replace:: + new_boolean_value creates a ``TRUE`` or ``FALSE`` value. For ``UNKNOWN`` (the + ``BOOLEAN`` equivilent of NULL) you will need to use ``new_unknown_value``. + +.. |v.new_character_value| replace:: + new_character_value creates a ``CHARACTER`` value. The value will be padded + with spaces up to the size specified. + +.. |v.new_date_value| replace:: + new_date_value creates a ``DATE`` value. + +.. |v.new_double_precision_value| replace:: + new_double_precision_value creates a ``DOUBLE PRECISION`` value. + +.. |v.new_integer_value| replace:: + new_integer_value creates an ``INTEGER`` value. + +.. |v.new_null_value| replace:: + new_null_value creates a NULL value of a specific type. In SQL, all NULL + values need to have a type. + +.. |v.new_query_cache| replace:: + Create a new query cache. + +.. |v.new_real_value| replace:: + new_real_value creates a ``REAL`` value. + +.. |v.new_smallint_value| replace:: + new_smallint_value creates a ``SMALLINT`` value. + +.. |v.new_time_value| replace:: + new_time_value creates a ``TIME`` value. + +.. |v.new_timestamp_value| replace:: + new_timestamp_value creates a ``TIMESTAMP`` value. + +.. |v.new_unknown_value| replace:: + new_unknown_value returns an ``UNKNOWN`` value. This is the ``NULL`` + representation of ``BOOLEAN``. + +.. |v.new_varchar_value| replace:: + new_varchar_value creates a ``CHARACTER VARYING`` value. + +.. |v.open| replace:: + open is the convenience function for open_database() with default options. + +.. |v.open_database| replace:: + open_database will open an existing database file or create a new file if the + path does not exist. + |br| |br| + If the file does exist, open_database will assume that the file is a valid + database file (not corrupt). Otherwise unexpected behavior or even a crash + may occur. + |br| |br| + The special file name ":memory:" can be used to create an entirely in-memory + database. This will be faster but all data will be lost when the connection + is closed. + |br| |br| + open_database can be used concurrently for reading and writing to the same + file and provides the following default protections: + |br| |br| + - Fine: Multiple processes open_database() the same file. + |br| |br| + - Fine: Multiple goroutines sharing an open_database() on the same file. + |br| |br| + - Bad: Multiple goroutines open_database() the same file. + |br| |br| + See ConnectionOptions and default_connection_options(). + +.. |v.sqlstate_from_int| replace:: + sqlstate_from_int performs the inverse operation of sqlstate_to_int. + +.. |v.sqlstate_to_int| replace:: + sqlstate_to_int converts the 5 character SQLSTATE code (such as "42P01") into + an integer representation. The returned value can be converted back to its + respective string by using sqlstate_from_int(). + |br| |br| + If code is invalid the result will be unexpected. + diff --git a/docs/v-client-library.rst b/docs/v-client-library.rst new file mode 100644 index 0000000..9c2f9e8 --- /dev/null +++ b/docs/v-client-library.rst @@ -0,0 +1,414 @@ +V Client Library +================ + +.. include:: snippets.rst + +.. contents:: + +Install/Update +-------------- + +Install or update to the latest with: + +.. code-block:: sh + + v install elliotchance.vsql + +Example +------- + +.. code-block:: text + + import elliotchance.vsql.vsql + + fn example() ? { + mut db := vsql.open('test.vsql') ? + + // All SQL commands use query(): + db.query('CREATE TABLE foo (x DOUBLE PRECISION)') ? + db.query('INSERT INTO foo (x) VALUES (1.23)') ? + db.query('INSERT INTO foo (x) VALUES (4.56)') ? + + // Iterate through a result: + result := db.query('SELECT * FROM foo') ? + println(result.columns) + + for row in result { + println(row.get_f64('X') ?) + } + + // See SQLSTATE (Errors) below for more examples. + } + +Outputs: + +.. code-block:: text + + ['A'] + 1.23 + 4.56 + +V Module +-------- + +enum Boolean +^^^^^^^^^^^^ + +|v.Boolean| + +See definition in +`value.v `_. + +fn Boolean.str() string +*********************** + +|v.Boolean.str| + +struct Column +^^^^^^^^^^^^^ + +|v.Column| + +name string +*********** + +|v.Column.name| + +not_null bool +************* + +|v.Column.not_null| + +typ Type +******** + +|v.Column.typ| + +fn Column.str() string +********************** + +|v.Column.str| + +struct Connection +^^^^^^^^^^^^^^^^^ + +|v.Connection| + +fn open(path string) ?&Connection +********************************* + +|v.open| + +fn open_database(path string, options ConnectionOptions) ?&Connection +********************************************************************* + +|v.open_database| + +fn Connection.prepare(sql string) ?PreparedStmt +*********************************************** + +|v.Connection.prepare| + +fn Connection.query(sql string) ?Result +*************************************** + +|v.Connection.query| + +fn Connection.register_function(prototype string, func fn ([]Value) ?Value) ? +***************************************************************************** + +|v.Connection.register_function| + +fn Connection.register_virtual_table(create_table string, data VirtualTableProviderFn) ? +**************************************************************************************** + +|v.Connection.register_virtual_table| + +struct ConnectionOptions +^^^^^^^^^^^^^^^^^^^^^^^^ + +|v.ConnectionOptions| + +mutex &sync.RwMutex +******************* + +|v.ConnectionOptions.mutex| + +now fn () (time.Time, i16) +************************** + +|v.ConnectionOptions.now| + +page_size int +************* + +|v.ConnectionOptions.page_size| + +query_cache &QueryCache +*********************** + +|v.ConnectionOptions.query_cache| + +fn default_connection_options() ConnectionOptions +************************************************* + +|v.default_connection_options| + +struct PreparedStmt +^^^^^^^^^^^^^^^^^^^ + +fn PreparedStmt.query(params map[string]Value) ?Result +****************************************************** + +|v.PreparedStmt.query| + +struct QueryCache +^^^^^^^^^^^^^^^^^ + +new_query_cache() &QueryCache +***************************** + +|v.new_query_cache| + +struct Result +^^^^^^^^^^^^^ + +|v.Result| + +columns []Column +**************** + +|v.Result.columns| + +elapsed_exec time.Duration +************************** + +|v.Result.elapsed_exec| + +elapsed_parse time.Duration +*************************** + +|v.Result.elapsed_parse| + +fn Result.next() ?Row +********************* + +|v.Result.next| + +struct Row +^^^^^^^^^^ + +|v.Row| + +get_null(name string) ?bool +*************************** + +|v.Row.get_null| + +get_f64(name string) ?f64 +************************* + +|v.Row.get_f64| + +get_int(name string) ?int +************************* + +|v.Row.get_int| + +get_string(name string) ?string +******************************* + +|v.Row.get_string| + +get_bool(name string) ?Boolean +****************************** + +|v.Row.get_bool| + +get(name string) ?Value +*********************** + +|v.Row.get| + +struct SQLState +^^^^^^^^^^^^^^^ + +|v.SQLState| + +fn sqlstate_from_int(code int) string +************************************* + +|v.sqlstate_from_int| + +fn sqlstate_to_int(code string) int +*********************************** + +|v.sqlstate_to_int| + +fn SQLState.code() int +********************** + +|v.SQLState.code| + +fn SQLState.msg() string +************************ + +|v.SQLState.msg| + +enum SQLType +^^^^^^^^^^^^ + +|v.SQLType| + +See definition in +`type.v `_. + +fn SQLType.str() string +*********************** + +|v.SQLType.str| + +struct Time +^^^^^^^^^^^ + +|v.Time| + +t time.Time +*********** + +|v.Time.t| + +typ Type +******** + +|v.Time.typ| + +time_zone i16 +************* + +|v.Time.time_zone| + +fn Time.str() string +******************** + +|v.Time.str| + +struct Type +^^^^^^^^^^^ + +|v.Type| + +not_null bool +************* + +|v.Type.not_null| + +size int +******** + +|v.Type.size| + +fn Type.str() string +******************** + +|v.Type.str| + +typ SQLType +*********** + +|v.Type.typ| + +struct Value +^^^^^^^^^^^^ + +|v.Value| + +is_null bool +************ + +|v.Value.is_null| + +typ Type +******** + +|v.Value.typ| + +fn new_bigint_value(x i64) Value +******************************** + +|v.new_bigint_value| + +fn new_boolean_value(b bool) Value +********************************** + +|v.new_boolean_value| + +fn new_character_value(x string, size int) Value +************************************************ + +|v.new_character_value| + +fn new_date_value(ts string) ?Value +*********************************** + +|v.new_date_value| + +fn new_double_precision_value(x f64) Value +****************************************** + +|v.new_double_precision_value| + +fn new_integer_value(x int) Value +********************************* + +|v.new_integer_value| + +fn new_null_value(typ SQLType) Value +************************************ + +|v.new_null_value| + +fn new_real_value(x f32) Value +****************************** + +|v.new_real_value| + +fn new_smallint_value(x i16) Value +********************************** + +|v.new_smallint_value| + +fn new_time_value(ts string) ?Value +*********************************** + +|v.new_time_value| + +fn new_timestamp_value(ts string) ?Value +**************************************** + +|v.new_timestamp_value| + +fn new_unknown_value() Value +**************************** + +|v.new_unknown_value| + +fn new_varchar_value(x string, size int) Value +********************************************** + +|v.new_varchar_value| + +fn Value.cmp(v2 Value) ?(int, bool) +*********************************** + +|v.Value.cmp| + +fn Value.str() string +********************* + +|v.Value.str| + +type VirtualTableProviderFn +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +|v.VirtualTableProviderFn| diff --git a/docs/v-module.rst b/docs/v-module.rst deleted file mode 100644 index 19528b1..0000000 --- a/docs/v-module.rst +++ /dev/null @@ -1,42 +0,0 @@ -V Module -======== - -Install or update to the latest with: - -.. code-block:: sh - - v install elliotchance.vsql - -.. code-block:: text - - import elliotchance.vsql.vsql - - fn example() ? { - mut db := vsql.open('test.vsql') ? - - // All SQL commands use query(): - db.query('CREATE TABLE foo (x DOUBLE PRECISION)') ? - db.query('INSERT INTO foo (x) VALUES (1.23)') ? - db.query('INSERT INTO foo (x) VALUES (4.56)') ? - - // Iterate through a result: - result := db.query('SELECT * FROM foo') ? - println(result.columns) - - for row in result { - println(row.get_f64('X') ?) - } - - // See SQLSTATE (Errors) below for more examples. - } - -Outputs: - -.. code-block:: text - - ['A'] - 1.23 - 4.56 - -You can find the documentation for a -`Row here `_. diff --git a/examples/virtual-tables.v b/examples/virtual-tables.v index 64029b2..f08f8cb 100644 --- a/examples/virtual-tables.v +++ b/examples/virtual-tables.v @@ -30,8 +30,8 @@ fn example() ? { } assert lines == [ - 'VIRTUAL TABLE FOO (FOO.num INTEGER, FOO.WORD CHARACTER VARYING(32))', - 'EXPR (num INTEGER, WORD CHARACTER VARYING(32))', + 'VIRTUAL TABLE FOO ("FOO.num" INTEGER, FOO.WORD CHARACTER VARYING(32))', + 'EXPR ("num" INTEGER, WORD CHARACTER VARYING(32))', ] result := db.query('SELECT * FROM foo')? diff --git a/generate-snippets.py b/generate-snippets.py new file mode 100644 index 0000000..a74b770 --- /dev/null +++ b/generate-snippets.py @@ -0,0 +1,30 @@ +from os import listdir +from os.path import isfile, join + +src = 'vsql' + +v_files = [join(src, f) for f in listdir(src) if isfile(join(src, f)) and f.endswith('.v')] +snippets = { + 'br': """.. |br| raw:: html + +
+""" +} + + +for v_file in sorted(v_files): + with open(v_file) as f: + snippet = [] + for line in [line.rstrip() for line in f]: + if line.strip().startswith('//'): + snippet.append(line.strip()[2:].strip()) + else: + if len(snippet) > 0: + if snippet[-1].startswith('snippet:'): + snippet_name = snippet[-1].split(':')[1].strip() + block = ' ' + '\n '.join([element or '|br| |br|' for element in snippet[:-2]]) + '\n' + snippets[snippet_name] = f'.. |{snippet_name}| replace::\n{block}' + snippet = [] + +for snippet_name in sorted(snippets): + print(snippets[snippet_name]) diff --git a/tests/as.sql b/tests/as.sql index cd8737b..058fb1c 100644 --- a/tests/as.sql +++ b/tests/as.sql @@ -11,7 +11,7 @@ SELECT 1 AS bob FROM t1; EXPLAIN SELECT 1 AS "Bob" FROM t1; -- EXPLAIN: TABLE T1 (T1.X INTEGER) --- EXPLAIN: EXPR (Bob BIGINT) +-- EXPLAIN: EXPR ("Bob" BIGINT) SELECT 1 AS "Bob" FROM t1; -- Bob: 1 diff --git a/tests/values.sql b/tests/values.sql index e903cc0..53aefb1 100644 --- a/tests/values.sql +++ b/tests/values.sql @@ -27,9 +27,9 @@ EXPLAIN SELECT * FROM (VALUES 1, 'foo', TRUE); EXPLAIN SELECT * FROM (VALUES 1, 'foo', TRUE) AS t1 (abc, col2, "f"); -- EXPLAIN: T1: --- EXPLAIN: VALUES (ABC BIGINT, COL2 CHARACTER VARYING, f BOOLEAN) = ROW(1, 'foo', TRUE) --- EXPLAIN: TABLE T1 (T1.ABC BIGINT, T1.COL2 CHARACTER VARYING, T1.f BOOLEAN) --- EXPLAIN: EXPR (ABC BIGINT, COL2 CHARACTER VARYING, f BOOLEAN) +-- EXPLAIN: VALUES (ABC BIGINT, COL2 CHARACTER VARYING, "f" BOOLEAN) = ROW(1, 'foo', TRUE) +-- EXPLAIN: TABLE T1 (T1.ABC BIGINT, T1.COL2 CHARACTER VARYING, "T1.f" BOOLEAN) +-- EXPLAIN: EXPR (ABC BIGINT, COL2 CHARACTER VARYING, "f" BOOLEAN) SELECT * FROM (VALUES 1, 'foo', TRUE) AS t1 (abc, col2, "f"); -- ABC: 1 COL2: foo f: TRUE diff --git a/vsql/connection.v b/vsql/connection.v index c49553c..b68e815 100644 --- a/vsql/connection.v +++ b/vsql/connection.v @@ -7,6 +7,10 @@ import os import sync import time +// A Connection allows querying and other introspection for a database file. Use +// open() or open_database() to create a Connection. +// +// snippet: v.Connection [heap] pub struct Connection { // path is the file name of the database. It can be the special name @@ -31,6 +35,8 @@ mut: } // open is the convenience function for open_database() with default options. +// +// snippet: v.open pub fn open(path string) ?&Connection { return open_database(path, default_connection_options()) } @@ -50,10 +56,14 @@ pub fn open(path string) ?&Connection { // file and provides the following default protections: // // - Fine: Multiple processes open_database() the same file. +// // - Fine: Multiple goroutines sharing an open_database() on the same file. +// // - Bad: Multiple goroutines open_database() the same file. // // See ConnectionOptions and default_connection_options(). +// +// snippet: v.open_database pub fn open_database(path string, options ConnectionOptions) ?&Connection { if path == ':memory:' { return open_connection(path, options) @@ -139,6 +149,10 @@ fn (mut c Connection) release_read_connection() { c.options.mutex.runlock() } +// prepare returns a precompiled statement that can be executed multiple times +// with different provided parameters. +// +// snippet: v.Connection.prepare pub fn (mut c Connection) prepare(sql string) ?PreparedStmt { t := start_timer() stmt, params, explain := c.query_cache.parse(sql) or { @@ -150,6 +164,9 @@ pub fn (mut c Connection) prepare(sql string) ?PreparedStmt { return PreparedStmt{stmt, params, explain, &c, elapsed_parse} } +// query executes a statement. If there is a result set it will be returned. +// +// snippet: v.Connection.query pub fn (mut c Connection) query(sql string) ?Result { if c.storage.transaction_state == .aborted { return sqlstate_25p02() @@ -166,7 +183,7 @@ pub fn (mut c Connection) query(sql string) ?Result { } } -pub fn (mut c Connection) register_func(func Func) ? { +fn (mut c Connection) register_func(func Func) ? { c.funcs << func } @@ -194,6 +211,10 @@ fn (c Connection) find_function(func_name string, arg_types []Type) ?Func { return sqlstate_42883('function does not exist: ${func_name}(${arg_types.map(it.str()).join(', ')})') } +// register_function will register a function that can be used in SQL +// expressions. +// +// snippet: v.Connection.register_function pub fn (mut c Connection) register_function(prototype string, func fn ([]Value) ?Value) ? { // TODO(elliotchance): A rather crude way to decode the prototype... parts := prototype.replace('(', '|').replace(')', '|').split('|') @@ -210,6 +231,10 @@ pub fn (mut c Connection) register_function(prototype string, func fn ([]Value) c.register_func(Func{function_name, arg_types, false, func, return_type})? } +// register_virtual_table will register a function that can provide data at +// runtime to a virtual table. +// +// snippet: v.Connection.register_virtual_table pub fn (mut c Connection) register_virtual_table(create_table string, data VirtualTableProviderFn) ? { // Registering virtual tables does not need use query cache. mut tokens := tokenize(create_table) @@ -229,6 +254,11 @@ pub fn (mut c Connection) register_virtual_table(create_table string, data Virtu return error('must provide a CREATE TABLE statement') } +// ConnectionOptions can modify the behavior of a connection when it is opened. +// You should not create the ConnectionOptions instance manually. Instead, use +// default_connection_options() as a starting point and modify the attributes. +// +// snippet: v.ConnectionOptions struct ConnectionOptions { pub mut: // query_cache contains the precompiled prepared statements that can be @@ -238,15 +268,21 @@ pub mut: // By default each connection will be given its own query cache. However, // you can safely share a single cache over multiple connections and you are // encouraged to do so. + // + // snippet: v.ConnectionOptions.query_cache query_cache &QueryCache // Warning: This only works for :memory: databases. Configuring it for // file-based databases will either be ignored or causes crashes. + // + // snippet: v.ConnectionOptions.page_size page_size int // In short, vsql (with default options) when dealing with concurrent // read/write access to single file provides the following protections: // // - Fine: Multiple processes open() the same file. + // // - Fine: Multiple goroutines sharing an open() on the same file. + // // - Bad: Multiple goroutines open() the same file. // // The mutex option will protect against the third Bad case if you @@ -259,15 +295,21 @@ pub mut: // // Since locking all database isn't ideal. You could provide a consistent // RwMutex that belongs to each file - such as from a map. + // + // snippet: v.ConnectionOptions.mutex mutex &sync.RwMutex // now allows you to override the wall clock that is used. The Time must be // in UTC with a separate offset for the current local timezone (in positive // or negative minutes). + // + // snippet: v.ConnectionOptions.now now fn () (time.Time, i16) } // default_connection_options returns the sensible defaults used by open() and // the correct base to provide your own option overrides. See ConnectionOptions. +// +// snippet: v.default_connection_options fn default_connection_options() ConnectionOptions { return ConnectionOptions{ query_cache: new_query_cache() diff --git a/vsql/prepare.v b/vsql/prepare.v index f1c10e0..72b0fd4 100644 --- a/vsql/prepare.v +++ b/vsql/prepare.v @@ -1,13 +1,15 @@ -// prepare.v is for prepared statements. A prepared statement is compiled and -// validated, but not executed. It can then be executed with a set of host -// parameters to be substituted into the statement. Each invocation requires all -// host parameters to be passed in. +// prepare.v is for prepared statements. module vsql import time -struct PreparedStmt { +// A prepared statement is compiled and validated, but not executed. It can then +// be executed with a set of host parameters to be substituted into the +// statement. Each invocation requires all host parameters to be passed in. +// +// snippet: v.PreparedStmt +pub struct PreparedStmt { stmt Stmt // params can be set on the statement and will be merged with the extra // params at execution time. If name collisions occur, the params provided @@ -22,6 +24,9 @@ mut: elapsed_parse time.Duration } +// Execute the prepared statement. +// +// snippet: v.PreparedStmt.query pub fn (mut p PreparedStmt) query(params map[string]Value) ?Result { return p.query_internal(params) or { p.c.storage.transaction_aborted() diff --git a/vsql/query_cache.v b/vsql/query_cache.v index 452b6b2..8c08e08 100644 --- a/vsql/query_cache.v +++ b/vsql/query_cache.v @@ -8,13 +8,22 @@ module vsql +// A QueryCache improves the performance of parsing by caching previously cached +// statements. By default, a new QueryCache is created for each Connection. +// However, you can share a single QueryCache safely amung multiple connections +// for even better performance. See ConnectionOptions. +// +// snippet: v.QueryCache [heap] -struct QueryCache { +pub struct QueryCache { mut: stmts map[string]Stmt } -fn new_query_cache() &QueryCache { +// Create a new query cache. +// +// snippet: v.new_query_cache +pub fn new_query_cache() &QueryCache { return &QueryCache{} } diff --git a/vsql/result.v b/vsql/result.v index 8b0a806..45b453d 100644 --- a/vsql/result.v +++ b/vsql/result.v @@ -5,12 +5,28 @@ module vsql import time +// A Result contains zero or more rows returned from a query. +// +// See next() for an example on iterating rows in a Result. +// +// snippet: v.Result struct Result { + // rows is not public because in the future this may end up being a cursor. + // You should use V iteration to read the rows. + rows []Row pub: - columns []Column - rows []Row + // The columns provided for each row (even if there are zero rows.) + // + // snippet: v.Result.columns + columns []Column + // The time it took to parse/compile the query before running it. + // + // snippet: v.Result.elapsed_parse elapsed_parse time.Duration - elapsed_exec time.Duration + // The time is took to execute the query. + // + // snippet: v.Result.elapsed_exec + elapsed_exec time.Duration mut: idx int } @@ -34,6 +50,11 @@ fn new_result_msg(msg string, elapsed_parse time.Duration, elapsed_exec time.Dur ], elapsed_parse, elapsed_exec) } +// next provides the iteration for V, use it like: +// +// for row in result { } +// +// snippet: v.Result.next pub fn (mut r Result) next() ?Row { if r.idx >= r.rows.len { return error('') diff --git a/vsql/row.v b/vsql/row.v index db68b6c..9236843 100644 --- a/vsql/row.v +++ b/vsql/row.v @@ -5,6 +5,9 @@ module vsql import time +// Represents a single row which may contain one or more columns. +// +// snippet: v.Row struct Row { mut: // id is the unique row identifier within the table. If the table has a @@ -16,7 +19,7 @@ mut: data map[string]Value } -pub fn new_row(data map[string]Value) Row { +fn new_row(data map[string]Value) Row { return Row{ data: data } @@ -24,6 +27,8 @@ pub fn new_row(data map[string]Value) Row { // get_null will return true if the column name is NULL. An error will be // returned if the column does not exist. +// +// snippet: v.Row.get_null pub fn (r Row) get_null(name string) ?bool { value := r.get(name)? @@ -32,6 +37,8 @@ pub fn (r Row) get_null(name string) ?bool { // get_f64 will only work for columns that are numerical (DOUBLE PRECISION, // FLOAT, REAL, etc). If the value is NULL, 0 will be returned. See get_null(). +// +// snippet: v.Row.get_f64 pub fn (r Row) get_f64(name string) ?f64 { value := r.get(name)? if value.typ.uses_f64() { @@ -41,8 +48,10 @@ pub fn (r Row) get_f64(name string) ?f64 { return error("cannot use get_f64('$name') when type is $value.typ") } -// get_int will only work for columns that are integeres (SMALLINT, INTEGER or +// get_int will only work for columns that are integers (SMALLINT, INTEGER or // BIGINT). If the value is NULL, 0 will be returned. See get_null(). +// +// snippet: v.Row.get_int pub fn (r Row) get_int(name string) ?int { value := r.get(name)? if value.typ.uses_int() { @@ -57,15 +66,19 @@ pub fn (r Row) get_int(name string) ?int { // string. // // An error is only returned if the column does not exist. +// +// snippet: v.Row.get_string pub fn (r Row) get_string(name string) ?string { return (r.get(name)?).str() } -// get_bool only works on a BOOLEAN value. If the value is UNKNOWN (same as -// NULL), false will be returned. See get_null() and get_unknown() respectively. +// get_bool only works on a BOOLEAN value. If the column permits NULL values it +// will be represented as UNKNOWN. // // An error is returned if the type is not a BOOLEAN or the column name does not // exist. +// +// snippet: v.Row.get_bool pub fn (r Row) get_bool(name string) ?Boolean { value := r.get(name)? @@ -79,7 +92,11 @@ pub fn (r Row) get_bool(name string) ?Boolean { } } -fn (r Row) get(name string) ?Value { +// get returns the underlying Value. It will return an error if the column does +// not exist. +// +// snippet: v.Row.get +pub fn (r Row) get(name string) ?Value { return r.data[name] or { // Be helpful and look for silly mistakes. for n, _ in r.data { diff --git a/vsql/sqlstate.v b/vsql/sqlstate.v index 0d078d7..afda470 100644 --- a/vsql/sqlstate.v +++ b/vsql/sqlstate.v @@ -12,6 +12,8 @@ module vsql // respective string by using sqlstate_from_int(). // // If code is invalid the result will be unexpected. +// +// snippet: v.sqlstate_to_int pub fn sqlstate_to_int(code string) int { upper_code := code.to_upper() @@ -36,6 +38,9 @@ fn sqlstate_ord(ch int) u8 { return u8(`A`) + (u8(ch) - 10) } +// sqlstate_from_int performs the inverse operation of sqlstate_to_int. +// +// snippet: v.sqlstate_from_int pub fn sqlstate_from_int(code int) string { mut b := []u8{len: 5} @@ -51,15 +56,26 @@ pub fn sqlstate_from_int(code int) string { return b.bytestr() } +// SQLState is a compatible V error. It contains a human-readable message and' +// the SQLSTATE code. +// +// snippet: v.SQLState struct SQLState { msg string code int } +// Provides the human-readable message. +// +// snippet: v.SQLState.msg fn (err SQLState) msg() string { return err.msg } +// Is the integer representation of the SQLSTATE. Convert to a string with +// sqlstate_from_int. +// +// snippet: v.SQLState.code fn (err SQLState) code() int { return err.code } diff --git a/vsql/table.v b/vsql/table.v index b7f77db..e45bb8a 100644 --- a/vsql/table.v +++ b/vsql/table.v @@ -2,15 +2,40 @@ module vsql +// A column definition. +// +// snippet: v.Column struct Column { pub: - name string - typ Type + // name is case-sensitive. The name is equivilent to using a deliminated + // identifier (with double quotes). + // + // snippet: v.Column.name + name string + // typ of the column contains more specifics like size and precision. + // + // snippet: v.Column.typ + typ Type + // not_null will be true if ``NOT NULL`` was specified on the column. + // + // snippet: v.Column.not_null not_null bool } -fn (c Column) str() string { - return c.name +// str returns the column definition like: +// +// "foo" INT +// BAR DOUBLE PRECISION NOT NULL +// +// snippet: v.Column.str +pub fn (c Column) str() string { + name := if c.name.is_upper() { c.name } else { '"$c.name"' } + mut f := '$name $c.typ' + if c.not_null { + f += ' NOT NULL' + } + + return f } type Columns = []Column @@ -18,11 +43,7 @@ type Columns = []Column fn (c Columns) str() string { mut s := []string{} for col in c { - mut f := '$col.name $col.typ' - if col.not_null { - f += ' NOT NULL' - } - s << f + s << col.str() } return s.join(', ') diff --git a/vsql/time.v b/vsql/time.v index 66feba0..d1a1ca7 100644 --- a/vsql/time.v +++ b/vsql/time.v @@ -55,12 +55,22 @@ const ( // Time is the internal way that time is represented and provides other // conversions such as to/from storage and to/from V's native time.Time. -struct Time { -mut: +// +// snippet: v.Time +pub struct Time { +pub mut: // typ.size is the precision (0 to 6) - typ Type - time_zone i16 // number of minutes from 00:00 (positive or negative) - t time.Time + // + // snippet: v.Time.typ + typ Type + // Number of minutes from 00:00 (positive or negative) + // + // snippet: v.Time.time_zone + time_zone i16 + // Internal V time represenation. + // + // snippet: v.Time.t + t time.Time } // This is an internal constructor, you will want to use new_timestamp_value @@ -243,6 +253,9 @@ fn (t Time) date_i64() i64 { return t.t.year * vsql.year_period + t.t.month * vsql.month_period + t.t.day * vsql.day_period } +// Returns the Time formatted based on its type. +// +// snippet: v.Time.str fn (t Time) str() string { return match t.typ.typ { .is_timestamp_with_time_zone, .is_timestamp_without_time_zone { diff --git a/vsql/type.v b/vsql/type.v index 3460a3c..f2e60a4 100644 --- a/vsql/type.v +++ b/vsql/type.v @@ -2,14 +2,28 @@ module vsql +// Represents a fully-qualified SQL type. +// +// snippet: v.Type struct Type { mut: - // TODO(elliotchance): Make these non-mutable. - typ SQLType - size int // the size specified for the type - not_null bool // NOT NULL? + // Base SQL type. + // + // snippet: v.Type.typ + typ SQLType + // The size specified for the type. + // + // snippet: v.Type.size + size int + // Is NOT NULL? + // + // snippet: v.Type.not_null + not_null bool } +// Represents the fundamental SQL type. +// +// snippet: v.SQLType enum SQLType { is_bigint // BIGINT is_boolean // BOOLEAN @@ -26,6 +40,9 @@ enum SQLType { is_timestamp_with_time_zone // TIMESTAMP WITH TIME ZONE } +// The SQL representation, such as ``TIME WITHOUT TIME ZONE``. +// +// snippet: v.SQLType.str fn (t SQLType) str() string { return match t { .is_bigint { 'BIGINT' } @@ -94,6 +111,9 @@ fn new_type(name string, size int) Type { } } +// The SQL representation, such as ``TIME(3) WITHOUT TIME ZONE``. +// +// snippet: v.Type.str fn (t Type) str() string { mut s := match t.typ { .is_bigint { diff --git a/vsql/value.v b/vsql/value.v index 38058ae..1d1e9a8 100644 --- a/vsql/value.v +++ b/vsql/value.v @@ -6,15 +6,20 @@ module vsql import strings -// Possible values for a BOOLEAN. These must not be negative values because they -// are encoded as u8 on disk. +// Possible values for a BOOLEAN. +// +// snippet: v.Boolean pub enum Boolean { + // These must not be negative values because they are encoded as u8 on disk. is_unknown = 0 // same as NULL is_false = 1 is_true = 2 } -fn (b Boolean) str() string { +// Returns ``TRUE``, ``FALSE`` or ``UNKNOWN``. +// +// snippet: v.Boolean.str +pub fn (b Boolean) str() string { return match b { .is_false { 'FALSE' } .is_true { 'TRUE' } @@ -22,11 +27,19 @@ fn (b Boolean) str() string { } } +// A single value. It contains it's type information in ``typ``. +// +// snippet: v.Value pub struct Value { pub mut: // TODO(elliotchance): Make these non-mutable. + // The type of this Value. + // + // snippet: v.Value.typ typ Type // Used by all types (including those that have NULL built in like BOOLEAN). + // + // snippet: v.Value.is_null is_null bool // BOOLEAN bool_value Boolean @@ -46,6 +59,10 @@ pub mut: is_coercible bool } +// new_null_value creates a NULL value of a specific type. In SQL, all NULL +// values need to have a type. +// +// snippet: v.new_null_value pub fn new_null_value(typ SQLType) Value { return Value{ typ: Type{typ, 0, false} @@ -53,6 +70,10 @@ pub fn new_null_value(typ SQLType) Value { } } +// new_boolean_value creates a ``TRUE`` or ``FALSE`` value. For ``UNKNOWN`` (the +// ``BOOLEAN`` equivilent of NULL) you will need to use ``new_unknown_value``. +// +// snippet: v.new_boolean_value pub fn new_boolean_value(b bool) Value { return Value{ typ: Type{.is_boolean, 0, false} @@ -60,6 +81,10 @@ pub fn new_boolean_value(b bool) Value { } } +// new_unknown_value returns an ``UNKNOWN`` value. This is the ``NULL`` +// representation of ``BOOLEAN``. +// +// snippet: v.new_unknown_value pub fn new_unknown_value() Value { return Value{ typ: Type{.is_boolean, 0, false} @@ -67,6 +92,9 @@ pub fn new_unknown_value() Value { } } +// new_double_precision_value creates a ``DOUBLE PRECISION`` value. +// +// snippet: v.new_double_precision_value pub fn new_double_precision_value(x f64) Value { return Value{ typ: Type{.is_double_precision, 0, false} @@ -74,6 +102,9 @@ pub fn new_double_precision_value(x f64) Value { } } +// new_integer_value creates an ``INTEGER`` value. +// +// snippet: v.new_integer_value pub fn new_integer_value(x int) Value { return Value{ typ: Type{.is_integer, 0, false} @@ -81,6 +112,9 @@ pub fn new_integer_value(x int) Value { } } +// new_bigint_value creates a ``BIGINT`` value. +// +// snippet: v.new_bigint_value pub fn new_bigint_value(x i64) Value { return Value{ typ: Type{.is_bigint, 0, false} @@ -88,6 +122,9 @@ pub fn new_bigint_value(x i64) Value { } } +// new_real_value creates a ``REAL`` value. +// +// snippet: v.new_real_value pub fn new_real_value(x f32) Value { return Value{ typ: Type{.is_real, 0, false} @@ -95,6 +132,9 @@ pub fn new_real_value(x f32) Value { } } +// new_smallint_value creates a ``SMALLINT`` value. +// +// snippet: v.new_smallint_value pub fn new_smallint_value(x i16) Value { return Value{ typ: Type{.is_smallint, 0, false} @@ -102,6 +142,9 @@ pub fn new_smallint_value(x i16) Value { } } +// new_varchar_value creates a ``CHARACTER VARYING`` value. +// +// snippet: v.new_varchar_value pub fn new_varchar_value(x string, size int) Value { return Value{ typ: Type{.is_varchar, size, false} @@ -109,7 +152,10 @@ pub fn new_varchar_value(x string, size int) Value { } } -// The value will be padded with spaces up to the size specified. +// new_character_value creates a ``CHARACTER`` value. The value will be padded +// with spaces up to the size specified. +// +// snippet: v.new_character_value pub fn new_character_value(x string, size int) Value { // TODO(elliotchance): Doesn't handle size < x.len @@ -119,6 +165,9 @@ pub fn new_character_value(x string, size int) Value { } } +// new_timestamp_value creates a ``TIMESTAMP`` value. +// +// snippet: v.new_timestamp_value pub fn new_timestamp_value(ts string) ?Value { t := new_timestamp_from_string(ts)? @@ -128,6 +177,9 @@ pub fn new_timestamp_value(ts string) ?Value { } } +// new_time_value creates a ``TIME`` value. +// +// snippet: v.new_time_value pub fn new_time_value(ts string) ?Value { t := new_time_from_string(ts)? @@ -137,6 +189,9 @@ pub fn new_time_value(ts string) ?Value { } } +// new_date_value creates a ``DATE`` value. +// +// snippet: v.new_date_value pub fn new_date_value(ts string) ?Value { t := new_date_from_string(ts)? @@ -166,6 +221,10 @@ fn (v Value) as_int() i64 { return i64(v.f64_value) } +// The string representation of this value. Different types will have different +// formatting. +// +// snippet: v.Value.str fn (v Value) str() string { if v.is_null && v.typ.typ != .is_boolean { return 'NULL' @@ -202,7 +261,9 @@ fn (v Value) str() string { // value. The second return value will be true if either value is NULL. // // Or an error if the values are different types (cannot be compared). -fn (v Value) cmp(v2 Value) ?(int, bool) { +// +// snippet: v.Value.cmp +pub fn (v Value) cmp(v2 Value) ?(int, bool) { if v.is_null && v2.is_null { return 0, true } diff --git a/vsql/virtual_table.v b/vsql/virtual_table.v index 6dcd57a..0b6d1c2 100644 --- a/vsql/virtual_table.v +++ b/vsql/virtual_table.v @@ -1,5 +1,8 @@ module vsql +// A function than will provide rows to a virtual table. +// +// snippet: v.VirtualTableProviderFn type VirtualTableProviderFn = fn (mut t VirtualTable) ? pub struct VirtualTable {